FPSMS-frontend
Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 

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