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

JobPickExecutionForm.tsx 16 KiB

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