FPSMS-frontend
Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 

371 linhas
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. import { useRef } from "react";
  23. import dayjs from 'dayjs';
  24. import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
  25. interface LotPickData {
  26. id: number;
  27. lotId: number;
  28. lotNo: string;
  29. expiryDate: string;
  30. location: string;
  31. stockUnit: string;
  32. inQty: number;
  33. outQty: number;
  34. holdQty: number;
  35. totalPickedByAllPickOrders: number;
  36. availableQty: number;
  37. requiredQty: number;
  38. actualPickQty: number;
  39. lotStatus: string;
  40. lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected';
  41. stockOutLineId?: number;
  42. stockOutLineStatus?: string;
  43. stockOutLineQty?: number;
  44. }
  45. interface PickExecutionFormProps {
  46. open: boolean;
  47. onClose: () => void;
  48. onSubmit: (data: PickExecutionIssueData) => Promise<void>;
  49. selectedLot: LotPickData | null;
  50. selectedPickOrderLine: (GetPickOrderLineInfo & { pickOrderCode: string }) | null;
  51. pickOrderId?: number;
  52. pickOrderCreateDate: any;
  53. // ✅ Remove these props since we're not handling normal cases
  54. // onNormalPickSubmit?: (lineId: number, lotId: number, qty: number) => Promise<void>;
  55. // selectedRowId?: number | null;
  56. }
  57. // 定义错误类型
  58. interface FormErrors {
  59. actualPickQty?: string;
  60. missQty?: string;
  61. badItemQty?: string;
  62. issueRemark?: string;
  63. handledBy?: string;
  64. }
  65. const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
  66. open,
  67. onClose,
  68. onSubmit,
  69. selectedLot,
  70. selectedPickOrderLine,
  71. pickOrderId,
  72. pickOrderCreateDate,
  73. // ✅ Remove these props
  74. // onNormalPickSubmit,
  75. // selectedRowId,
  76. }) => {
  77. const { t } = useTranslation();
  78. const [formData, setFormData] = useState<Partial<PickExecutionIssueData>>({});
  79. const [errors, setErrors] = useState<FormErrors>({});
  80. const [loading, setLoading] = useState(false);
  81. const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]);
  82. // 计算剩余可用数量
  83. const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => {
  84. const remainingQty = lot.inQty - lot.outQty;
  85. return Math.max(0, remainingQty);
  86. }, []);
  87. const calculateRequiredQty = useCallback((lot: LotPickData) => {
  88. // ✅ Use the original required quantity, not subtracting actualPickQty
  89. // The actualPickQty in the form should be independent of the database value
  90. return lot.requiredQty || 0;
  91. }, []);
  92. const remaining = selectedLot ? calculateRemainingAvailableQty(selectedLot) : 0;
  93. const req = selectedLot ? calculateRequiredQty(selectedLot) : 0;
  94. const ap = Number(formData.actualPickQty) || 0;
  95. const miss = Number(formData.missQty) || 0;
  96. const bad = Number(formData.badItemQty) || 0;
  97. // Max the user can type
  98. const maxPick = Math.min(remaining, req);
  99. const maxIssueTotal = Math.max(0, req - ap); // remaining room for miss+bad
  100. const clamp0 = (v: any) => Math.max(0, Number(v) || 0);
  101. // 获取处理人员列表
  102. useEffect(() => {
  103. const fetchHandlers = async () => {
  104. try {
  105. const escalationCombo = await fetchEscalationCombo();
  106. setHandlers(escalationCombo);
  107. } catch (error) {
  108. console.error("Error fetching handlers:", error);
  109. }
  110. };
  111. fetchHandlers();
  112. }, []);
  113. const initKeyRef = useRef<string | null>(null);
  114. useEffect(() => {
  115. if (!open || !selectedLot || !selectedPickOrderLine || !pickOrderId) return;
  116. // Only initialize once per (pickOrderLineId + lotId) while dialog open
  117. const key = `${selectedPickOrderLine.id}-${selectedLot.lotId}`;
  118. if (initKeyRef.current === key) return;
  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. setFormData({
  132. pickOrderId: pickOrderId,
  133. pickOrderCode: selectedPickOrderLine.pickOrderCode,
  134. pickOrderCreateDate: getSafeDate(pickOrderCreateDate),
  135. pickExecutionDate: dayjs().format(INPUT_DATE_FORMAT),
  136. pickOrderLineId: selectedPickOrderLine.id,
  137. itemId: selectedPickOrderLine.itemId,
  138. itemCode: selectedPickOrderLine.itemCode,
  139. itemDescription: selectedPickOrderLine.itemName,
  140. lotId: selectedLot.lotId,
  141. lotNo: selectedLot.lotNo,
  142. storeLocation: selectedLot.location,
  143. requiredQty: selectedLot.requiredQty,
  144. actualPickQty: selectedLot.actualPickQty || 0,
  145. missQty: 0,
  146. badItemQty: 0,
  147. issueRemark: '',
  148. pickerName: '',
  149. handledBy: undefined,
  150. });
  151. initKeyRef.current = key;
  152. }, [open, selectedPickOrderLine?.id, selectedLot?.lotId, pickOrderId, pickOrderCreateDate]);
  153. // Mutually exclusive inputs: picking vs reporting issues
  154. const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => {
  155. setFormData(prev => ({ ...prev, [field]: value }));
  156. // 清除错误
  157. if (errors[field as keyof FormErrors]) {
  158. setErrors(prev => ({ ...prev, [field]: undefined }));
  159. }
  160. }, [errors]);
  161. // ✅ Update form validation to require either missQty > 0 OR badItemQty > 0
  162. const validateForm = (): boolean => {
  163. const newErrors: FormErrors = {};
  164. const req = selectedLot?.requiredQty || 0;
  165. const ap = formData.actualPickQty || 0;
  166. const miss = formData.missQty || 0;
  167. const bad = formData.badItemQty || 0;
  168. if (ap < 0) newErrors.actualPickQty = t('Qty is required');
  169. if (ap > Math.min(remainingAvailableQty, req)) newErrors.actualPickQty = t('Qty is not allowed to be greater than required/available qty');
  170. if (miss < 0) newErrors.missQty = t('Invalid qty');
  171. if (bad < 0) newErrors.badItemQty = t('Invalid qty');
  172. if (ap + miss + bad > req) {
  173. newErrors.actualPickQty = t('Total exceeds required qty');
  174. newErrors.missQty = t('Total exceeds required qty');
  175. }
  176. if (ap === 0 && miss === 0 && bad === 0) {
  177. newErrors.actualPickQty = t('Enter pick qty or issue qty');
  178. newErrors.missQty = t('Enter pick qty or issue qty');
  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('Required Qty')}
  228. value={selectedLot?.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('Remaining Available Qty')}
  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('Actual Pick Qty')}
  248. type="number"
  249. value={formData.actualPickQty ?? ''}
  250. onChange={(e) => handleInputChange('actualPickQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))}
  251. error={!!errors.actualPickQty}
  252. helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(remainingAvailableQty, selectedLot?.requiredQty || 0)}`}
  253. variant="outlined"
  254. />
  255. </Grid>
  256. <Grid item xs={12}>
  257. <TextField
  258. fullWidth
  259. label={t('Missing item Qty')}
  260. type="number"
  261. value={formData.missQty || 0}
  262. onChange={(e) => handleInputChange('missQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))}
  263. error={!!errors.missQty}
  264. variant="outlined"
  265. //disabled={(formData.actualPickQty || 0) > 0}
  266. />
  267. </Grid>
  268. <Grid item xs={12}>
  269. <TextField
  270. fullWidth
  271. label={t('Bad Item Qty')}
  272. type="number"
  273. value={formData.badItemQty || 0}
  274. onChange={(e) => handleInputChange('badItemQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))}
  275. error={!!errors.badItemQty}
  276. variant="outlined"
  277. //disabled={(formData.actualPickQty || 0) > 0}
  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('Issue Remark')}
  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;