FPSMS-frontend
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

JobPickExecutionForm.tsx 16 KiB

6ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
4ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
6ヶ月前
3ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. // FPSMS-frontend/src/components/PickOrderSearch/PickExecutionForm.tsx
  2. "use client";
  3. import {
  4. Box,
  5. Button,
  6. Dialog,
  7. DialogActions,
  8. DialogContent,
  9. DialogTitle,
  10. FormControl,
  11. Grid,
  12. InputLabel,
  13. MenuItem,
  14. Select,
  15. TextField,
  16. Typography,
  17. } from "@mui/material";
  18. import { useCallback, useEffect, useState } from "react";
  19. import { useTranslation } from "react-i18next";
  20. import { GetPickOrderLineInfo, PickExecutionIssueData } from "@/app/api/pickOrder/actions";
  21. import { fetchEscalationCombo } from "@/app/api/user/actions";
  22. import { useSession } from "next-auth/react";
  23. import { SessionWithTokens } from "@/config/authConfig";
  24. import dayjs from 'dayjs';
  25. import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
  26. interface LotPickData {
  27. id: number;
  28. lotId: number;
  29. lotNo: string;
  30. expiryDate: string;
  31. location: string;
  32. stockUnit: string;
  33. inQty: number;
  34. outQty: number;
  35. holdQty: number;
  36. totalPickedByAllPickOrders: number;
  37. availableQty: number;
  38. requiredQty: number;
  39. actualPickQty: number;
  40. lotStatus: string;
  41. lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected';
  42. stockOutLineId?: number;
  43. stockOutLineStatus?: string;
  44. stockOutLineQty?: number;
  45. pickOrderLineId?: number;
  46. pickOrderId?: number;
  47. pickOrderCode?: string;
  48. }
  49. interface PickExecutionFormProps {
  50. open: boolean;
  51. onClose: () => void;
  52. onSubmit: (data: PickExecutionIssueData) => Promise<void>;
  53. selectedLot: LotPickData | null;
  54. selectedPickOrderLine: (GetPickOrderLineInfo & { pickOrderCode: string }) | null;
  55. pickOrderId?: number;
  56. pickOrderCreateDate: any;
  57. onNormalPickSubmit?: (lot: LotPickData, submitQty: number) => Promise<void>;
  58. // Remove these props since we're not handling normal cases
  59. // onNormalPickSubmit?: (lineId: number, lotId: number, qty: number) => Promise<void>;
  60. // selectedRowId?: number | null;
  61. }
  62. // 定义错误类型
  63. interface FormErrors {
  64. actualPickQty?: string;
  65. missQty?: string;
  66. badItemQty?: string;
  67. issueRemark?: string;
  68. handledBy?: string;
  69. badReason?: string;
  70. }
  71. const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
  72. open,
  73. onClose,
  74. onSubmit,
  75. selectedLot,
  76. selectedPickOrderLine,
  77. pickOrderId,
  78. pickOrderCreateDate,
  79. onNormalPickSubmit,
  80. }) => {
  81. const { t } = useTranslation();
  82. const [formData, setFormData] = useState<Partial<PickExecutionIssueData>>({});
  83. const [errors, setErrors] = useState<FormErrors>({});
  84. const [loading, setLoading] = useState(false);
  85. const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]);
  86. const [verifiedQty, setVerifiedQty] = useState<number>(0);
  87. const { data: session } = useSession() as { data: SessionWithTokens | null };
  88. const missSet = formData.missQty != null;
  89. const badItemSet = formData.badItemQty != null;
  90. const badPackageSet = (formData as any).badPackageQty != null;
  91. const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => {
  92. return lot.availableQty || 0;
  93. }, []);
  94. const calculateRequiredQty = useCallback((lot: LotPickData) => {
  95. // Use the original required quantity, not subtracting actualPickQty
  96. // The actualPickQty in the form should be independent of the database value
  97. return lot.requiredQty || 0;
  98. }, []);
  99. useEffect(() => {
  100. console.log('PickExecutionForm props:', {
  101. open,
  102. onNormalPickSubmit: typeof onNormalPickSubmit,
  103. hasOnNormalPickSubmit: !!onNormalPickSubmit,
  104. onSubmit: typeof onSubmit,
  105. });
  106. }, [open, onNormalPickSubmit, onSubmit]);
  107. // 获取处理人员列表
  108. useEffect(() => {
  109. const fetchHandlers = async () => {
  110. try {
  111. const escalationCombo = await fetchEscalationCombo();
  112. setHandlers(escalationCombo);
  113. } catch (error) {
  114. console.error("Error fetching handlers:", error);
  115. }
  116. };
  117. fetchHandlers();
  118. }, []);
  119. // 初始化表单数据 - 每次打开时都重新初始化
  120. useEffect(() => {
  121. if (open && selectedLot && selectedPickOrderLine && pickOrderId) {
  122. const getSafeDate = (dateValue: any): string => {
  123. if (!dateValue) return dayjs().format(INPUT_DATE_FORMAT);
  124. try {
  125. const date = dayjs(dateValue);
  126. if (!date.isValid()) {
  127. return dayjs().format(INPUT_DATE_FORMAT);
  128. }
  129. return date.format(INPUT_DATE_FORMAT);
  130. } catch {
  131. return dayjs().format(INPUT_DATE_FORMAT);
  132. }
  133. };
  134. // Initialize verified quantity to the received quantity (actualPickQty)
  135. const initialVerifiedQty = selectedLot.actualPickQty || 0;
  136. setVerifiedQty(initialVerifiedQty);
  137. console.log("=== PickExecutionForm Debug ===");
  138. console.log("selectedLot:", selectedLot);
  139. console.log("initialVerifiedQty:", initialVerifiedQty);
  140. console.log("=== End Debug ===");
  141. setFormData({
  142. pickOrderId: pickOrderId,
  143. pickOrderCode: selectedPickOrderLine.pickOrderCode,
  144. pickOrderCreateDate: getSafeDate(pickOrderCreateDate),
  145. pickExecutionDate: dayjs().format(INPUT_DATE_FORMAT),
  146. pickOrderLineId: selectedPickOrderLine.id,
  147. itemId: selectedPickOrderLine.itemId,
  148. itemCode: selectedPickOrderLine.itemCode,
  149. itemDescription: selectedPickOrderLine.itemName,
  150. lotId: selectedLot.lotId,
  151. lotNo: selectedLot.lotNo,
  152. storeLocation: selectedLot.location,
  153. requiredQty: selectedLot.requiredQty,
  154. actualPickQty: initialVerifiedQty,
  155. missQty: undefined,
  156. badItemQty: undefined,
  157. badPackageQty: undefined,
  158. issueRemark: "",
  159. pickerName: "",
  160. handledBy: undefined,
  161. reason: "",
  162. badReason: "",
  163. });
  164. }
  165. // 只在 open 状态改变时重新初始化,移除其他依赖
  166. // eslint-disable-next-line react-hooks/exhaustive-deps
  167. }, [open]);
  168. const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => {
  169. setFormData(prev => ({ ...prev, [field]: value }));
  170. // Update verified quantity state when actualPickQty changes
  171. if (field === 'actualPickQty') {
  172. setVerifiedQty(value);
  173. }
  174. // 清除错误
  175. if (errors[field as keyof FormErrors]) {
  176. setErrors(prev => ({ ...prev, [field]: undefined }));
  177. }
  178. }, [errors]);
  179. // Updated validation logic (same as GoodPickExecutionForm)
  180. const validateForm = (): boolean => {
  181. const newErrors: FormErrors = {};
  182. const ap = Number(verifiedQty) || 0;
  183. const miss = Number(formData.missQty) || 0;
  184. const badItem = Number(formData.badItemQty) ?? 0;
  185. const badPackage = Number((formData as any).badPackageQty) ?? 0;
  186. const totalBadQty = badItem + badPackage;
  187. const total = ap + miss + totalBadQty;
  188. const availableQty = selectedLot?.availableQty || 0;
  189. // 1. Check actualPickQty cannot be negative
  190. if (ap < 0) {
  191. newErrors.actualPickQty = t("Qty cannot be negative");
  192. }
  193. // 2. Check actualPickQty cannot exceed available quantity
  194. if (ap > availableQty) {
  195. newErrors.actualPickQty = t("Actual pick qty cannot exceed available qty");
  196. }
  197. // 3. Check missQty and both bad qtys cannot be negative
  198. if (miss < 0) {
  199. newErrors.missQty = t("Invalid qty");
  200. }
  201. if (badItem < 0 || badPackage < 0) {
  202. newErrors.badItemQty = t("Invalid qty");
  203. }
  204. // 4. Total (actualPickQty + missQty + badItemQty + badPackageQty) cannot exceed lot available qty
  205. if (total > availableQty) {
  206. const errorMsg = t(
  207. "Total qty (actual pick + miss + bad) cannot exceed available qty: {available}",
  208. { available: availableQty }
  209. );
  210. newErrors.actualPickQty = errorMsg;
  211. newErrors.missQty = errorMsg;
  212. newErrors.badItemQty = errorMsg;
  213. }
  214. // 5. At least one field must have a value
  215. if (ap === 0 && miss === 0 && totalBadQty === 0) {
  216. newErrors.actualPickQty = t("Enter pick qty or issue qty");
  217. }
  218. setErrors(newErrors);
  219. return Object.keys(newErrors).length === 0;
  220. };
  221. const handleSubmit = async () => {
  222. if (!formData.pickOrderId || !selectedLot) {
  223. return;
  224. }
  225. // 增加 badPackageQty 判断,确保有坏包装会走 issue 流程
  226. const badPackageQty = Number((formData as any).badPackageQty) || 0;
  227. const isNormalPick = verifiedQty > 0
  228. && formData.missQty == 0
  229. && formData.badItemQty == 0
  230. && badPackageQty == 0;
  231. if (isNormalPick) {
  232. if (onNormalPickSubmit) {
  233. setLoading(true);
  234. try {
  235. console.log('Calling onNormalPickSubmit with:', { lot: selectedLot, submitQty: verifiedQty });
  236. await onNormalPickSubmit(selectedLot, verifiedQty);
  237. onClose();
  238. } catch (error) {
  239. console.error('Error submitting normal pick:', error);
  240. } finally {
  241. setLoading(false);
  242. }
  243. } else {
  244. console.warn('onNormalPickSubmit callback not provided');
  245. }
  246. return;
  247. }
  248. // ❌ 有问题(或全部为 0)才进入 Issue 提报流程
  249. if (!validateForm() || !formData.pickOrderId) {
  250. return;
  251. }
  252. const badItem = Number(formData.badItemQty) || 0;
  253. const badPackage = Number((formData as any).badPackageQty) || 0;
  254. const totalBadQty = badItem + badPackage;
  255. let badReason: string | undefined;
  256. if (totalBadQty > 0) {
  257. // assumption: only one of them is > 0
  258. badReason = badPackage > 0 ? "package_problem" : "quantity_problem";
  259. }
  260. setLoading(true);
  261. try {
  262. const submissionData: PickExecutionIssueData = {
  263. ...(formData as PickExecutionIssueData),
  264. actualPickQty: verifiedQty,
  265. lotId: formData.lotId ?? selectedLot?.lotId ?? 0,
  266. lotNo: formData.lotNo ?? selectedLot?.lotNo ?? '',
  267. pickOrderCode: formData.pickOrderCode ?? selectedPickOrderLine?.pickOrderCode ?? '',
  268. pickerName: session?.user?.name ?? '',
  269. missQty: formData.missQty ?? 0, // 这里:null/undefined → 0
  270. badItemQty: totalBadQty, // totalBadQty 下面用 ?? 0 算
  271. badReason,
  272. };
  273. await onSubmit(submissionData);
  274. onClose();
  275. } catch (error: any) {
  276. console.error('Error submitting pick execution issue:', error);
  277. alert(
  278. t("Failed to submit issue. Please try again.") +
  279. (error.message ? `: ${error.message}` : "")
  280. );
  281. } finally {
  282. setLoading(false);
  283. }
  284. };
  285. const handleClose = () => {
  286. setFormData({});
  287. setErrors({});
  288. setVerifiedQty(0);
  289. onClose();
  290. };
  291. if (!selectedLot || !selectedPickOrderLine) {
  292. return null;
  293. }
  294. const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot);
  295. const requiredQty = calculateRequiredQty(selectedLot);
  296. return (
  297. <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
  298. <DialogTitle>
  299. {t('Pick Execution Issue Form')} {/* Always show issue form title */}
  300. </DialogTitle>
  301. <DialogContent>
  302. <Box sx={{ mt: 2 }}>
  303. {/* Add instruction text */}
  304. <Grid container spacing={2}>
  305. <Grid item xs={12}>
  306. <Box sx={{ p: 2, backgroundColor: '#fff3cd', borderRadius: 1, mb: 2 }}>
  307. <Typography variant="body2" color="warning.main">
  308. <strong>{t('Note:')}</strong> {t('This form is for reporting issues only. You must report either missing items or bad items.')}
  309. </Typography>
  310. </Box>
  311. </Grid>
  312. {/* Keep the existing form fields */}
  313. <Grid item xs={6}>
  314. <TextField
  315. fullWidth
  316. label={t('Required Qty')}
  317. value={selectedLot?.requiredQty || 0}
  318. disabled
  319. variant="outlined"
  320. // helperText={t('Still need to pick')}
  321. />
  322. </Grid>
  323. <Grid item xs={6}>
  324. <TextField
  325. fullWidth
  326. label={t('Remaining Available Qty')}
  327. value={remainingAvailableQty}
  328. disabled
  329. variant="outlined"
  330. />
  331. </Grid>
  332. <Grid item xs={12}>
  333. <TextField
  334. fullWidth
  335. label={t('Actual Pick Qty')}
  336. type="number"
  337. inputProps={{
  338. inputMode: "numeric",
  339. pattern: "[0-9]*",
  340. min: 0,
  341. }}
  342. value={verifiedQty ?? ""}
  343. onChange={(e) => {
  344. const newValue = e.target.value === ""
  345. ? undefined
  346. : Math.max(0, Number(e.target.value) || 0);
  347. setVerifiedQty(newValue || 0);
  348. }}
  349. error={!!errors.actualPickQty}
  350. helperText={
  351. errors.actualPickQty || `${t("Max")}: ${remainingAvailableQty}`
  352. }
  353. variant="outlined"
  354. />
  355. </Grid>
  356. <Grid item xs={12}>
  357. <TextField
  358. fullWidth
  359. label={t('Missing item Qty')}
  360. type="number"
  361. inputProps={{
  362. inputMode: "numeric",
  363. pattern: "[0-9]*",
  364. min: 0,
  365. }}
  366. disabled={badItemSet || badPackageSet}
  367. value={formData.missQty || ""}
  368. onChange={(e) => {
  369. handleInputChange(
  370. "missQty",
  371. e.target.value === ""
  372. ? undefined
  373. : Math.max(0, Number(e.target.value) || 0)
  374. );
  375. }}
  376. error={!!errors.missQty}
  377. variant="outlined"
  378. />
  379. </Grid>
  380. <Grid item xs={12}>
  381. <TextField
  382. fullWidth
  383. label={t('Bad Item Qty')}
  384. type="number"
  385. inputProps={{
  386. inputMode: "numeric",
  387. pattern: "[0-9]*",
  388. min: 0,
  389. }}
  390. value={formData.badItemQty || ""}
  391. onChange={(e) => {
  392. const newBadItemQty = e.target.value === ""
  393. ? undefined
  394. : Math.max(0, Number(e.target.value) || 0);
  395. handleInputChange('badItemQty', newBadItemQty);
  396. }}
  397. error={!!errors.badItemQty}
  398. disabled={missSet || badPackageSet}
  399. helperText={errors.badItemQty}
  400. variant="outlined"
  401. />
  402. </Grid>
  403. <Grid item xs={12}>
  404. <TextField
  405. fullWidth
  406. label={t("Bad Package Qty")}
  407. type="number"
  408. inputProps={{
  409. inputMode: "numeric",
  410. pattern: "[0-9]*",
  411. min: 0,
  412. }}
  413. value={(formData as any).badPackageQty || ""}
  414. onChange={(e) => {
  415. handleInputChange(
  416. "badPackageQty",
  417. e.target.value === ""
  418. ? undefined
  419. : Math.max(0, Number(e.target.value) || 0)
  420. );
  421. }}
  422. disabled={missSet || badItemSet}
  423. error={!!errors.badItemQty}
  424. variant="outlined"
  425. />
  426. </Grid>
  427. <Grid item xs={12}>
  428. <FormControl fullWidth>
  429. <InputLabel>{t("Remark")}</InputLabel>
  430. <Select
  431. value={formData.reason || ""}
  432. onChange={(e) => handleInputChange("reason", e.target.value)}
  433. label={t("Remark")}
  434. >
  435. <MenuItem value="">{t("Select Remark")}</MenuItem>
  436. <MenuItem value="miss">{t("Edit")}</MenuItem>
  437. <MenuItem value="bad">{t("Just Complete")}</MenuItem>
  438. </Select>
  439. </FormControl>
  440. </Grid>
  441. </Grid>
  442. </Box>
  443. </DialogContent>
  444. <DialogActions>
  445. <Button onClick={handleClose} disabled={loading}>
  446. {t('Cancel')}
  447. </Button>
  448. <Button
  449. onClick={handleSubmit}
  450. variant="contained"
  451. disabled={loading}
  452. >
  453. {loading ? t('submitting') : t('submit')}
  454. </Button>
  455. </DialogActions>
  456. </Dialog>
  457. );
  458. };
  459. export default PickExecutionForm;