FPSMS-frontend
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 

439 líneas
14 KiB

  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;