FPSMS-frontend
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 

368 行
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. // ✅ Use the original required quantity, not subtracting actualPickQty
  86. // The actualPickQty in the form should be independent of the database value
  87. return lot.requiredQty || 0;
  88. }, []);
  89. // 获取处理人员列表
  90. useEffect(() => {
  91. const fetchHandlers = async () => {
  92. try {
  93. const escalationCombo = await fetchEscalationCombo();
  94. setHandlers(escalationCombo);
  95. } catch (error) {
  96. console.error("Error fetching handlers:", error);
  97. }
  98. };
  99. fetchHandlers();
  100. }, []);
  101. // 初始化表单数据 - 每次打开时都重新初始化
  102. useEffect(() => {
  103. if (open && selectedLot && selectedPickOrderLine && pickOrderId) {
  104. const getSafeDate = (dateValue: any): string => {
  105. if (!dateValue) return new Date().toISOString().split('T')[0];
  106. try {
  107. const date = new Date(dateValue);
  108. if (isNaN(date.getTime())) {
  109. return new Date().toISOString().split('T')[0];
  110. }
  111. return date.toISOString().split('T')[0];
  112. } catch {
  113. return new Date().toISOString().split('T')[0];
  114. }
  115. };
  116. // 计算剩余可用数量
  117. const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot);
  118. const requiredQty = calculateRequiredQty(selectedLot);
  119. console.log("=== PickExecutionForm Debug ===");
  120. console.log("selectedLot:", selectedLot);
  121. console.log("inQty:", selectedLot.inQty);
  122. console.log("outQty:", selectedLot.outQty);
  123. console.log("holdQty:", selectedLot.holdQty);
  124. console.log("availableQty:", selectedLot.availableQty);
  125. console.log("calculated remainingAvailableQty:", remainingAvailableQty);
  126. console.log("=== End Debug ===");
  127. setFormData({
  128. pickOrderId: pickOrderId,
  129. pickOrderCode: selectedPickOrderLine.pickOrderCode,
  130. pickOrderCreateDate: getSafeDate(pickOrderCreateDate),
  131. pickExecutionDate: new Date().toISOString().split('T')[0],
  132. pickOrderLineId: selectedPickOrderLine.id,
  133. itemId: selectedPickOrderLine.itemId,
  134. itemCode: selectedPickOrderLine.itemCode,
  135. itemDescription: selectedPickOrderLine.itemName,
  136. lotId: selectedLot.lotId,
  137. lotNo: selectedLot.lotNo,
  138. storeLocation: selectedLot.location,
  139. requiredQty: selectedLot.requiredQty,
  140. actualPickQty: selectedLot.actualPickQty || 0,
  141. missQty: 0,
  142. badItemQty: 0, // 初始化为 0,用户需要手动输入
  143. issueRemark: '',
  144. pickerName: '',
  145. handledBy: undefined,
  146. });
  147. }
  148. }, [open, selectedLot, selectedPickOrderLine, pickOrderId, pickOrderCreateDate, calculateRemainingAvailableQty]);
  149. const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => {
  150. setFormData(prev => ({ ...prev, [field]: value }));
  151. // 清除错误
  152. if (errors[field as keyof FormErrors]) {
  153. setErrors(prev => ({ ...prev, [field]: undefined }));
  154. }
  155. }, [errors]);
  156. // ✅ Update form validation to require either missQty > 0 OR badItemQty > 0
  157. const validateForm = (): boolean => {
  158. const newErrors: FormErrors = {};
  159. if (formData.actualPickQty === undefined || formData.actualPickQty < 0) {
  160. newErrors.actualPickQty = t('Qty is required');
  161. }
  162. // ✅ FIXED: Check if actual pick qty exceeds remaining available qty
  163. if (formData.actualPickQty && formData.actualPickQty > remainingAvailableQty) {
  164. newErrors.actualPickQty = t('Qty is not allowed to be greater than remaining available qty');
  165. }
  166. // ✅ FIXED: Check if actual pick qty exceeds required qty (use original required qty)
  167. if (formData.actualPickQty && formData.actualPickQty > (selectedLot?.requiredQty || 0)) {
  168. newErrors.actualPickQty = t('Qty is not allowed to be greater than required qty');
  169. }
  170. // ✅ NEW: Require either missQty > 0 OR badItemQty > 0 (at least one issue must be reported)
  171. const hasMissQty = formData.missQty && formData.missQty > 0;
  172. const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0;
  173. if (!hasMissQty && !hasBadItemQty) {
  174. newErrors.missQty = t('At least one issue must be reported');
  175. newErrors.badItemQty = t('At least one issue must be reported');
  176. }
  177. setErrors(newErrors);
  178. return Object.keys(newErrors).length === 0;
  179. };
  180. const handleSubmit = async () => {
  181. if (!validateForm() || !formData.pickOrderId) {
  182. return;
  183. }
  184. setLoading(true);
  185. try {
  186. await onSubmit(formData as PickExecutionIssueData);
  187. onClose();
  188. } catch (error) {
  189. console.error('Error submitting pick execution issue:', error);
  190. } finally {
  191. setLoading(false);
  192. }
  193. };
  194. const handleClose = () => {
  195. setFormData({});
  196. setErrors({});
  197. onClose();
  198. };
  199. if (!selectedLot || !selectedPickOrderLine) {
  200. return null;
  201. }
  202. const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot);
  203. const requiredQty = calculateRequiredQty(selectedLot);
  204. return (
  205. <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
  206. <DialogTitle>
  207. {t('Pick Execution Issue Form')} {/* ✅ Always show issue form title */}
  208. </DialogTitle>
  209. <DialogContent>
  210. <Box sx={{ mt: 2 }}>
  211. {/* ✅ Add instruction text */}
  212. <Grid container spacing={2}>
  213. <Grid item xs={12}>
  214. <Box sx={{ p: 2, backgroundColor: '#fff3cd', borderRadius: 1, mb: 2 }}>
  215. <Typography variant="body2" color="warning.main">
  216. <strong>{t('Note:')}</strong> {t('This form is for reporting issues only. You must report either missing items or bad items.')}
  217. </Typography>
  218. </Box>
  219. </Grid>
  220. {/* ✅ Keep the existing form fields */}
  221. <Grid item xs={6}>
  222. <TextField
  223. fullWidth
  224. label={t('Required Qty')}
  225. value={selectedLot?.requiredQty || 0}
  226. disabled
  227. variant="outlined"
  228. // helperText={t('Still need to pick')}
  229. />
  230. </Grid>
  231. <Grid item xs={6}>
  232. <TextField
  233. fullWidth
  234. label={t('Remaining Available Qty')}
  235. value={remainingAvailableQty}
  236. disabled
  237. variant="outlined"
  238. // helperText={t('Available in warehouse')}
  239. />
  240. </Grid>
  241. <Grid item xs={12}>
  242. <TextField
  243. fullWidth
  244. label={t('Actual Pick Qty')}
  245. type="number"
  246. value={formData.actualPickQty || 0}
  247. onChange={(e) => handleInputChange('actualPickQty', parseFloat(e.target.value) || 0)}
  248. error={!!errors.actualPickQty}
  249. helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(remainingAvailableQty, selectedLot?.requiredQty || 0)}`}
  250. variant="outlined"
  251. />
  252. </Grid>
  253. <Grid item xs={12}>
  254. <TextField
  255. fullWidth
  256. label={t('Missing item Qty')}
  257. type="number"
  258. value={formData.missQty || 0}
  259. onChange={(e) => handleInputChange('missQty', parseFloat(e.target.value) || 0)}
  260. error={!!errors.missQty}
  261. // helperText={errors.missQty || t('Enter missing quantity (required if no bad items)')}
  262. variant="outlined"
  263. />
  264. </Grid>
  265. <Grid item xs={12}>
  266. <TextField
  267. fullWidth
  268. label={t('Bad Item Qty')}
  269. type="number"
  270. value={formData.badItemQty || 0}
  271. onChange={(e) => handleInputChange('badItemQty', parseFloat(e.target.value) || 0)}
  272. error={!!errors.badItemQty}
  273. // helperText={errors.badItemQty || t('Enter bad item quantity (required if no missing items)')}
  274. variant="outlined"
  275. />
  276. </Grid>
  277. {/* ✅ Show issue description and handler fields when bad items > 0 */}
  278. {(formData.badItemQty && formData.badItemQty > 0) ? (
  279. <>
  280. <Grid item xs={12}>
  281. <TextField
  282. fullWidth
  283. id="issueRemark"
  284. label={t('Issue Remark')}
  285. multiline
  286. rows={4}
  287. value={formData.issueRemark || ''}
  288. onChange={(e) => handleInputChange('issueRemark', e.target.value)}
  289. error={!!errors.issueRemark}
  290. helperText={errors.issueRemark}
  291. //placeholder={t('Describe the issue with bad items')}
  292. variant="outlined"
  293. />
  294. </Grid>
  295. <Grid item xs={12}>
  296. <FormControl fullWidth error={!!errors.handledBy}>
  297. <InputLabel>{t('handler')}</InputLabel>
  298. <Select
  299. value={formData.handledBy ? formData.handledBy.toString() : ''}
  300. onChange={(e) => handleInputChange('handledBy', e.target.value ? parseInt(e.target.value) : undefined)}
  301. label={t('handler')}
  302. >
  303. {handlers.map((handler) => (
  304. <MenuItem key={handler.id} value={handler.id.toString()}>
  305. {handler.name}
  306. </MenuItem>
  307. ))}
  308. </Select>
  309. {errors.handledBy && (
  310. <Typography variant="caption" color="error" sx={{ mt: 0.5, ml: 1.75 }}>
  311. {errors.handledBy}
  312. </Typography>
  313. )}
  314. </FormControl>
  315. </Grid>
  316. </>
  317. ) : (<></>)}
  318. </Grid>
  319. </Box>
  320. </DialogContent>
  321. <DialogActions>
  322. <Button onClick={handleClose} disabled={loading}>
  323. {t('cancel')}
  324. </Button>
  325. <Button
  326. onClick={handleSubmit}
  327. variant="contained"
  328. disabled={loading}
  329. >
  330. {loading ? t('submitting') : t('submit')}
  331. </Button>
  332. </DialogActions>
  333. </Dialog>
  334. );
  335. };
  336. export default PickExecutionForm;