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

JobPickExecutionForm.tsx 14 KiB

3ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
1週間前
2ヶ月前
1週間前
2ヶ月前
3ヶ月前
1週間前
3ヶ月前
1週間前
1週間前
3ヶ月前
1週間前
3ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
2ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
3ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  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. }
  70. const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
  71. open,
  72. onClose,
  73. onSubmit,
  74. selectedLot,
  75. selectedPickOrderLine,
  76. pickOrderId,
  77. pickOrderCreateDate,
  78. onNormalPickSubmit,
  79. }) => {
  80. const { t } = useTranslation();
  81. const [formData, setFormData] = useState<Partial<PickExecutionIssueData>>({});
  82. const [errors, setErrors] = useState<FormErrors>({});
  83. const [loading, setLoading] = useState(false);
  84. const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]);
  85. const [verifiedQty, setVerifiedQty] = useState<number>(0);
  86. const { data: session } = useSession() as { data: SessionWithTokens | null };
  87. const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => {
  88. return lot.availableQty || 0;
  89. }, []);
  90. const calculateRequiredQty = useCallback((lot: LotPickData) => {
  91. // Use the original required quantity, not subtracting actualPickQty
  92. // The actualPickQty in the form should be independent of the database value
  93. return lot.requiredQty || 0;
  94. }, []);
  95. useEffect(() => {
  96. console.log('PickExecutionForm props:', {
  97. open,
  98. onNormalPickSubmit: typeof onNormalPickSubmit,
  99. hasOnNormalPickSubmit: !!onNormalPickSubmit,
  100. onSubmit: typeof onSubmit,
  101. });
  102. }, [open, onNormalPickSubmit, onSubmit]);
  103. // 获取处理人员列表
  104. useEffect(() => {
  105. const fetchHandlers = async () => {
  106. try {
  107. const escalationCombo = await fetchEscalationCombo();
  108. setHandlers(escalationCombo);
  109. } catch (error) {
  110. console.error("Error fetching handlers:", error);
  111. }
  112. };
  113. fetchHandlers();
  114. }, []);
  115. // 初始化表单数据 - 每次打开时都重新初始化
  116. useEffect(() => {
  117. if (open && selectedLot && selectedPickOrderLine && pickOrderId) {
  118. const getSafeDate = (dateValue: any): string => {
  119. if (!dateValue) return dayjs().format(INPUT_DATE_FORMAT);
  120. try {
  121. const date = dayjs(dateValue);
  122. if (!date.isValid()) {
  123. return dayjs().format(INPUT_DATE_FORMAT);
  124. }
  125. return date.format(INPUT_DATE_FORMAT);
  126. } catch {
  127. return dayjs().format(INPUT_DATE_FORMAT);
  128. }
  129. };
  130. // Initialize verified quantity to the received quantity (actualPickQty)
  131. const initialVerifiedQty = selectedLot.actualPickQty || 0;
  132. setVerifiedQty(initialVerifiedQty);
  133. console.log("=== PickExecutionForm Debug ===");
  134. console.log("selectedLot:", selectedLot);
  135. console.log("initialVerifiedQty:", initialVerifiedQty);
  136. console.log("=== End Debug ===");
  137. setFormData({
  138. pickOrderId: pickOrderId,
  139. pickOrderCode: selectedPickOrderLine.pickOrderCode,
  140. pickOrderCreateDate: getSafeDate(pickOrderCreateDate),
  141. pickExecutionDate: dayjs().format(INPUT_DATE_FORMAT),
  142. pickOrderLineId: selectedPickOrderLine.id,
  143. itemId: selectedPickOrderLine.itemId,
  144. itemCode: selectedPickOrderLine.itemCode,
  145. itemDescription: selectedPickOrderLine.itemName,
  146. lotId: selectedLot.lotId,
  147. lotNo: selectedLot.lotNo,
  148. storeLocation: selectedLot.location,
  149. requiredQty: selectedLot.requiredQty,
  150. actualPickQty: initialVerifiedQty,
  151. missQty: 0,
  152. badItemQty: 0,
  153. issueRemark: '',
  154. handledBy: undefined,
  155. });
  156. }
  157. // 只在 open 状态改变时重新初始化,移除其他依赖
  158. // eslint-disable-next-line react-hooks/exhaustive-deps
  159. }, [open]);
  160. const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => {
  161. setFormData(prev => ({ ...prev, [field]: value }));
  162. // Update verified quantity state when actualPickQty changes
  163. if (field === 'actualPickQty') {
  164. setVerifiedQty(value);
  165. }
  166. // 清除错误
  167. if (errors[field as keyof FormErrors]) {
  168. setErrors(prev => ({ ...prev, [field]: undefined }));
  169. }
  170. }, [errors]);
  171. // Update form validation to require either missQty > 0 OR badItemQty > 0
  172. const validateForm = (): boolean => {
  173. const newErrors: FormErrors = {};
  174. const requiredQty = selectedLot?.requiredQty || 0;
  175. const badItemQty = formData.badItemQty || 0;
  176. const missQty = formData.missQty || 0;
  177. if (verifiedQty === undefined || verifiedQty < 0) {
  178. newErrors.actualPickQty = t('Qty is required');
  179. }
  180. const totalQty = verifiedQty + badItemQty + missQty;
  181. const hasAnyValue = verifiedQty > 0 || badItemQty > 0 || missQty > 0;
  182. // ✅ 新增:必须至少有一个 > 0
  183. if (!hasAnyValue) {
  184. newErrors.actualPickQty = t('At least one of Verified / Missing / Bad must be greater than 0');
  185. }
  186. if (hasAnyValue && totalQty !== requiredQty) {
  187. newErrors.actualPickQty = t('Total (Verified + Bad + Missing) must equal Required quantity');
  188. }
  189. setErrors(newErrors);
  190. return Object.keys(newErrors).length === 0;
  191. };
  192. const handleSubmit = async () => {
  193. if (!formData.pickOrderId || !selectedLot) {
  194. return;
  195. }
  196. // ✅ 只允许 Verified>0 且没有问题时,走 normal pick
  197. const isNormalPick = verifiedQty > 0
  198. && formData.missQty == 0
  199. && formData.badItemQty == 0;
  200. if (isNormalPick) {
  201. if (onNormalPickSubmit) {
  202. setLoading(true);
  203. try {
  204. console.log('Calling onNormalPickSubmit with:', { lot: selectedLot, submitQty: verifiedQty });
  205. await onNormalPickSubmit(selectedLot, verifiedQty);
  206. onClose();
  207. } catch (error) {
  208. console.error('Error submitting normal pick:', error);
  209. } finally {
  210. setLoading(false);
  211. }
  212. } else {
  213. console.warn('onNormalPickSubmit callback not provided');
  214. }
  215. return;
  216. }
  217. // ❌ 有问题(或全部为 0)才进入 Issue 提报流程
  218. if (!validateForm() || !formData.pickOrderId) {
  219. return;
  220. }
  221. setLoading(true);
  222. try {
  223. const submissionData = {
  224. ...formData,
  225. actualPickQty: verifiedQty,
  226. lotId: formData.lotId || selectedLot?.lotId || 0,
  227. lotNo: formData.lotNo || selectedLot?.lotNo || '',
  228. pickOrderCode: formData.pickOrderCode || selectedPickOrderLine?.pickOrderCode || '',
  229. pickerName: session?.user?.name || ''
  230. } as PickExecutionIssueData;
  231. await onSubmit(submissionData);
  232. onClose();
  233. } catch (error) {
  234. console.error('Error submitting pick execution issue:', error);
  235. } finally {
  236. setLoading(false);
  237. }
  238. };
  239. const handleClose = () => {
  240. setFormData({});
  241. setErrors({});
  242. setVerifiedQty(0);
  243. onClose();
  244. };
  245. if (!selectedLot || !selectedPickOrderLine) {
  246. return null;
  247. }
  248. const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot);
  249. const requiredQty = calculateRequiredQty(selectedLot);
  250. return (
  251. <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
  252. <DialogTitle>
  253. {t('Pick Execution Issue Form')} {/* Always show issue form title */}
  254. </DialogTitle>
  255. <DialogContent>
  256. <Box sx={{ mt: 2 }}>
  257. {/* Add instruction text */}
  258. <Grid container spacing={2}>
  259. <Grid item xs={12}>
  260. <Box sx={{ p: 2, backgroundColor: '#fff3cd', borderRadius: 1, mb: 2 }}>
  261. <Typography variant="body2" color="warning.main">
  262. <strong>{t('Note:')}</strong> {t('This form is for reporting issues only. You must report either missing items or bad items.')}
  263. </Typography>
  264. </Box>
  265. </Grid>
  266. {/* Keep the existing form fields */}
  267. <Grid item xs={6}>
  268. <TextField
  269. fullWidth
  270. label={t('Required Qty')}
  271. value={selectedLot?.requiredQty || 0}
  272. disabled
  273. variant="outlined"
  274. // helperText={t('Still need to pick')}
  275. />
  276. </Grid>
  277. <Grid item xs={6}>
  278. <TextField
  279. fullWidth
  280. label={t('Remaining Available Qty')}
  281. value={remainingAvailableQty}
  282. disabled
  283. variant="outlined"
  284. />
  285. </Grid>
  286. <Grid item xs={12}>
  287. <TextField
  288. fullWidth
  289. label={t('Verified Qty')}
  290. type="number"
  291. value={verifiedQty}
  292. onChange={(e) => {
  293. const newValue = parseFloat(e.target.value) || 0;
  294. setVerifiedQty(newValue);
  295. // handleInputChange('actualPickQty', newValue);
  296. }}
  297. error={!!errors.actualPickQty}
  298. helperText={errors.actualPickQty || `${t('Max')}: ${selectedLot?.actualPickQty || 0}`} // 使用原始接收数量
  299. variant="outlined"
  300. />
  301. </Grid>
  302. <Grid item xs={12}>
  303. <TextField
  304. fullWidth
  305. label={t('Missing item Qty')}
  306. type="number"
  307. value={formData.missQty || 0}
  308. onChange={(e) => {
  309. const newMissQty = parseFloat(e.target.value) || 0;
  310. handleInputChange('missQty', newMissQty);
  311. // 不要自动修改其他字段
  312. }}
  313. error={!!errors.missQty}
  314. helperText={errors.missQty}
  315. variant="outlined"
  316. />
  317. </Grid>
  318. <Grid item xs={12}>
  319. <TextField
  320. fullWidth
  321. label={t('Bad Item Qty')}
  322. type="number"
  323. value={formData.badItemQty || 0}
  324. onChange={(e) => {
  325. const newBadItemQty = parseFloat(e.target.value) || 0;
  326. handleInputChange('badItemQty', newBadItemQty);
  327. // 不要自动修改其他字段
  328. }}
  329. error={!!errors.badItemQty}
  330. helperText={errors.badItemQty}
  331. variant="outlined"
  332. />
  333. </Grid>
  334. {/* Show issue description and handler fields when bad items > 0 */}
  335. {(formData.badItemQty && formData.badItemQty > 0) ? (
  336. <>
  337. <Grid item xs={12}>
  338. <TextField
  339. fullWidth
  340. id="issueRemark"
  341. label={t('Issue Remark')}
  342. multiline
  343. rows={4}
  344. value={formData.issueRemark || ''}
  345. onChange={(e) => {
  346. handleInputChange('issueRemark', e.target.value);
  347. // Don't reset badItemQty when typing in issue remark
  348. }}
  349. error={!!errors.issueRemark}
  350. helperText={errors.issueRemark}
  351. //placeholder={t('Describe the issue with bad items')}
  352. variant="outlined"
  353. />
  354. </Grid>
  355. <Grid item xs={12}>
  356. <FormControl fullWidth error={!!errors.handledBy}>
  357. <InputLabel>{t('handler')}</InputLabel>
  358. <Select
  359. value={formData.handledBy ? formData.handledBy.toString() : ''}
  360. onChange={(e) => {
  361. handleInputChange('handledBy', e.target.value ? parseInt(e.target.value) : undefined);
  362. // Don't reset badItemQty when selecting handler
  363. }}
  364. label={t('handler')}
  365. >
  366. {handlers.map((handler) => (
  367. <MenuItem key={handler.id} value={handler.id.toString()}>
  368. {handler.name}
  369. </MenuItem>
  370. ))}
  371. </Select>
  372. {errors.handledBy && (
  373. <Typography variant="caption" color="error" sx={{ mt: 0.5, ml: 1.75 }}>
  374. {errors.handledBy}
  375. </Typography>
  376. )}
  377. </FormControl>
  378. </Grid>
  379. </>
  380. ) : (<></>)}
  381. </Grid>
  382. </Box>
  383. </DialogContent>
  384. <DialogActions>
  385. <Button onClick={handleClose} disabled={loading}>
  386. {t('Cancel')}
  387. </Button>
  388. <Button
  389. onClick={handleSubmit}
  390. variant="contained"
  391. disabled={loading}
  392. >
  393. {loading ? t('submitting') : t('submit')}
  394. </Button>
  395. </DialogActions>
  396. </Dialog>
  397. );
  398. };
  399. export default PickExecutionForm;