FPSMS-frontend
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 

439 líneas
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 || 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;