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

447 行
13 KiB

  1. "use client";
  2. import {
  3. Box,
  4. Button,
  5. Dialog,
  6. DialogActions,
  7. DialogContent,
  8. DialogTitle,
  9. FormControl,
  10. Grid,
  11. InputLabel,
  12. MenuItem,
  13. Select,
  14. TextField,
  15. Typography,
  16. } from "@mui/material";
  17. import { useCallback, useEffect, useState, useRef } from "react";
  18. import { useTranslation } from "react-i18next";
  19. import {
  20. GetPickOrderLineInfo,
  21. PickExecutionIssueData,
  22. } from "@/app/api/pickOrder/actions";
  23. import { fetchEscalationCombo } from "@/app/api/user/actions";
  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:
  42. | "available"
  43. | "insufficient_stock"
  44. | "expired"
  45. | "status_unavailable"
  46. | "rejected";
  47. stockOutLineId?: number;
  48. stockOutLineStatus?: string;
  49. stockOutLineQty?: number;
  50. }
  51. interface PickExecutionFormProps {
  52. open: boolean;
  53. onClose: () => void;
  54. onSubmit: (data: PickExecutionIssueData) => Promise<void>;
  55. selectedLot: LotPickData | null;
  56. selectedPickOrderLine: (GetPickOrderLineInfo & { pickOrderCode: string }) | null;
  57. pickOrderId?: number;
  58. pickOrderCreateDate: any;
  59. }
  60. interface FormErrors {
  61. actualPickQty?: string;
  62. missQty?: string;
  63. badItemQty?: string;
  64. badReason?: string;
  65. issueRemark?: string;
  66. handledBy?: string;
  67. }
  68. const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
  69. open,
  70. onClose,
  71. onSubmit,
  72. selectedLot,
  73. selectedPickOrderLine,
  74. pickOrderId,
  75. pickOrderCreateDate,
  76. }) => {
  77. const { t } = useTranslation("pickOrder");
  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. );
  84. const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => {
  85. return (lot.availableQty + lot.requiredQty ) || 0;
  86. }, []);
  87. const calculateRequiredQty = useCallback((lot: LotPickData) => {
  88. return lot.requiredQty || 0;
  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. const initKeyRef = useRef<string | null>(null);
  102. useEffect(() => {
  103. if (!open || !selectedLot || !selectedPickOrderLine || !pickOrderId) return;
  104. const key = `${selectedPickOrderLine.id}-${selectedLot.lotId}`;
  105. if (initKeyRef.current === key) return;
  106. const getSafeDate = (dateValue: any): string => {
  107. if (!dateValue) return dayjs().format(INPUT_DATE_FORMAT);
  108. try {
  109. const date = dayjs(dateValue);
  110. if (!date.isValid()) {
  111. return dayjs().format(INPUT_DATE_FORMAT);
  112. }
  113. return date.format(INPUT_DATE_FORMAT);
  114. } catch {
  115. return dayjs().format(INPUT_DATE_FORMAT);
  116. }
  117. };
  118. setFormData({
  119. pickOrderId: pickOrderId,
  120. pickOrderCode: selectedPickOrderLine.pickOrderCode,
  121. pickOrderCreateDate: getSafeDate(pickOrderCreateDate),
  122. pickExecutionDate: dayjs().format(INPUT_DATE_FORMAT),
  123. pickOrderLineId: selectedPickOrderLine.id,
  124. itemId: selectedPickOrderLine.itemId,
  125. itemCode: selectedPickOrderLine.itemCode,
  126. itemDescription: selectedPickOrderLine.itemName,
  127. lotId: selectedLot.lotId,
  128. lotNo: selectedLot.lotNo,
  129. stockOutLineId: selectedLot.stockOutLineId,
  130. storeLocation: selectedLot.location,
  131. requiredQty: selectedLot.requiredQty,
  132. actualPickQty: selectedLot.actualPickQty || 0,
  133. missQty: 0,
  134. badItemQty: 0, // Bad Item Qty
  135. badPackageQty: 0, // Bad Package Qty (frontend only)
  136. issueRemark: "",
  137. pickerName: "",
  138. handledBy: undefined,
  139. reason: "",
  140. badReason: "",
  141. });
  142. initKeyRef.current = key;
  143. }, [
  144. open,
  145. selectedPickOrderLine?.id,
  146. selectedLot?.lotId,
  147. pickOrderId,
  148. pickOrderCreateDate,
  149. ]);
  150. const handleInputChange = useCallback(
  151. (field: keyof PickExecutionIssueData, value: any) => {
  152. setFormData((prev) => ({ ...prev, [field]: value }));
  153. if (errors[field as keyof FormErrors]) {
  154. setErrors((prev) => ({ ...prev, [field]: undefined }));
  155. }
  156. },
  157. [errors]
  158. );
  159. // Updated validation logic
  160. const validateForm = (): boolean => {
  161. const newErrors: FormErrors = {};
  162. const ap = Number(formData.actualPickQty) || 0;
  163. const miss = Number(formData.missQty) || 0;
  164. const badItem = Number(formData.badItemQty) || 0;
  165. const badPackage = Number((formData as any).badPackageQty) || 0;
  166. const totalBad = badItem + badPackage;
  167. const total = ap + miss + totalBad;
  168. const availableQty = selectedLot?.availableQty || 0;
  169. // 1. Check actualPickQty cannot be negative
  170. if (ap < 0) {
  171. newErrors.actualPickQty = t("Qty cannot be negative");
  172. }
  173. // 2. Check actualPickQty cannot exceed available quantity
  174. if (ap > maxActual) {
  175. newErrors.actualPickQty = t("Actual pick qty cannot exceed available qty");
  176. }
  177. // 3. Check missQty and both bad qtys cannot be negative
  178. if (miss < 0) {
  179. newErrors.missQty = t("Invalid qty");
  180. }
  181. if (badItem < 0 || badPackage < 0) {
  182. newErrors.badItemQty = t("Invalid qty");
  183. }
  184. // 4. Total (actualPickQty + missQty + badItemQty + badPackageQty) cannot exceed lot available qty
  185. if (total > maxActual) {
  186. const errorMsg = t(
  187. "Total qty (actual pick + miss + bad) cannot exceed available qty: {available}",
  188. { available: maxActual }
  189. );
  190. newErrors.actualPickQty = errorMsg;
  191. newErrors.missQty = errorMsg;
  192. newErrors.badItemQty = errorMsg;
  193. }
  194. if (selectedLot?.stockOutLineStatus === 'pending' && ap >0 && miss === 0 && totalBad === 0) {
  195. newErrors.actualPickQty = t("if need just edit number, please scan the lot again");
  196. }
  197. // 5. At least one field must have a value
  198. // if (ap === 0 && miss === 0 && totalBad === 0) {
  199. //await handleSubmitPickQtyWithQty(selectedLot,0);
  200. // }
  201. setErrors(newErrors);
  202. return Object.keys(newErrors).length === 0;
  203. };
  204. const handleSubmit = async () => {
  205. if (!validateForm()) {
  206. console.error("Form validation failed:", errors);
  207. return;
  208. }
  209. if (!formData.pickOrderId) {
  210. console.error("Missing pickOrderId");
  211. return;
  212. }
  213. const badItem = Number(formData.badItemQty) || 0;
  214. const badPackage = Number((formData as any).badPackageQty) || 0;
  215. const totalBadQty = badItem + badPackage;
  216. let badReason: string | undefined;
  217. if (totalBadQty > 0) {
  218. // assumption: only one of them is > 0
  219. badReason = badPackage > 0 ? "package_problem" : "quantity_problem";
  220. }
  221. const submitData: PickExecutionIssueData = {
  222. ...(formData as PickExecutionIssueData),
  223. badItemQty: totalBadQty,
  224. badReason,
  225. };
  226. setLoading(true);
  227. try {
  228. await onSubmit(submitData);
  229. } catch (error: any) {
  230. console.error("Error submitting pick execution issue:", error);
  231. alert(
  232. t("Failed to submit issue. Please try again.") +
  233. (error.message ? `: ${error.message}` : "")
  234. );
  235. } finally {
  236. setLoading(false);
  237. }
  238. };
  239. const handleClose = () => {
  240. setFormData({});
  241. setErrors({});
  242. onClose();
  243. };
  244. if (!selectedLot || !selectedPickOrderLine) {
  245. return null;
  246. }
  247. const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot);
  248. const requiredQty = calculateRequiredQty(selectedLot);
  249. const availableQty = selectedLot?.availableQty || 0;
  250. const maxActual = requiredQty + availableQty;
  251. return (
  252. <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
  253. <DialogTitle>
  254. {t("Pick Execution Issue Form") }
  255. <br />
  256. {selectedPickOrderLine.itemCode+ " "+ selectedPickOrderLine.itemName}
  257. <br />
  258. {selectedLot.lotNo}
  259. </DialogTitle>
  260. <DialogContent>
  261. <Box sx={{ mt: 2 }}>
  262. <Grid container spacing={2}>
  263. <Grid item xs={6}>
  264. <TextField
  265. fullWidth
  266. label={t("Required Qty")}
  267. value={requiredQty}
  268. disabled
  269. variant="outlined"
  270. />
  271. </Grid>
  272. <Grid item xs={6}>
  273. <TextField
  274. fullWidth
  275. label={t("Remaining Available Qty")}
  276. value={remainingAvailableQty}
  277. disabled
  278. variant="outlined"
  279. />
  280. </Grid>
  281. <Grid item xs={12}>
  282. <TextField
  283. fullWidth
  284. label={t("Actual Pick Qty")}
  285. type="number"
  286. inputProps={{
  287. inputMode: "numeric",
  288. pattern: "[0-9]*",
  289. min: 0,
  290. }}
  291. value={formData.actualPickQty ?? ""}
  292. onChange={(e) =>
  293. handleInputChange(
  294. "actualPickQty",
  295. e.target.value === ""
  296. ? undefined
  297. : Math.max(0, Number(e.target.value) || 0)
  298. )
  299. }
  300. error={!!errors.actualPickQty}
  301. helperText={
  302. errors.actualPickQty || `${t("Max")}: ${remainingAvailableQty}`
  303. }
  304. variant="outlined"
  305. />
  306. </Grid>
  307. <Grid item xs={12}>
  308. <TextField
  309. fullWidth
  310. label={t("Missing item Qty")}
  311. type="number"
  312. inputProps={{
  313. inputMode: "numeric",
  314. pattern: "[0-9]*",
  315. min: 0,
  316. }}
  317. value={formData.missQty || 0}
  318. onChange={(e) =>
  319. handleInputChange(
  320. "missQty",
  321. e.target.value === ""
  322. ? undefined
  323. : Math.max(0, Number(e.target.value) || 0)
  324. )
  325. }
  326. error={!!errors.missQty}
  327. variant="outlined"
  328. />
  329. </Grid>
  330. <Grid item xs={12}>
  331. <TextField
  332. fullWidth
  333. label={t("Bad Item Qty")}
  334. type="number"
  335. inputProps={{
  336. inputMode: "numeric",
  337. pattern: "[0-9]*",
  338. min: 0,
  339. }}
  340. value={formData.badItemQty || 0}
  341. onChange={(e) =>
  342. handleInputChange(
  343. "badItemQty",
  344. e.target.value === ""
  345. ? undefined
  346. : Math.max(0, Number(e.target.value) || 0)
  347. )
  348. }
  349. error={!!errors.badItemQty}
  350. //helperText={t("Quantity Problem")}
  351. variant="outlined"
  352. />
  353. </Grid>
  354. <Grid item xs={12}>
  355. <TextField
  356. fullWidth
  357. label={t("Bad Package Qty")}
  358. type="number"
  359. inputProps={{
  360. inputMode: "numeric",
  361. pattern: "[0-9]*",
  362. min: 0,
  363. }}
  364. value={(formData as any).badPackageQty || 0}
  365. onChange={(e) =>
  366. handleInputChange(
  367. "badPackageQty",
  368. e.target.value === ""
  369. ? undefined
  370. : Math.max(0, Number(e.target.value) || 0)
  371. )
  372. }
  373. error={!!errors.badItemQty}
  374. //helperText={t("Package Problem")}
  375. variant="outlined"
  376. />
  377. </Grid>
  378. </Grid>
  379. <Grid item xs={12}>
  380. <FormControl fullWidth>
  381. <InputLabel>{t("Remark")}</InputLabel>
  382. <Select
  383. value={formData.reason || ""}
  384. onChange={(e) => handleInputChange("reason", e.target.value)}
  385. label={t("Remark")}
  386. >
  387. <MenuItem value="">{t("Select Remark")}</MenuItem>
  388. <MenuItem value="miss">{t("Edit")}</MenuItem>
  389. <MenuItem value="bad">{t("Just Complete")}</MenuItem>
  390. </Select>
  391. </FormControl>
  392. </Grid>
  393. </Box>
  394. </DialogContent>
  395. <DialogActions>
  396. <Button onClick={handleClose} disabled={loading}>
  397. {t("Cancel")}
  398. </Button>
  399. <Button onClick={handleSubmit} variant="contained" disabled={loading}>
  400. {loading ? t("submitting") : t("submit")}
  401. </Button>
  402. </DialogActions>
  403. </Dialog>
  404. );
  405. };
  406. export default PickExecutionForm;