FPSMS-frontend
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 

494 lignes
16 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. badReason?: string;
  70. }
  71. const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
  72. open,
  73. onClose,
  74. onSubmit,
  75. selectedLot,
  76. selectedPickOrderLine,
  77. pickOrderId,
  78. pickOrderCreateDate,
  79. onNormalPickSubmit,
  80. }) => {
  81. const { t } = useTranslation();
  82. const [formData, setFormData] = useState<Partial<PickExecutionIssueData>>({});
  83. const [errors, setErrors] = useState<FormErrors>({});
  84. const [loading, setLoading] = useState(false);
  85. const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]);
  86. const [verifiedQty, setVerifiedQty] = useState<number>(0);
  87. const { data: session } = useSession() as { data: SessionWithTokens | null };
  88. const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => {
  89. return lot.availableQty || 0;
  90. }, []);
  91. const calculateRequiredQty = useCallback((lot: LotPickData) => {
  92. // Use the original required quantity, not subtracting actualPickQty
  93. // The actualPickQty in the form should be independent of the database value
  94. return lot.requiredQty || 0;
  95. }, []);
  96. useEffect(() => {
  97. console.log('PickExecutionForm props:', {
  98. open,
  99. onNormalPickSubmit: typeof onNormalPickSubmit,
  100. hasOnNormalPickSubmit: !!onNormalPickSubmit,
  101. onSubmit: typeof onSubmit,
  102. });
  103. }, [open, onNormalPickSubmit, onSubmit]);
  104. // 获取处理人员列表
  105. useEffect(() => {
  106. const fetchHandlers = async () => {
  107. try {
  108. const escalationCombo = await fetchEscalationCombo();
  109. setHandlers(escalationCombo);
  110. } catch (error) {
  111. console.error("Error fetching handlers:", error);
  112. }
  113. };
  114. fetchHandlers();
  115. }, []);
  116. // 初始化表单数据 - 每次打开时都重新初始化
  117. useEffect(() => {
  118. if (open && selectedLot && selectedPickOrderLine && pickOrderId) {
  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. // Initialize verified quantity to the received quantity (actualPickQty)
  132. const initialVerifiedQty = selectedLot.actualPickQty || 0;
  133. setVerifiedQty(initialVerifiedQty);
  134. console.log("=== PickExecutionForm Debug ===");
  135. console.log("selectedLot:", selectedLot);
  136. console.log("initialVerifiedQty:", initialVerifiedQty);
  137. console.log("=== End Debug ===");
  138. setFormData({
  139. pickOrderId: pickOrderId,
  140. pickOrderCode: selectedPickOrderLine.pickOrderCode,
  141. pickOrderCreateDate: getSafeDate(pickOrderCreateDate),
  142. pickExecutionDate: dayjs().format(INPUT_DATE_FORMAT),
  143. pickOrderLineId: selectedPickOrderLine.id,
  144. itemId: selectedPickOrderLine.itemId,
  145. itemCode: selectedPickOrderLine.itemCode,
  146. itemDescription: selectedPickOrderLine.itemName,
  147. lotId: selectedLot.lotId,
  148. lotNo: selectedLot.lotNo,
  149. storeLocation: selectedLot.location,
  150. requiredQty: selectedLot.requiredQty,
  151. actualPickQty: initialVerifiedQty,
  152. missQty: 0,
  153. badItemQty: 0,
  154. badPackageQty: 0, // Bad Package Qty (frontend only)
  155. issueRemark: "",
  156. pickerName: "",
  157. handledBy: undefined,
  158. reason: "",
  159. badReason: "",
  160. });
  161. }
  162. // 只在 open 状态改变时重新初始化,移除其他依赖
  163. // eslint-disable-next-line react-hooks/exhaustive-deps
  164. }, [open]);
  165. const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => {
  166. setFormData(prev => ({ ...prev, [field]: value }));
  167. // Update verified quantity state when actualPickQty changes
  168. if (field === 'actualPickQty') {
  169. setVerifiedQty(value);
  170. }
  171. // 清除错误
  172. if (errors[field as keyof FormErrors]) {
  173. setErrors(prev => ({ ...prev, [field]: undefined }));
  174. }
  175. }, [errors]);
  176. // Updated validation logic (same as GoodPickExecutionForm)
  177. const validateForm = (): boolean => {
  178. const newErrors: FormErrors = {};
  179. const ap = Number(verifiedQty) || 0;
  180. const miss = Number(formData.missQty) || 0;
  181. const badItem = Number(formData.badItemQty) || 0;
  182. const badPackage = Number((formData as any).badPackageQty) || 0;
  183. const totalBad = badItem + badPackage;
  184. const total = ap + miss + totalBad;
  185. const availableQty = selectedLot?.availableQty || 0;
  186. // 1. Check actualPickQty cannot be negative
  187. if (ap < 0) {
  188. newErrors.actualPickQty = t("Qty cannot be negative");
  189. }
  190. // 2. Check actualPickQty cannot exceed available quantity
  191. if (ap > availableQty) {
  192. newErrors.actualPickQty = t("Actual pick qty cannot exceed available qty");
  193. }
  194. // 3. Check missQty and both bad qtys cannot be negative
  195. if (miss < 0) {
  196. newErrors.missQty = t("Invalid qty");
  197. }
  198. if (badItem < 0 || badPackage < 0) {
  199. newErrors.badItemQty = t("Invalid qty");
  200. }
  201. // 4. Total (actualPickQty + missQty + badItemQty + badPackageQty) cannot exceed lot available qty
  202. if (total > availableQty) {
  203. const errorMsg = t(
  204. "Total qty (actual pick + miss + bad) cannot exceed available qty: {available}",
  205. { available: availableQty }
  206. );
  207. newErrors.actualPickQty = errorMsg;
  208. newErrors.missQty = errorMsg;
  209. newErrors.badItemQty = errorMsg;
  210. }
  211. // 5. At least one field must have a value
  212. if (ap === 0 && miss === 0 && totalBad === 0) {
  213. newErrors.actualPickQty = t("Enter pick qty or issue qty");
  214. }
  215. setErrors(newErrors);
  216. return Object.keys(newErrors).length === 0;
  217. };
  218. const handleSubmit = async () => {
  219. if (!formData.pickOrderId || !selectedLot) {
  220. return;
  221. }
  222. // ✅ 只允许 Verified>0 且没有问题时,走 normal pick
  223. const isNormalPick = verifiedQty > 0
  224. && formData.missQty == 0
  225. && formData.badItemQty == 0;
  226. if (isNormalPick) {
  227. if (onNormalPickSubmit) {
  228. setLoading(true);
  229. try {
  230. console.log('Calling onNormalPickSubmit with:', { lot: selectedLot, submitQty: verifiedQty });
  231. await onNormalPickSubmit(selectedLot, verifiedQty);
  232. onClose();
  233. } catch (error) {
  234. console.error('Error submitting normal pick:', error);
  235. } finally {
  236. setLoading(false);
  237. }
  238. } else {
  239. console.warn('onNormalPickSubmit callback not provided');
  240. }
  241. return;
  242. }
  243. // ❌ 有问题(或全部为 0)才进入 Issue 提报流程
  244. if (!validateForm() || !formData.pickOrderId) {
  245. return;
  246. }
  247. const badItem = Number(formData.badItemQty) || 0;
  248. const badPackage = Number((formData as any).badPackageQty) || 0;
  249. const totalBadQty = badItem + badPackage;
  250. let badReason: string | undefined;
  251. if (totalBadQty > 0) {
  252. // assumption: only one of them is > 0
  253. badReason = badPackage > 0 ? "package_problem" : "quantity_problem";
  254. }
  255. setLoading(true);
  256. try {
  257. const submissionData: PickExecutionIssueData = {
  258. ...(formData as PickExecutionIssueData),
  259. actualPickQty: verifiedQty,
  260. lotId: formData.lotId || selectedLot?.lotId || 0,
  261. lotNo: formData.lotNo || selectedLot?.lotNo || '',
  262. pickOrderCode: formData.pickOrderCode || selectedPickOrderLine?.pickOrderCode || '',
  263. pickerName: session?.user?.name || '',
  264. badItemQty: totalBadQty,
  265. badReason,
  266. };
  267. await onSubmit(submissionData);
  268. onClose();
  269. } catch (error: any) {
  270. console.error('Error submitting pick execution issue:', error);
  271. alert(
  272. t("Failed to submit issue. Please try again.") +
  273. (error.message ? `: ${error.message}` : "")
  274. );
  275. } finally {
  276. setLoading(false);
  277. }
  278. };
  279. const handleClose = () => {
  280. setFormData({});
  281. setErrors({});
  282. setVerifiedQty(0);
  283. onClose();
  284. };
  285. if (!selectedLot || !selectedPickOrderLine) {
  286. return null;
  287. }
  288. const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot);
  289. const requiredQty = calculateRequiredQty(selectedLot);
  290. return (
  291. <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
  292. <DialogTitle>
  293. {t('Pick Execution Issue Form')} {/* Always show issue form title */}
  294. </DialogTitle>
  295. <DialogContent>
  296. <Box sx={{ mt: 2 }}>
  297. {/* Add instruction text */}
  298. <Grid container spacing={2}>
  299. <Grid item xs={12}>
  300. <Box sx={{ p: 2, backgroundColor: '#fff3cd', borderRadius: 1, mb: 2 }}>
  301. <Typography variant="body2" color="warning.main">
  302. <strong>{t('Note:')}</strong> {t('This form is for reporting issues only. You must report either missing items or bad items.')}
  303. </Typography>
  304. </Box>
  305. </Grid>
  306. {/* Keep the existing form fields */}
  307. <Grid item xs={6}>
  308. <TextField
  309. fullWidth
  310. label={t('Required Qty')}
  311. value={selectedLot?.requiredQty || 0}
  312. disabled
  313. variant="outlined"
  314. // helperText={t('Still need to pick')}
  315. />
  316. </Grid>
  317. <Grid item xs={6}>
  318. <TextField
  319. fullWidth
  320. label={t('Remaining Available Qty')}
  321. value={remainingAvailableQty}
  322. disabled
  323. variant="outlined"
  324. />
  325. </Grid>
  326. <Grid item xs={12}>
  327. <TextField
  328. fullWidth
  329. label={t('Actual Pick Qty')}
  330. type="number"
  331. inputProps={{
  332. inputMode: "numeric",
  333. pattern: "[0-9]*",
  334. min: 0,
  335. }}
  336. value={verifiedQty ?? ""}
  337. onChange={(e) => {
  338. const newValue = e.target.value === ""
  339. ? undefined
  340. : Math.max(0, Number(e.target.value) || 0);
  341. setVerifiedQty(newValue || 0);
  342. }}
  343. error={!!errors.actualPickQty}
  344. helperText={
  345. errors.actualPickQty || `${t("Max")}: ${remainingAvailableQty}`
  346. }
  347. variant="outlined"
  348. />
  349. </Grid>
  350. <Grid item xs={12}>
  351. <TextField
  352. fullWidth
  353. label={t('Missing item Qty')}
  354. type="number"
  355. inputProps={{
  356. inputMode: "numeric",
  357. pattern: "[0-9]*",
  358. min: 0,
  359. }}
  360. value={formData.missQty || 0}
  361. onChange={(e) => {
  362. handleInputChange(
  363. "missQty",
  364. e.target.value === ""
  365. ? undefined
  366. : Math.max(0, Number(e.target.value) || 0)
  367. );
  368. }}
  369. error={!!errors.missQty}
  370. variant="outlined"
  371. />
  372. </Grid>
  373. <Grid item xs={12}>
  374. <TextField
  375. fullWidth
  376. label={t('Bad Item Qty')}
  377. type="number"
  378. inputProps={{
  379. inputMode: "numeric",
  380. pattern: "[0-9]*",
  381. min: 0,
  382. }}
  383. value={formData.badItemQty || 0}
  384. onChange={(e) => {
  385. const newBadItemQty = e.target.value === ""
  386. ? undefined
  387. : Math.max(0, Number(e.target.value) || 0);
  388. handleInputChange('badItemQty', newBadItemQty);
  389. }}
  390. error={!!errors.badItemQty}
  391. helperText={errors.badItemQty}
  392. variant="outlined"
  393. />
  394. </Grid>
  395. <Grid item xs={12}>
  396. <TextField
  397. fullWidth
  398. label={t("Bad Package Qty")}
  399. type="number"
  400. inputProps={{
  401. inputMode: "numeric",
  402. pattern: "[0-9]*",
  403. min: 0,
  404. }}
  405. value={(formData as any).badPackageQty || 0}
  406. onChange={(e) => {
  407. handleInputChange(
  408. "badPackageQty",
  409. e.target.value === ""
  410. ? undefined
  411. : Math.max(0, Number(e.target.value) || 0)
  412. );
  413. }}
  414. error={!!errors.badItemQty}
  415. variant="outlined"
  416. />
  417. </Grid>
  418. <Grid item xs={12}>
  419. <FormControl fullWidth>
  420. <InputLabel>{t("Remark")}</InputLabel>
  421. <Select
  422. value={formData.reason || ""}
  423. onChange={(e) => handleInputChange("reason", e.target.value)}
  424. label={t("Remark")}
  425. >
  426. <MenuItem value="">{t("Select Remark")}</MenuItem>
  427. <MenuItem value="miss">{t("Edit")}</MenuItem>
  428. <MenuItem value="bad">{t("Just Complete")}</MenuItem>
  429. </Select>
  430. </FormControl>
  431. </Grid>
  432. </Grid>
  433. </Box>
  434. </DialogContent>
  435. <DialogActions>
  436. <Button onClick={handleClose} disabled={loading}>
  437. {t('Cancel')}
  438. </Button>
  439. <Button
  440. onClick={handleSubmit}
  441. variant="contained"
  442. disabled={loading}
  443. >
  444. {loading ? t('submitting') : t('submit')}
  445. </Button>
  446. </DialogActions>
  447. </Dialog>
  448. );
  449. };
  450. export default PickExecutionForm;