FPSMS-frontend
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

GoodPickExecutionForm.tsx 13 KiB

7ヶ月前
3ヶ月前
7ヶ月前
3ヶ月前
7ヶ月前
3ヶ月前
7ヶ月前
3ヶ月前
7ヶ月前
3ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
3ヶ月前
1ヶ月前
7ヶ月前
1ヶ月前
7ヶ月前
3ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
3ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
1ヶ月前
7ヶ月前
3ヶ月前
7ヶ月前
3ヶ月前
7ヶ月前
3ヶ月前
7ヶ月前
3ヶ月前
7ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
3ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
3ヶ月前
7ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
7ヶ月前
3ヶ月前
7ヶ月前
3ヶ月前
6ヶ月前
3ヶ月前
7ヶ月前
1ヶ月前
7ヶ月前
3ヶ月前
7ヶ月前
3ヶ月前
7ヶ月前
3ヶ月前
7ヶ月前
3ヶ月前
7ヶ月前
3ヶ月前
7ヶ月前
3ヶ月前
7ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
7ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
7ヶ月前
3ヶ月前
7ヶ月前
3ヶ月前
7ヶ月前
3ヶ月前
7ヶ月前
3ヶ月前
7ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  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;