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.
 
 

496 line
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. // 增加 badPackageQty 判断,确保有坏包装会走 issue 流程
  223. const badPackageQty = Number((formData as any).badPackageQty) || 0;
  224. const isNormalPick = verifiedQty > 0
  225. && formData.missQty == 0
  226. && formData.badItemQty == 0
  227. && badPackageQty == 0;
  228. if (isNormalPick) {
  229. if (onNormalPickSubmit) {
  230. setLoading(true);
  231. try {
  232. console.log('Calling onNormalPickSubmit with:', { lot: selectedLot, submitQty: verifiedQty });
  233. await onNormalPickSubmit(selectedLot, verifiedQty);
  234. onClose();
  235. } catch (error) {
  236. console.error('Error submitting normal pick:', error);
  237. } finally {
  238. setLoading(false);
  239. }
  240. } else {
  241. console.warn('onNormalPickSubmit callback not provided');
  242. }
  243. return;
  244. }
  245. // ❌ 有问题(或全部为 0)才进入 Issue 提报流程
  246. if (!validateForm() || !formData.pickOrderId) {
  247. return;
  248. }
  249. const badItem = Number(formData.badItemQty) || 0;
  250. const badPackage = Number((formData as any).badPackageQty) || 0;
  251. const totalBadQty = badItem + badPackage;
  252. let badReason: string | undefined;
  253. if (totalBadQty > 0) {
  254. // assumption: only one of them is > 0
  255. badReason = badPackage > 0 ? "package_problem" : "quantity_problem";
  256. }
  257. setLoading(true);
  258. try {
  259. const submissionData: PickExecutionIssueData = {
  260. ...(formData as PickExecutionIssueData),
  261. actualPickQty: verifiedQty,
  262. lotId: formData.lotId || selectedLot?.lotId || 0,
  263. lotNo: formData.lotNo || selectedLot?.lotNo || '',
  264. pickOrderCode: formData.pickOrderCode || selectedPickOrderLine?.pickOrderCode || '',
  265. pickerName: session?.user?.name || '',
  266. badItemQty: totalBadQty,
  267. badReason,
  268. };
  269. await onSubmit(submissionData);
  270. onClose();
  271. } catch (error: any) {
  272. console.error('Error submitting pick execution issue:', error);
  273. alert(
  274. t("Failed to submit issue. Please try again.") +
  275. (error.message ? `: ${error.message}` : "")
  276. );
  277. } finally {
  278. setLoading(false);
  279. }
  280. };
  281. const handleClose = () => {
  282. setFormData({});
  283. setErrors({});
  284. setVerifiedQty(0);
  285. onClose();
  286. };
  287. if (!selectedLot || !selectedPickOrderLine) {
  288. return null;
  289. }
  290. const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot);
  291. const requiredQty = calculateRequiredQty(selectedLot);
  292. return (
  293. <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
  294. <DialogTitle>
  295. {t('Pick Execution Issue Form')} {/* Always show issue form title */}
  296. </DialogTitle>
  297. <DialogContent>
  298. <Box sx={{ mt: 2 }}>
  299. {/* Add instruction text */}
  300. <Grid container spacing={2}>
  301. <Grid item xs={12}>
  302. <Box sx={{ p: 2, backgroundColor: '#fff3cd', borderRadius: 1, mb: 2 }}>
  303. <Typography variant="body2" color="warning.main">
  304. <strong>{t('Note:')}</strong> {t('This form is for reporting issues only. You must report either missing items or bad items.')}
  305. </Typography>
  306. </Box>
  307. </Grid>
  308. {/* Keep the existing form fields */}
  309. <Grid item xs={6}>
  310. <TextField
  311. fullWidth
  312. label={t('Required Qty')}
  313. value={selectedLot?.requiredQty || 0}
  314. disabled
  315. variant="outlined"
  316. // helperText={t('Still need to pick')}
  317. />
  318. </Grid>
  319. <Grid item xs={6}>
  320. <TextField
  321. fullWidth
  322. label={t('Remaining Available Qty')}
  323. value={remainingAvailableQty}
  324. disabled
  325. variant="outlined"
  326. />
  327. </Grid>
  328. <Grid item xs={12}>
  329. <TextField
  330. fullWidth
  331. label={t('Actual Pick Qty')}
  332. type="number"
  333. inputProps={{
  334. inputMode: "numeric",
  335. pattern: "[0-9]*",
  336. min: 0,
  337. }}
  338. value={verifiedQty ?? ""}
  339. onChange={(e) => {
  340. const newValue = e.target.value === ""
  341. ? undefined
  342. : Math.max(0, Number(e.target.value) || 0);
  343. setVerifiedQty(newValue || 0);
  344. }}
  345. error={!!errors.actualPickQty}
  346. helperText={
  347. errors.actualPickQty || `${t("Max")}: ${remainingAvailableQty}`
  348. }
  349. variant="outlined"
  350. />
  351. </Grid>
  352. <Grid item xs={12}>
  353. <TextField
  354. fullWidth
  355. label={t('Missing item Qty')}
  356. type="number"
  357. inputProps={{
  358. inputMode: "numeric",
  359. pattern: "[0-9]*",
  360. min: 0,
  361. }}
  362. value={formData.missQty || 0}
  363. onChange={(e) => {
  364. handleInputChange(
  365. "missQty",
  366. e.target.value === ""
  367. ? undefined
  368. : Math.max(0, Number(e.target.value) || 0)
  369. );
  370. }}
  371. error={!!errors.missQty}
  372. variant="outlined"
  373. />
  374. </Grid>
  375. <Grid item xs={12}>
  376. <TextField
  377. fullWidth
  378. label={t('Bad Item Qty')}
  379. type="number"
  380. inputProps={{
  381. inputMode: "numeric",
  382. pattern: "[0-9]*",
  383. min: 0,
  384. }}
  385. value={formData.badItemQty || 0}
  386. onChange={(e) => {
  387. const newBadItemQty = e.target.value === ""
  388. ? undefined
  389. : Math.max(0, Number(e.target.value) || 0);
  390. handleInputChange('badItemQty', newBadItemQty);
  391. }}
  392. error={!!errors.badItemQty}
  393. helperText={errors.badItemQty}
  394. variant="outlined"
  395. />
  396. </Grid>
  397. <Grid item xs={12}>
  398. <TextField
  399. fullWidth
  400. label={t("Bad Package Qty")}
  401. type="number"
  402. inputProps={{
  403. inputMode: "numeric",
  404. pattern: "[0-9]*",
  405. min: 0,
  406. }}
  407. value={(formData as any).badPackageQty || 0}
  408. onChange={(e) => {
  409. handleInputChange(
  410. "badPackageQty",
  411. e.target.value === ""
  412. ? undefined
  413. : Math.max(0, Number(e.target.value) || 0)
  414. );
  415. }}
  416. error={!!errors.badItemQty}
  417. variant="outlined"
  418. />
  419. </Grid>
  420. <Grid item xs={12}>
  421. <FormControl fullWidth>
  422. <InputLabel>{t("Remark")}</InputLabel>
  423. <Select
  424. value={formData.reason || ""}
  425. onChange={(e) => handleInputChange("reason", e.target.value)}
  426. label={t("Remark")}
  427. >
  428. <MenuItem value="">{t("Select Remark")}</MenuItem>
  429. <MenuItem value="miss">{t("Edit")}</MenuItem>
  430. <MenuItem value="bad">{t("Just Complete")}</MenuItem>
  431. </Select>
  432. </FormControl>
  433. </Grid>
  434. </Grid>
  435. </Box>
  436. </DialogContent>
  437. <DialogActions>
  438. <Button onClick={handleClose} disabled={loading}>
  439. {t('Cancel')}
  440. </Button>
  441. <Button
  442. onClick={handleSubmit}
  443. variant="contained"
  444. disabled={loading}
  445. >
  446. {loading ? t('submitting') : t('submit')}
  447. </Button>
  448. </DialogActions>
  449. </Dialog>
  450. );
  451. };
  452. export default PickExecutionForm;