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.
 
 
 

597 line
21 KiB

  1. "use client";
  2. import { StockInLine } from "@/app/api/po";
  3. import { ModalFormInput, PurchaseQcResult, StockInLineEntry, updateStockInLine, PurchaseQCInput, printQrCodeForSil, PrintQrCodeForSilRequest } from "@/app/api/po/actions";
  4. import { QcItemWithChecks, QcData } from "@/app/api/qc";
  5. import {
  6. Autocomplete,
  7. Box,
  8. Button,
  9. Grid,
  10. Modal,
  11. ModalProps,
  12. Stack,
  13. TextField,
  14. Typography,
  15. } from "@mui/material";
  16. import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
  17. import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form";
  18. import { StockInLineRow } from "./PoInputGrid";
  19. import { useTranslation } from "react-i18next";
  20. import StockInForm from "./StockInForm";
  21. import StockInFormVer2 from "./StockInFormVer2";
  22. import QcComponent from "./QcComponent";
  23. import { dummyPutAwayLine, dummyQCData } from "./dummyQcTemplate";
  24. // import QcFormVer2 from "./QcFormVer2";
  25. import PutAwayForm from "./PutAwayForm";
  26. import { GridRowModes, useGridApiRef } from "@mui/x-data-grid";
  27. import {submitDialogWithWarning} from "../Swal/CustomAlerts";
  28. import { INPUT_DATE_FORMAT, arrayToDateString, arrayToInputDateString, dayjsToInputDateString } from "@/app/utils/formatUtil";
  29. import dayjs from "dayjs";
  30. import { fetchPoQrcode } from "@/app/api/pdf/actions";
  31. import { downloadFile } from "@/app/utils/commonUtil";
  32. import { PrinterCombo } from "@/app/api/settings/printer";
  33. import { EscalationResult } from "@/app/api/escalation";
  34. import { SessionWithTokens } from "@/config/authConfig";
  35. import { GridRowModesModel } from "@mui/x-data-grid";
  36. import { isEmpty } from "lodash";
  37. const style = {
  38. position: "absolute",
  39. top: "50%",
  40. left: "50%",
  41. transform: "translate(-50%, -50%)",
  42. bgcolor: "background.paper",
  43. pt: 5,
  44. px: 5,
  45. pb: 10,
  46. display: "block",
  47. width: { xs: "90%", sm: "90%", md: "90%" },
  48. // height: { xs: "60%", sm: "60%", md: "60%" },
  49. };
  50. interface CommonProps extends Omit<ModalProps, "children"> {
  51. // setRows: Dispatch<SetStateAction<PurchaseOrderLine[]>>;
  52. setEntries?: Dispatch<SetStateAction<StockInLineRow[]>>;
  53. setStockInLine?: Dispatch<SetStateAction<StockInLine[]>>;
  54. itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] };
  55. setItemDetail: Dispatch<
  56. SetStateAction<
  57. | (StockInLine & {
  58. warehouseId?: number;
  59. })
  60. | undefined
  61. >
  62. >;
  63. session: SessionWithTokens | null;
  64. qc?: QcItemWithChecks[];
  65. warehouse?: any[];
  66. // type: "qc" | "stockIn" | "escalation" | "putaway" | "reject";
  67. handleMailTemplateForStockInLine: (stockInLineId: number) => void;
  68. printerCombo: PrinterCombo[];
  69. onClose: () => void;
  70. }
  71. interface Props extends CommonProps {
  72. itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] };
  73. }
  74. const PoQcStockInModalVer2: React.FC<Props> = ({
  75. // type,
  76. // setRows,
  77. setEntries,
  78. setStockInLine,
  79. open,
  80. onClose,
  81. itemDetail,
  82. setItemDetail,
  83. session,
  84. qc,
  85. warehouse,
  86. handleMailTemplateForStockInLine,
  87. printerCombo
  88. }) => {
  89. const {
  90. t,
  91. i18n: { language },
  92. } = useTranslation("purchaseOrder");
  93. // Select Printer
  94. const [selectedPrinter, setSelectedPrinter] = useState(printerCombo[0]);
  95. const defaultNewValue = useMemo(() => {
  96. return (
  97. {
  98. ...itemDetail,
  99. status: itemDetail.status ?? "pending",
  100. dnDate: arrayToInputDateString(itemDetail.dnDate)?? dayjsToInputDateString(dayjs()),
  101. // putAwayLines: dummyPutAwayLine,
  102. // putAwayLines: itemDetail.putAwayLines.map((line) => (return {...line, printQty: 1})) ?? [],
  103. putAwayLines: itemDetail.putAwayLines?.map((line) => ({...line, printQty: 1, _isNew: false})) ?? [],
  104. // qcResult: (itemDetail.qcResult && itemDetail.qcResult?.length > 0) ? itemDetail.qcResult : [],//[...dummyQCData],
  105. escResult: (itemDetail.escResult && itemDetail.escResult?.length > 0) ? itemDetail.escResult : [],
  106. receiptDate: itemDetail.receiptDate ?? dayjs().add(0, "month").format(INPUT_DATE_FORMAT),
  107. acceptQty: itemDetail.demandQty?? itemDetail.acceptedQty,
  108. warehouseId: itemDetail.defaultWarehouseId ?? 1
  109. }
  110. )
  111. },[itemDetail])
  112. const [qcItems, setQcItems] = useState(dummyQCData)
  113. const formProps = useForm<ModalFormInput>({
  114. defaultValues: {
  115. ...defaultNewValue,
  116. },
  117. });
  118. const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>(
  119. () => {
  120. onClose?.();
  121. // reset();
  122. },
  123. [onClose],
  124. );
  125. const isPutaway = () => {
  126. if (itemDetail) {
  127. const status = itemDetail.status;
  128. return status == "received";
  129. } else return false;
  130. };
  131. const [viewOnly, setViewOnly] = useState(false);
  132. useEffect(() => {
  133. if (itemDetail && itemDetail.status) {
  134. const isViewOnly = itemDetail.status.toLowerCase() == "completed"
  135. || itemDetail.status.toLowerCase() == "partially_completed" // TODO update DB
  136. || itemDetail.status.toLowerCase() == "rejected"
  137. || (itemDetail.status.toLowerCase() == "escalated" && session?.id != itemDetail.handlerId)
  138. setViewOnly(isViewOnly)
  139. }
  140. console.log("Modal ItemDetail updated:", itemDetail);
  141. }, [itemDetail]);
  142. useEffect(() => {
  143. const qcRes = itemDetail?.qcResult;
  144. // if (!qcRes || qcRes?.length <= 0) {
  145. // itemDetail.qcResult = dummyQCData;
  146. // }
  147. formProps.reset({
  148. ...defaultNewValue
  149. })
  150. setQcItems(dummyQCData);
  151. setOpenPutaway(isPutaway);
  152. }, [open])
  153. const [openPutaway, setOpenPutaway] = useState(false);
  154. const onOpenPutaway = useCallback(() => {
  155. setOpenPutaway(true);
  156. }, []);
  157. const onClosePutaway = useCallback(() => {
  158. setOpenPutaway(false);
  159. }, []);
  160. // Stock In submission handler
  161. const onSubmitStockIn = useCallback<SubmitHandler<ModalFormInput>>(
  162. async (data, event) => {
  163. console.log("Stock In Submission:", event!.nativeEvent);
  164. // Extract only stock-in related fields
  165. const stockInData = {
  166. // quantity: data.quantity,
  167. // receiptDate: data.receiptDate,
  168. // batchNumber: data.batchNumber,
  169. // expiryDate: data.expiryDate,
  170. // warehouseId: data.warehouseId,
  171. // location: data.location,
  172. // unitCost: data.unitCost,
  173. data: data,
  174. // Add other stock-in specific fields from your form
  175. };
  176. console.log("Stock In Data:", stockInData);
  177. // Handle stock-in submission logic here
  178. // e.g., call API, update state, etc.
  179. },
  180. [],
  181. );
  182. // QC submission handler
  183. const onSubmitErrorQc = useCallback<SubmitErrorHandler<ModalFormInput>>(
  184. async (data, event) => {
  185. console.log("Error", data);
  186. }, []
  187. );
  188. // QC submission handler
  189. const onSubmitQc = useCallback<SubmitHandler<ModalFormInput>>(
  190. async (data, event) => {
  191. console.log("QC Submission:", event!.nativeEvent);
  192. // TODO: Move validation into QC page
  193. // if (errors.length > 0) {
  194. // alert(`未完成品檢: ${errors.map((err) => err[1].message)}`);
  195. // return;
  196. // }
  197. // Get QC data from the shared form context
  198. const qcAccept = data.qcDecision == 1;
  199. // const qcAccept = data.qcAccept;
  200. const acceptQty = Number(data.acceptQty);
  201. const qcResults = data.qcResult || dummyQCData; // qcItems;
  202. // const qcResults = data.qcResult as PurchaseQcResult[]; // qcItems;
  203. // const qcResults = viewOnly? data.qcResult as PurchaseQcResult[] : qcItems;
  204. // Validate QC data
  205. const validationErrors : string[] = [];
  206. // Check if failed items have failed quantity
  207. const failedItemsWithoutQty = qcResults.filter(item =>
  208. item.qcPassed === false && (!item.failQty || item.failQty <= 0)
  209. );
  210. if (failedItemsWithoutQty.length > 0) {
  211. validationErrors.push(`${t("Failed items must have failed quantity")}`);
  212. // validationErrors.push(`${t("Failed items must have failed quantity")}: ${failedItemsWithoutQty.map(item => item.code).join(', ')}`);
  213. }
  214. // Check if QC accept decision is made
  215. if (data.qcDecision === undefined) {
  216. // if (qcAccept === undefined) {
  217. validationErrors.push(t("QC decision is required"));
  218. }
  219. // Check if accept quantity is valid
  220. if (acceptQty === undefined || acceptQty <= 0) {
  221. validationErrors.push("Accept quantity must be greater than 0");
  222. }
  223. // Check if dates are input
  224. if (data.productionDate === undefined || data.productionDate == null) {
  225. alert("請輸入生產日期!");
  226. return;
  227. }
  228. if (data.expiryDate === undefined || data.expiryDate == null) {
  229. alert("請輸入到期日!");
  230. return;
  231. }
  232. if (!qcResults.every((qc) => qc.qcPassed) && qcAccept && itemDetail.status != "escalated") { //TODO: fix it please!
  233. validationErrors.push("有不合格檢查項目,無法收貨!");
  234. // submitDialogWithWarning(() => postStockInLineWithQc(qcData), t, {title:"有不合格檢查項目,確認接受收貨?",
  235. // confirmButtonText: t("confirm putaway"), html: ""});
  236. // return;
  237. }
  238. // Check if all QC items have results
  239. const itemsWithoutResult = qcResults.filter(item => item.qcPassed === undefined);
  240. if (itemsWithoutResult.length > 0 && itemDetail.status != "escalated") { //TODO: fix it please!
  241. validationErrors.push(`${t("QC items without result")}`);
  242. // validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.code).join(', ')}`);
  243. }
  244. if (validationErrors.length > 0) {
  245. console.error("QC Validation failed:", validationErrors);
  246. alert(`未完成品檢: ${validationErrors}`);
  247. return;
  248. }
  249. const qcData = {
  250. dnNo : data.dnNo? data.dnNo : "DN00000",
  251. dnDate : data.dnDate? arrayToInputDateString(data.dnDate) : dayjsToInputDateString(dayjs()),
  252. productionDate : arrayToInputDateString(data.productionDate),
  253. expiryDate : arrayToInputDateString(data.expiryDate),
  254. receiptDate : arrayToInputDateString(data.receiptDate),
  255. qcAccept: qcAccept? qcAccept : false,
  256. acceptQty: acceptQty? acceptQty : 0,
  257. qcResult: itemDetail.status != "escalated" ? qcResults.map(item => ({
  258. id: item.id,
  259. qcItemId: item.id,
  260. // code: item.code,
  261. // qcDescription: item.qcDescription,
  262. qcPassed: item.qcPassed? item.qcPassed : false,
  263. failQty: (item.failQty && !item.qcPassed) ? item.failQty : 0,
  264. // failedQty: (typeof item.failedQty === "number" && !item.isPassed) ? item.failedQty : 0,
  265. remarks: item.remarks || ''
  266. })) : [],
  267. };
  268. // const qcData = data;
  269. console.log("QC Data for submission:", qcData);
  270. if (data.qcDecision == 3) { // Escalate
  271. const escalationLog = {
  272. type : "qc",
  273. status : "pending", // TODO: update with supervisor decision
  274. reason : data.escalationLog?.reason,
  275. recordDate : dayjsToInputDateString(dayjs()),
  276. handlerId : Number(session?.id),
  277. }
  278. console.log("Escalation Data for submission", escalationLog);
  279. await postStockInLine({...qcData, escalationLog});
  280. } else {
  281. await postStockInLine(qcData);
  282. }
  283. if (qcData.qcAccept) {
  284. // submitDialogWithWarning(onOpenPutaway, t, {title:"Save success, confirm to proceed?",
  285. // confirmButtonText: t("confirm putaway"), html: ""});
  286. onOpenPutaway();
  287. } else {
  288. closeHandler({}, "backdropClick");
  289. }
  290. return ;
  291. },
  292. [onOpenPutaway, qcItems, formProps.formState.errors],
  293. );
  294. const postStockInLine = useCallback(async (args: ModalFormInput) => {
  295. const submitData = {
  296. ...itemDetail, ...args
  297. } as StockInLineEntry & ModalFormInput;
  298. console.log("Submitting", submitData);
  299. const res = await updateStockInLine(submitData);
  300. return res;
  301. },[itemDetail])
  302. // Email supplier handler
  303. const onSubmitEmailSupplier = useCallback<SubmitHandler<ModalFormInput>>(
  304. async (data, event) => {
  305. console.log("Email Supplier Submission:", event!.nativeEvent);
  306. // Extract only email supplier related fields
  307. const emailData = {
  308. // supplierEmail: data.supplierEmail,
  309. // issueDescription: data.issueDescription,
  310. // qcComments: data.qcComments,
  311. // defectNotes: data.defectNotes,
  312. // attachments: data.attachments,
  313. // escalationReason: data.escalationReason,
  314. data: data,
  315. // Add other email-specific fields
  316. };
  317. console.log("Email Supplier Data:", emailData);
  318. // Handle email supplier logic here
  319. // e.g., send email to supplier, log escalation, etc.
  320. },
  321. [],
  322. );
  323. // Put away model
  324. const [pafRowModesModel, setPafRowModesModel] = useState<GridRowModesModel>({})
  325. const pafSubmitDisable = useMemo(() => {
  326. // console.log("%c mode: ", "background:#90EE90; color:red", Object.entries(pafRowModesModel))
  327. // console.log("%c mode: ", "background:pink; color:#87CEEB", Object.entries(pafRowModesModel))
  328. return Object.entries(pafRowModesModel).length > 0 || Object.entries(pafRowModesModel).some(([key, value], index) => value.mode === GridRowModes.Edit)
  329. }, [pafRowModesModel])
  330. // Putaway submission handler
  331. const onSubmitPutaway = useCallback<SubmitHandler<ModalFormInput>>(
  332. async (data, event) => {
  333. // console.log("Putaway Submission:", event!.nativeEvent);
  334. // console.log(data.putAwayLines)
  335. // console.log(data.putAwayLines?.filter((line) => line._isNew !== false))
  336. // Extract only putaway related fields
  337. const putawayData = {
  338. // putawayLine: data.putawayLine,
  339. // putawayLocation: data.putawayLocation,
  340. // binLocation: data.binLocation,
  341. // putawayQuantity: data.putawayQuantity,
  342. // putawayNotes: data.putawayNotes,
  343. acceptQty: Number(data.acceptQty?? (itemDetail.demandQty?? (itemDetail.acceptedQty))), //TODO improve
  344. warehouseId: data.warehouseId,
  345. status: data.status, //TODO Fix it!
  346. // ...data,
  347. dnDate : data.dnDate? arrayToInputDateString(data.dnDate) : dayjsToInputDateString(dayjs()),
  348. productionDate : arrayToInputDateString(data.productionDate),
  349. expiryDate : arrayToInputDateString(data.expiryDate),
  350. receiptDate : arrayToInputDateString(data.receiptDate),
  351. // for putaway data
  352. inventoryLotLines: data.putAwayLines?.filter((line) => line._isNew !== false)
  353. // Add other putaway specific fields
  354. } as ModalFormInput;
  355. console.log("Putaway Data:", putawayData);
  356. console.log("DEBUG",data.putAwayLines);
  357. if (data.putAwayLines!!.filter((line) => line._isNew !== false).length <= 0) {
  358. alert("請新增上架資料!");
  359. return;
  360. }
  361. console.log(typeof data.putAwayLines!![0].qty + " = 'number'");
  362. console.log(typeof data.putAwayLines!![0].qty !== "number");
  363. if (data.putAwayLines!!.filter((line) => /[^0-9]/.test(String(line.qty))).length > 0) { //TODO Improve
  364. alert("上架數量不正確!");
  365. return;
  366. }
  367. if (data.putAwayLines!!.reduce((acc, cur) => acc + Number(cur.qty), 0) > putawayData.acceptQty!!) {
  368. alert(`上架數量不能大於 ${putawayData.acceptQty}!`);
  369. return;
  370. }
  371. // Handle putaway submission logic here
  372. const res = await postStockInLine(putawayData);
  373. console.log("result ", res);
  374. // Close modal after successful putaway
  375. closeHandler({}, "backdropClick");
  376. },
  377. [closeHandler],
  378. );
  379. // Print handler
  380. const [isPrinting, setIsPrinting] = useState(false)
  381. const handlePrint = useCallback(async () => {
  382. // console.log("Print putaway documents");
  383. console.log("%c data", "background: white; color: red", formProps.watch("putAwayLines"));
  384. // Handle print logic here
  385. // window.print();
  386. // const postData = { stockInLineIds: [itemDetail.id]};
  387. // const response = await fetchPoQrcode(postData);
  388. // if (response) {
  389. // downloadFile(new Uint8Array(response.blobValue), response.filename)
  390. // }
  391. try {
  392. setIsPrinting(() => true)
  393. const data: PrintQrCodeForSilRequest = {
  394. stockInLineId: itemDetail.id,
  395. printerId: selectedPrinter.id,
  396. printQty: formProps.watch("putAwayLines")?.reduce((acc, cur) => acc + cur.printQty, 0)
  397. }
  398. const response = await printQrCodeForSil(data);
  399. if (response) {
  400. console.log(response)
  401. }
  402. } finally {
  403. setIsPrinting(() => false)
  404. }
  405. }, [itemDetail.id]);
  406. const acceptQty = formProps.watch("acceptedQty")
  407. const checkQcIsPassed = useCallback((qcItems: PurchaseQcResult[]) => {
  408. const isPassed = qcItems.every((qc) => qc.qcPassed);
  409. console.log(isPassed)
  410. if (isPassed) {
  411. formProps.setValue("passingQty", acceptQty)
  412. } else {
  413. formProps.setValue("passingQty", 0)
  414. }
  415. return isPassed
  416. }, [acceptQty, formProps])
  417. useEffect(() => {
  418. // maybe check if submitted before
  419. console.log("Modal QC Items updated:", qcItems);
  420. // checkQcIsPassed(qcItems)
  421. }, [qcItems, checkQcIsPassed])
  422. return (
  423. <>
  424. <FormProvider {...formProps}>
  425. <Modal open={open} onClose={closeHandler}>
  426. <Box
  427. sx={{
  428. ...style,
  429. padding: 2,
  430. maxHeight: "90vh",
  431. overflowY: "auto",
  432. marginLeft: 3,
  433. marginRight: 3,
  434. }}
  435. >
  436. {openPutaway ? (
  437. <Box
  438. component="form"
  439. onSubmit={formProps.handleSubmit(onSubmitPutaway)}
  440. >
  441. <PutAwayForm
  442. printerCombo={printerCombo}
  443. itemDetail={itemDetail}
  444. warehouse={warehouse!}
  445. disabled={viewOnly}
  446. setRowModesModel={setPafRowModesModel}
  447. />
  448. <Stack direction="row" justifyContent="flex-end" gap={1}>
  449. <Autocomplete
  450. disableClearable
  451. options={printerCombo}
  452. defaultValue={selectedPrinter}
  453. onChange={(event, value) => {
  454. setSelectedPrinter(value)
  455. }}
  456. renderInput={(params) => (
  457. <TextField
  458. {...params}
  459. variant="outlined"
  460. label={t("Printer")}
  461. sx={{ width: 300}}
  462. />
  463. )}
  464. />
  465. <Button
  466. id="printButton"
  467. type="button"
  468. variant="contained"
  469. color="primary"
  470. sx={{ mt: 1 }}
  471. onClick={handlePrint}
  472. disabled={isPrinting || printerCombo.length <= 0}
  473. >
  474. {isPrinting ? t("Printing") : t("print")}
  475. </Button>
  476. <Button
  477. id="putawaySubmit"
  478. type="submit"
  479. variant="contained"
  480. color="primary"
  481. sx={{ mt: 1 }}
  482. onClick={formProps.handleSubmit(onSubmitPutaway)}
  483. disabled={pafSubmitDisable}
  484. >
  485. {t("confirm putaway")}
  486. </Button>
  487. </Stack>
  488. </Box>
  489. ) : (
  490. <>
  491. <Grid
  492. container
  493. justifyContent="flex-start"
  494. alignItems="flex-start"
  495. >
  496. <Grid item xs={12}>
  497. <Typography variant="h6" display="block" marginBlockEnd={1}>
  498. {t("qc processing")}
  499. </Typography>
  500. </Grid>
  501. <Grid item xs={12}>
  502. <StockInFormVer2 itemDetail={itemDetail} disabled={viewOnly} />
  503. </Grid>
  504. </Grid>
  505. {/* <Stack direction="row" justifyContent="flex-end" gap={1}>
  506. <Button
  507. id="stockInSubmit"
  508. type="button"
  509. variant="contained"
  510. color="primary"
  511. onClick={formProps.handleSubmit(onSubmitStockIn)}
  512. >
  513. {t("submitStockIn")}
  514. </Button>
  515. </Stack> */}
  516. <Grid
  517. container
  518. justifyContent="flex-start"
  519. alignItems="flex-start"
  520. >
  521. <QcComponent
  522. qc={qc!}
  523. itemDetail={itemDetail}
  524. disabled={viewOnly}
  525. // qcItems={qcItems}
  526. // setQcItems={setQcItems}
  527. />
  528. </Grid>
  529. <Stack direction="row" justifyContent="flex-end" gap={1}>
  530. {!viewOnly && (<Button
  531. id="qcSubmit"
  532. type="button"
  533. variant="contained"
  534. color="primary"
  535. sx={{ mt: 1 }}
  536. onClick={formProps.handleSubmit(onSubmitQc, onSubmitErrorQc)}
  537. >
  538. {t("confirm qc result")}
  539. </Button>)}
  540. </Stack>
  541. </>
  542. )}
  543. </Box>
  544. </Modal>
  545. </FormProvider>
  546. </>
  547. );
  548. };
  549. export default PoQcStockInModalVer2;