FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

372 lines
13 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. interface LotPickData {
  23. id: number;
  24. lotId: number;
  25. lotNo: string;
  26. expiryDate: string;
  27. location: string;
  28. stockUnit: string;
  29. inQty: number;
  30. outQty: number;
  31. holdQty: number;
  32. totalPickedByAllPickOrders: number;
  33. availableQty: number;
  34. requiredQty: number;
  35. actualPickQty: number;
  36. lotStatus: string;
  37. lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected';
  38. stockOutLineId?: number;
  39. stockOutLineStatus?: string;
  40. stockOutLineQty?: number;
  41. }
  42. interface PickExecutionFormProps {
  43. open: boolean;
  44. onClose: () => void;
  45. onSubmit: (data: PickExecutionIssueData) => Promise<void>;
  46. selectedLot: LotPickData | null;
  47. selectedPickOrderLine: (GetPickOrderLineInfo & { pickOrderCode: string }) | null;
  48. pickOrderId?: number;
  49. pickOrderCreateDate: any;
  50. // ✅ Remove these props since we're not handling normal cases
  51. // onNormalPickSubmit?: (lineId: number, lotId: number, qty: number) => Promise<void>;
  52. // selectedRowId?: number | null;
  53. }
  54. // 定义错误类型
  55. interface FormErrors {
  56. actualPickQty?: string;
  57. missQty?: string;
  58. badItemQty?: string;
  59. issueRemark?: string;
  60. handledBy?: string;
  61. }
  62. const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
  63. open,
  64. onClose,
  65. onSubmit,
  66. selectedLot,
  67. selectedPickOrderLine,
  68. pickOrderId,
  69. pickOrderCreateDate,
  70. // ✅ Remove these props
  71. // onNormalPickSubmit,
  72. // selectedRowId,
  73. }) => {
  74. const { t } = useTranslation();
  75. const [formData, setFormData] = useState<Partial<PickExecutionIssueData>>({});
  76. const [errors, setErrors] = useState<FormErrors>({});
  77. const [loading, setLoading] = useState(false);
  78. const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]);
  79. // 计算剩余可用数量
  80. const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => {
  81. const remainingQty = lot.inQty - lot.outQty;
  82. return Math.max(0, remainingQty);
  83. }, []);
  84. const calculateRequiredQty = useCallback((lot: LotPickData) => {
  85. const requiredQty = lot.requiredQty-(lot.actualPickQty||0);
  86. return Math.max(0, requiredQty);
  87. }, []);
  88. // 获取处理人员列表
  89. useEffect(() => {
  90. const fetchHandlers = async () => {
  91. try {
  92. const escalationCombo = await fetchEscalationCombo();
  93. setHandlers(escalationCombo);
  94. } catch (error) {
  95. console.error("Error fetching handlers:", error);
  96. }
  97. };
  98. fetchHandlers();
  99. }, []);
  100. // 初始化表单数据 - 每次打开时都重新初始化
  101. useEffect(() => {
  102. if (open && selectedLot && selectedPickOrderLine && pickOrderId) {
  103. const getSafeDate = (dateValue: any): string => {
  104. if (!dateValue) return new Date().toISOString().split('T')[0];
  105. try {
  106. const date = new Date(dateValue);
  107. if (isNaN(date.getTime())) {
  108. return new Date().toISOString().split('T')[0];
  109. }
  110. return date.toISOString().split('T')[0];
  111. } catch {
  112. return new Date().toISOString().split('T')[0];
  113. }
  114. };
  115. // 计算剩余可用数量
  116. const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot);
  117. const requiredQty = calculateRequiredQty(selectedLot);
  118. console.log("=== PickExecutionForm Debug ===");
  119. console.log("selectedLot:", selectedLot);
  120. console.log("inQty:", selectedLot.inQty);
  121. console.log("outQty:", selectedLot.outQty);
  122. console.log("holdQty:", selectedLot.holdQty);
  123. console.log("availableQty:", selectedLot.availableQty);
  124. console.log("calculated remainingAvailableQty:", remainingAvailableQty);
  125. console.log("=== End Debug ===");
  126. setFormData({
  127. pickOrderId: pickOrderId,
  128. pickOrderCode: selectedPickOrderLine.pickOrderCode,
  129. pickOrderCreateDate: getSafeDate(pickOrderCreateDate),
  130. pickExecutionDate: new Date().toISOString().split('T')[0],
  131. pickOrderLineId: selectedPickOrderLine.id,
  132. itemId: selectedPickOrderLine.itemId,
  133. itemCode: selectedPickOrderLine.itemCode,
  134. itemDescription: selectedPickOrderLine.itemName,
  135. lotId: selectedLot.lotId,
  136. lotNo: selectedLot.lotNo,
  137. storeLocation: selectedLot.location,
  138. requiredQty: selectedLot.requiredQty,
  139. actualPickQty: selectedLot.actualPickQty || 0,
  140. missQty: 0,
  141. badItemQty: 0, // 初始化为 0,用户需要手动输入
  142. issueRemark: '',
  143. pickerName: '',
  144. handledBy: undefined,
  145. });
  146. }
  147. }, [open, selectedLot, selectedPickOrderLine, pickOrderId, pickOrderCreateDate, calculateRemainingAvailableQty]);
  148. const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => {
  149. setFormData(prev => ({ ...prev, [field]: value }));
  150. // 清除错误
  151. if (errors[field as keyof FormErrors]) {
  152. setErrors(prev => ({ ...prev, [field]: undefined }));
  153. }
  154. }, [errors]);
  155. // ✅ Update form validation to require either missQty > 0 OR badItemQty > 0
  156. const validateForm = (): boolean => {
  157. const newErrors: FormErrors = {};
  158. if (formData.actualPickQty === undefined || formData.actualPickQty < 0) {
  159. newErrors.actualPickQty = t('pickOrder.validation.actualPickQtyRequired');
  160. }
  161. // ✅ NEW: Require either missQty > 0 OR badItemQty > 0 (at least one issue must be reported)
  162. const hasMissQty = formData.missQty && formData.missQty > 0;
  163. const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0;
  164. if (!hasMissQty && !hasBadItemQty) {
  165. newErrors.missQty = t('pickOrder.validation.mustReportMissOrBadItems');
  166. newErrors.badItemQty = t('pickOrder.validation.mustReportMissOrBadItems');
  167. }
  168. if (formData.missQty && formData.missQty < 0) {
  169. newErrors.missQty = t('pickOrder.validation.missQtyInvalid');
  170. }
  171. if (formData.badItemQty && formData.badItemQty < 0) {
  172. newErrors.badItemQty = t('pickOrder.validation.badItemQtyInvalid');
  173. }
  174. if (formData.badItemQty && formData.badItemQty > 0 && !formData.issueRemark) {
  175. newErrors.issueRemark = t('pickOrder.validation.issueRemarkRequired');
  176. }
  177. if (formData.badItemQty && formData.badItemQty > 0 && !formData.handledBy) {
  178. newErrors.handledBy = t('pickOrder.validation.handlerRequired');
  179. }
  180. setErrors(newErrors);
  181. return Object.keys(newErrors).length === 0;
  182. };
  183. const handleSubmit = async () => {
  184. if (!validateForm() || !formData.pickOrderId) {
  185. return;
  186. }
  187. setLoading(true);
  188. try {
  189. await onSubmit(formData as PickExecutionIssueData);
  190. onClose();
  191. } catch (error) {
  192. console.error('Error submitting pick execution issue:', error);
  193. } finally {
  194. setLoading(false);
  195. }
  196. };
  197. const handleClose = () => {
  198. setFormData({});
  199. setErrors({});
  200. onClose();
  201. };
  202. if (!selectedLot || !selectedPickOrderLine) {
  203. return null;
  204. }
  205. const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot);
  206. const requiredQty = calculateRequiredQty(selectedLot);
  207. return (
  208. <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
  209. <DialogTitle>
  210. {t('Pick Execution Issue Form')} {/* ✅ Always show issue form title */}
  211. </DialogTitle>
  212. <DialogContent>
  213. <Box sx={{ mt: 2 }}>
  214. {/* ✅ Add instruction text */}
  215. <Grid container spacing={2}>
  216. <Grid item xs={12}>
  217. <Box sx={{ p: 2, backgroundColor: '#fff3cd', borderRadius: 1, mb: 2 }}>
  218. <Typography variant="body2" color="warning.main">
  219. <strong>{t('Note:')}</strong> {t('This form is for reporting issues only. You must report either missing items or bad items.')}
  220. </Typography>
  221. </Box>
  222. </Grid>
  223. {/* ✅ Keep the existing form fields */}
  224. <Grid item xs={6}>
  225. <TextField
  226. fullWidth
  227. label={t('requiredQty')}
  228. value={requiredQty || 0}
  229. disabled
  230. variant="outlined"
  231. helperText={t('Still need to pick')}
  232. />
  233. </Grid>
  234. <Grid item xs={6}>
  235. <TextField
  236. fullWidth
  237. label={t('remainingAvailableQty')}
  238. value={remainingAvailableQty}
  239. disabled
  240. variant="outlined"
  241. helperText={t('Available in warehouse')}
  242. />
  243. </Grid>
  244. <Grid item xs={12}>
  245. <TextField
  246. fullWidth
  247. label={t('actualPickQty')}
  248. type="number"
  249. value={formData.actualPickQty || 0}
  250. onChange={(e) => handleInputChange('actualPickQty', parseFloat(e.target.value) || 0)}
  251. error={!!errors.actualPickQty}
  252. helperText={errors.actualPickQty || t('Enter the quantity actually picked')}
  253. variant="outlined"
  254. />
  255. </Grid>
  256. <Grid item xs={12}>
  257. <TextField
  258. fullWidth
  259. label={t('missQty')}
  260. type="number"
  261. value={formData.missQty || 0}
  262. onChange={(e) => handleInputChange('missQty', parseFloat(e.target.value) || 0)}
  263. error={!!errors.missQty}
  264. helperText={errors.missQty || t('Enter missing quantity (required if no bad items)')}
  265. variant="outlined"
  266. />
  267. </Grid>
  268. <Grid item xs={12}>
  269. <TextField
  270. fullWidth
  271. label={t('badItemQty')}
  272. type="number"
  273. value={formData.badItemQty || 0}
  274. onChange={(e) => handleInputChange('badItemQty', parseFloat(e.target.value) || 0)}
  275. error={!!errors.badItemQty}
  276. helperText={errors.badItemQty || t('Enter bad item quantity (required if no missing items)')}
  277. variant="outlined"
  278. />
  279. </Grid>
  280. {/* ✅ Show issue description and handler fields when bad items > 0 */}
  281. {(formData.badItemQty && formData.badItemQty > 0) && (
  282. <>
  283. <Grid item xs={12}>
  284. <TextField
  285. fullWidth
  286. id="issueRemark"
  287. label={t('issueRemark')}
  288. multiline
  289. rows={4}
  290. value={formData.issueRemark || ''}
  291. onChange={(e) => handleInputChange('issueRemark', e.target.value)}
  292. error={!!errors.issueRemark}
  293. helperText={errors.issueRemark}
  294. placeholder={t('Describe the issue with bad items')}
  295. variant="outlined"
  296. />
  297. </Grid>
  298. <Grid item xs={12}>
  299. <FormControl fullWidth error={!!errors.handledBy}>
  300. <InputLabel>{t('handler')}</InputLabel>
  301. <Select
  302. value={formData.handledBy ? formData.handledBy.toString() : ''}
  303. onChange={(e) => handleInputChange('handledBy', e.target.value ? parseInt(e.target.value) : undefined)}
  304. label={t('handler')}
  305. >
  306. {handlers.map((handler) => (
  307. <MenuItem key={handler.id} value={handler.id.toString()}>
  308. {handler.name}
  309. </MenuItem>
  310. ))}
  311. </Select>
  312. {errors.handledBy && (
  313. <Typography variant="caption" color="error" sx={{ mt: 0.5, ml: 1.75 }}>
  314. {errors.handledBy}
  315. </Typography>
  316. )}
  317. </FormControl>
  318. </Grid>
  319. </>
  320. )}
  321. </Grid>
  322. </Box>
  323. </DialogContent>
  324. <DialogActions>
  325. <Button onClick={handleClose} disabled={loading}>
  326. {t('cancel')}
  327. </Button>
  328. <Button
  329. onClick={handleSubmit}
  330. variant="contained"
  331. disabled={loading}
  332. >
  333. {loading ? t('submitting') : t('submit')}
  334. </Button>
  335. </DialogActions>
  336. </Dialog>
  337. );
  338. };
  339. export default PickExecutionForm;