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

GoodPickExecutionForm.tsx 13 KiB

4ヶ月前
2週間前
4ヶ月前
2週間前
4ヶ月前
2週間前
4ヶ月前
2週間前
4ヶ月前
2週間前
4ヶ月前
4ヶ月前
4ヶ月前
2週間前
4ヶ月前
3ヶ月前
4ヶ月前
2週間前
4ヶ月前
4ヶ月前
4ヶ月前
2週間前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
2週間前
4ヶ月前
2週間前
4ヶ月前
2週間前
4ヶ月前
2週間前
4ヶ月前
2週間前
2ヶ月前
2週間前
2週間前
2週間前
2週間前
2週間前
2週間前
2週間前
2週間前
2週間前
2週間前
4ヶ月前
3ヶ月前
2週間前
3ヶ月前
2週間前
3ヶ月前
2週間前
4ヶ月前
2週間前
4ヶ月前
2週間前
3ヶ月前
2週間前
4ヶ月前
2週間前
4ヶ月前
2週間前
4ヶ月前
2週間前
4ヶ月前
2週間前
4ヶ月前
2週間前
4ヶ月前
2週間前
2週間前
2週間前
2週間前
4ヶ月前
2週間前
2週間前
2週間前
4ヶ月前
2週間前
2週間前
2週間前
2週間前
4ヶ月前
2週間前
4ヶ月前
2週間前
4ヶ月前
2週間前
4ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  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 || 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. storeLocation: selectedLot.location,
  130. requiredQty: selectedLot.requiredQty,
  131. actualPickQty: selectedLot.actualPickQty || 0,
  132. missQty: 0,
  133. badItemQty: 0, // Bad Item Qty
  134. badPackageQty: 0, // Bad Package Qty (frontend only)
  135. issueRemark: "",
  136. pickerName: "",
  137. handledBy: undefined,
  138. reason: "",
  139. badReason: "",
  140. });
  141. initKeyRef.current = key;
  142. }, [
  143. open,
  144. selectedPickOrderLine?.id,
  145. selectedLot?.lotId,
  146. pickOrderId,
  147. pickOrderCreateDate,
  148. ]);
  149. const handleInputChange = useCallback(
  150. (field: keyof PickExecutionIssueData, value: any) => {
  151. setFormData((prev) => ({ ...prev, [field]: value }));
  152. if (errors[field as keyof FormErrors]) {
  153. setErrors((prev) => ({ ...prev, [field]: undefined }));
  154. }
  155. },
  156. [errors]
  157. );
  158. // Updated validation logic
  159. const validateForm = (): boolean => {
  160. const newErrors: FormErrors = {};
  161. const ap = Number(formData.actualPickQty) || 0;
  162. const miss = Number(formData.missQty) || 0;
  163. const badItem = Number(formData.badItemQty) || 0;
  164. const badPackage = Number((formData as any).badPackageQty) || 0;
  165. const totalBad = badItem + badPackage;
  166. const total = ap + miss + totalBad;
  167. const availableQty = selectedLot?.availableQty || 0;
  168. // 1. Check actualPickQty cannot be negative
  169. if (ap < 0) {
  170. newErrors.actualPickQty = t("Qty cannot be negative");
  171. }
  172. // 2. Check actualPickQty cannot exceed available quantity
  173. if (ap > availableQty) {
  174. newErrors.actualPickQty = t("Actual pick qty cannot exceed available qty");
  175. }
  176. // 3. Check missQty and both bad qtys cannot be negative
  177. if (miss < 0) {
  178. newErrors.missQty = t("Invalid qty");
  179. }
  180. if (badItem < 0 || badPackage < 0) {
  181. newErrors.badItemQty = t("Invalid qty");
  182. }
  183. // 4. Total (actualPickQty + missQty + badItemQty + badPackageQty) cannot exceed lot available qty
  184. if (total > availableQty) {
  185. const errorMsg = t(
  186. "Total qty (actual pick + miss + bad) cannot exceed available qty: {available}",
  187. { available: availableQty }
  188. );
  189. newErrors.actualPickQty = errorMsg;
  190. newErrors.missQty = errorMsg;
  191. newErrors.badItemQty = errorMsg;
  192. }
  193. // 5. At least one field must have a value
  194. if (ap === 0 && miss === 0 && totalBad === 0) {
  195. newErrors.actualPickQty = t("Enter pick qty or issue qty");
  196. }
  197. setErrors(newErrors);
  198. return Object.keys(newErrors).length === 0;
  199. };
  200. const handleSubmit = async () => {
  201. if (!validateForm()) {
  202. console.error("Form validation failed:", errors);
  203. return;
  204. }
  205. if (!formData.pickOrderId) {
  206. console.error("Missing pickOrderId");
  207. return;
  208. }
  209. const badItem = Number(formData.badItemQty) || 0;
  210. const badPackage = Number((formData as any).badPackageQty) || 0;
  211. const totalBadQty = badItem + badPackage;
  212. let badReason: string | undefined;
  213. if (totalBadQty > 0) {
  214. // assumption: only one of them is > 0
  215. badReason = badPackage > 0 ? "package_problem" : "quantity_problem";
  216. }
  217. const submitData: PickExecutionIssueData = {
  218. ...(formData as PickExecutionIssueData),
  219. badItemQty: totalBadQty,
  220. badReason,
  221. };
  222. setLoading(true);
  223. try {
  224. await onSubmit(submitData);
  225. } catch (error: any) {
  226. console.error("Error submitting pick execution issue:", error);
  227. alert(
  228. t("Failed to submit issue. Please try again.") +
  229. (error.message ? `: ${error.message}` : "")
  230. );
  231. } finally {
  232. setLoading(false);
  233. }
  234. };
  235. const handleClose = () => {
  236. setFormData({});
  237. setErrors({});
  238. onClose();
  239. };
  240. if (!selectedLot || !selectedPickOrderLine) {
  241. return null;
  242. }
  243. const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot);
  244. const requiredQty = calculateRequiredQty(selectedLot);
  245. return (
  246. <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
  247. <DialogTitle>
  248. {t("Pick Execution Issue Form") + " - "+selectedPickOrderLine.itemCode+ " "+ selectedPickOrderLine.itemName}
  249. <br />
  250. {selectedLot.lotNo}
  251. </DialogTitle>
  252. <DialogContent>
  253. <Box sx={{ mt: 2 }}>
  254. <Grid container spacing={2}>
  255. <Grid item xs={6}>
  256. <TextField
  257. fullWidth
  258. label={t("Required Qty")}
  259. value={requiredQty}
  260. disabled
  261. variant="outlined"
  262. />
  263. </Grid>
  264. <Grid item xs={6}>
  265. <TextField
  266. fullWidth
  267. label={t("Remaining Available Qty")}
  268. value={remainingAvailableQty}
  269. disabled
  270. variant="outlined"
  271. />
  272. </Grid>
  273. <Grid item xs={12}>
  274. <TextField
  275. fullWidth
  276. label={t("Actual Pick Qty")}
  277. type="number"
  278. inputProps={{
  279. inputMode: "numeric",
  280. pattern: "[0-9]*",
  281. min: 0,
  282. }}
  283. value={formData.actualPickQty ?? ""}
  284. onChange={(e) =>
  285. handleInputChange(
  286. "actualPickQty",
  287. e.target.value === ""
  288. ? undefined
  289. : Math.max(0, Number(e.target.value) || 0)
  290. )
  291. }
  292. error={!!errors.actualPickQty}
  293. helperText={
  294. errors.actualPickQty || `${t("Max")}: ${remainingAvailableQty}`
  295. }
  296. variant="outlined"
  297. />
  298. </Grid>
  299. <Grid item xs={12}>
  300. <FormControl fullWidth>
  301. <InputLabel>{t("Reason")}</InputLabel>
  302. <Select
  303. value={formData.reason || ""}
  304. onChange={(e) => handleInputChange("reason", e.target.value)}
  305. label={t("Reason")}
  306. >
  307. <MenuItem value="">{t("Select Reason")}</MenuItem>
  308. <MenuItem value="miss">{t("Edit")}</MenuItem>
  309. <MenuItem value="bad">{t("Just Complete")}</MenuItem>
  310. </Select>
  311. </FormControl>
  312. </Grid>
  313. <Grid item xs={12}>
  314. <TextField
  315. fullWidth
  316. label={t("Missing item Qty")}
  317. type="number"
  318. inputProps={{
  319. inputMode: "numeric",
  320. pattern: "[0-9]*",
  321. min: 0,
  322. }}
  323. value={formData.missQty || 0}
  324. onChange={(e) =>
  325. handleInputChange(
  326. "missQty",
  327. e.target.value === ""
  328. ? undefined
  329. : Math.max(0, Number(e.target.value) || 0)
  330. )
  331. }
  332. error={!!errors.missQty}
  333. variant="outlined"
  334. />
  335. </Grid>
  336. <Grid item xs={12}>
  337. <TextField
  338. fullWidth
  339. label={t("Bad Item Qty")}
  340. type="number"
  341. inputProps={{
  342. inputMode: "numeric",
  343. pattern: "[0-9]*",
  344. min: 0,
  345. }}
  346. value={formData.badItemQty || 0}
  347. onChange={(e) =>
  348. handleInputChange(
  349. "badItemQty",
  350. e.target.value === ""
  351. ? undefined
  352. : Math.max(0, Number(e.target.value) || 0)
  353. )
  354. }
  355. error={!!errors.badItemQty}
  356. //helperText={t("Quantity Problem")}
  357. variant="outlined"
  358. />
  359. </Grid>
  360. <Grid item xs={12}>
  361. <TextField
  362. fullWidth
  363. label={t("Bad Package Qty")}
  364. type="number"
  365. inputProps={{
  366. inputMode: "numeric",
  367. pattern: "[0-9]*",
  368. min: 0,
  369. }}
  370. value={(formData as any).badPackageQty || 0}
  371. onChange={(e) =>
  372. handleInputChange(
  373. "badPackageQty",
  374. e.target.value === ""
  375. ? undefined
  376. : Math.max(0, Number(e.target.value) || 0)
  377. )
  378. }
  379. error={!!errors.badItemQty}
  380. //helperText={t("Package Problem")}
  381. variant="outlined"
  382. />
  383. </Grid>
  384. </Grid>
  385. </Box>
  386. </DialogContent>
  387. <DialogActions>
  388. <Button onClick={handleClose} disabled={loading}>
  389. {t("Cancel")}
  390. </Button>
  391. <Button onClick={handleSubmit} variant="contained" disabled={loading}>
  392. {loading ? t("submitting") : t("submit")}
  393. </Button>
  394. </DialogActions>
  395. </Dialog>
  396. );
  397. };
  398. export default PickExecutionForm;