FPSMS-frontend
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

896 строки
34 KiB

  1. "use client";
  2. import { QcItemWithChecks, QcData } from "@/app/api/qc";
  3. import {
  4. Autocomplete,
  5. Box,
  6. Button,
  7. Divider,
  8. Grid,
  9. Modal,
  10. ModalProps,
  11. Stack,
  12. Tab,
  13. Tabs,
  14. TabsProps,
  15. TextField,
  16. Typography,
  17. } from "@mui/material";
  18. import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
  19. import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form";
  20. import { StockInLineRow } from "../PoDetail/PoInputGrid";
  21. import { useTranslation } from "react-i18next";
  22. import StockInForm from "../StockIn/StockInForm";
  23. import QcComponent from "./QcComponent";
  24. import PutAwayForm from "../PoDetail/PutAwayForm";
  25. import { GridRowModes, GridRowSelectionModel, useGridApiRef } from "@mui/x-data-grid";
  26. import {msg, submitDialogWithWarning} from "../Swal/CustomAlerts";
  27. import { INPUT_DATE_FORMAT, arrayToDateString, dayjsToDateTimeString } from "@/app/utils/formatUtil";
  28. import dayjs from "dayjs";
  29. import { fetchPoQrcode } from "@/app/api/pdf/actions";
  30. import { downloadFile } from "@/app/utils/commonUtil";
  31. import { PrinterCombo } from "@/app/api/settings/printer";
  32. import { EscalationResult } from "@/app/api/escalation";
  33. import { SessionWithTokens } from "@/config/authConfig";
  34. import { GridRowModesModel } from "@mui/x-data-grid";
  35. import { isEmpty } from "lodash";
  36. import { EscalationCombo } from "@/app/api/user";
  37. import { truncateSync } from "fs";
  38. import { ModalFormInput, StockInLineInput, StockInLine, StockInStatus } from "@/app/api/stockIn";
  39. import { StockInLineEntry, updateStockInLine, printQrCodeForSil, PrintQrCodeForSilRequest } from "@/app/api/stockIn/actions";
  40. import { fetchStockInLineInfo } from "@/app/api/stockIn/actions";
  41. import FgStockInForm from "../StockIn/FgStockInForm";
  42. import LoadingComponent from "../General/LoadingComponent";
  43. import { printFGStockInLabel, PrintFGStockInLabelRequest, fetchFGStockInLabel } from "@/app/api/jo/actions";
  44. import { fetchItemForPutAway } from "@/app/api/stockIn/actions";
  45. import { fetchWarehouseListClient } from "@/app/api/warehouse/client";
  46. const style = {
  47. position: "absolute",
  48. top: "50%",
  49. left: "50%",
  50. transform: "translate(-50%, -50%)",
  51. bgcolor: "background.paper",
  52. display: "block",
  53. width: "min(1280px, calc(100vw - 48px))",
  54. maxWidth: "1280px",
  55. height: "min(900px, calc(100vh - 48px))",
  56. maxHeight: "calc(100vh - 48px)",
  57. };
  58. interface CommonProps extends Omit<ModalProps, "children" | "onClose"> {
  59. // itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] } | undefined;
  60. inputDetail: StockInLineInput | undefined;
  61. session: SessionWithTokens | null;
  62. warehouse?: any[];
  63. printerCombo: PrinterCombo[];
  64. onClose: (updatedStockInLine?: StockInLine) => void;
  65. skipQc?: Boolean;
  66. printSource?: "stockIn" | "productionProcess";
  67. uiMode?: "default" | "dashboard" | "productionProcess";
  68. }
  69. interface Props extends CommonProps {
  70. // itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] };
  71. }
  72. const QcStockInModal: React.FC<Props> = ({
  73. open,
  74. onClose,
  75. // itemDetail,
  76. inputDetail,
  77. session,
  78. warehouse,
  79. printerCombo,
  80. skipQc = false,
  81. printSource = "stockIn",
  82. uiMode = "default",
  83. }) => {
  84. const {
  85. t,
  86. i18n: { language },
  87. } = useTranslation("purchaseOrder");
  88. const useCompactFields = useMemo(() => {
  89. return uiMode === "dashboard" || uiMode === "productionProcess";
  90. }, [uiMode]);
  91. const useDenseQcLayout = useMemo(() => {
  92. return uiMode === "productionProcess";
  93. }, [uiMode]);
  94. const [stockInLineInfo, setStockInLineInfo] = useState<StockInLine>();
  95. const [isLoading, setIsLoading] = useState<boolean>(false);
  96. const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
  97. // const [skipQc, setSkipQc] = useState<Boolean>(false);
  98. // const [viewOnly, setViewOnly] = useState(false);
  99. const [itemLocationCode, setItemLocationCode] = useState<string | null>(null);
  100. const printerStorageKey = useMemo(
  101. () => `qcStockInModal_selectedPrinterId_${session?.id ?? "guest"}`,
  102. [session?.id],
  103. );
  104. const labelPrinterCombo = useMemo(
  105. () => (printerCombo || []).filter((p) => p.type === "Label"),
  106. [printerCombo],
  107. );
  108. const getDefaultPrinter = useMemo(() => {
  109. if (!printerCombo.length) return undefined;
  110. if (typeof window === "undefined") return printerCombo[0];
  111. const savedId = sessionStorage.getItem(printerStorageKey);
  112. const matched = savedId ? printerCombo.find(p => p.id === Number(savedId)) : undefined;
  113. return matched ?? printerCombo[0];
  114. }, [printerCombo, printerStorageKey]);
  115. const [selectedPrinter, setSelectedPrinter] = useState(labelPrinterCombo[0]);
  116. const [printQty, setPrintQty] = useState(1);
  117. const [tabIndex, setTabIndex] = useState(0);
  118. const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
  119. (_e, newValue) => {
  120. setTabIndex(newValue);
  121. },
  122. [],
  123. );
  124. const fetchStockInLineData = useCallback(
  125. async (stockInLineId: number) => {
  126. try {
  127. const res = await fetchStockInLineInfo(stockInLineId);
  128. if (res) {
  129. console.log("%c Fetched Stock In Line: ", "color:orange", res);
  130. console.log("%c [QC] itemId in response:", "color:yellow", res.itemId);
  131. console.log("%c [QC] locationCode in response:", "color:yellow", res.locationCode);
  132. // 如果 res 中没有 itemId,检查是否有其他方式获取
  133. if (!res.itemId) {
  134. console.warn("%c [QC] Warning: itemId is missing in response!", "color:red");
  135. }
  136. setStockInLineInfo({...inputDetail, ...res, expiryDate: res.expiryDate});
  137. } else throw("Result is undefined");
  138. } catch (e) {
  139. console.log("%c Error when fetching Stock In Line: ", "color:red", e);
  140. console.log("%c Error details: ", "color:red", {
  141. message: e instanceof Error ? e.message : String(e),
  142. stack: e instanceof Error ? e.stack : undefined
  143. });
  144. alert("Something went wrong, please retry");
  145. closeHandler({}, "backdropClick");
  146. }
  147. },[fetchStockInLineInfo, inputDetail]
  148. );
  149. // Fetch info if id is input
  150. useEffect(() => {
  151. setIsLoading(true);
  152. setIsSubmitting(false);
  153. if (inputDetail && open) {
  154. console.log("%c Opened Modal with input:", "color:yellow", inputDetail);
  155. if (inputDetail.id) {
  156. const id = inputDetail.id;
  157. fetchStockInLineData(id);
  158. }
  159. }
  160. }, [open]);
  161. useEffect(() => {
  162. // 如果后端已经在 StockInLine 中返回了 locationCode,直接使用
  163. if (stockInLineInfo?.locationCode) {
  164. setItemLocationCode(stockInLineInfo.locationCode);
  165. console.log("%c [QC] item LocationCode from API:", "color:cyan", stockInLineInfo.locationCode);
  166. return;
  167. }
  168. // 如果没有 locationCode,尝试从 itemId 获取(向后兼容)
  169. const loadItemLocationCode = async () => {
  170. if (!stockInLineInfo?.itemId) return;
  171. try {
  172. const itemResult = await fetchItemForPutAway(stockInLineInfo.itemId);
  173. const item = itemResult.item;
  174. const locationCode = item.LocationCode || item.locationCode || null;
  175. setItemLocationCode(locationCode);
  176. console.log("%c [QC] item LocationCode from fetchItemForPutAway:", "color:cyan", locationCode);
  177. } catch (error) {
  178. console.error("Error fetching item to get LocationCode in QC:", error);
  179. setItemLocationCode(null);
  180. }
  181. };
  182. if (stockInLineInfo && stockInLineInfo.status !== StockInStatus.REJECTED) {
  183. loadItemLocationCode();
  184. }
  185. }, [stockInLineInfo]);
  186. // Make sure stock in line info is fetched
  187. useEffect(() => {
  188. if (stockInLineInfo) {
  189. if (stockInLineInfo.id) {
  190. if (isLoading) {
  191. formProps.reset({
  192. ...defaultNewValue
  193. });
  194. console.log("%c Modal loaded successfully", "color:lime");
  195. setIsLoading(false);
  196. }
  197. }
  198. }
  199. }, [stockInLineInfo]);
  200. const defaultNewValue = useMemo(() => {
  201. const d = stockInLineInfo;
  202. if (d !== undefined) {
  203. // console.log("%c sil info", "color:yellow", d )
  204. return (
  205. {
  206. ...d,
  207. // status: d.status ?? "pending",
  208. productionDate: d.productionDate ? arrayToDateString(d.productionDate, "input") : dayjs().format(INPUT_DATE_FORMAT),
  209. expiryDate: d.expiryDate ? (Array.isArray(d.expiryDate) ? arrayToDateString(d.expiryDate, "input") : d.expiryDate) : undefined,
  210. receiptDate: d.receiptDate ? arrayToDateString(d.receiptDate, "input")
  211. : dayjs().add(0, "month").format(INPUT_DATE_FORMAT),
  212. acceptQty: d.status != StockInStatus.REJECTED ? (d.acceptedQty ?? d.receivedQty ?? d.demandQty) : 0,
  213. // escResult: (d.escResult && d.escResult?.length > 0) ? d.escResult : [],
  214. // qcResult: (d.qcResult && d.qcResult?.length > 0) ? d.qcResult : [],//[...dummyQCData],
  215. warehouseId: d.defaultWarehouseId ?? 1141,
  216. putAwayLines: d.putAwayLines?.map((line) => ({...line, printQty: 1, _isNew: false, _disableDelete: true})) ?? [],
  217. } as ModalFormInput
  218. )
  219. } return undefined
  220. }, [stockInLineInfo])
  221. // const [qcItems, setQcItems] = useState(dummyQCData)
  222. const formProps = useForm<ModalFormInput>({
  223. defaultValues: {
  224. ...defaultNewValue,
  225. },
  226. });
  227. const closeWithResult = useCallback(
  228. (updatedStockInLine?: StockInLine) => {
  229. setStockInLineInfo(undefined);
  230. formProps.reset({});
  231. onClose?.(updatedStockInLine);
  232. },
  233. [onClose],
  234. );
  235. const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>(
  236. () => {
  237. setStockInLineInfo(undefined);
  238. formProps.reset({});
  239. onClose?.();
  240. },
  241. [onClose],
  242. );
  243. const isPutaway = () => {
  244. if (stockInLineInfo) {
  245. const status = stockInLineInfo.status;
  246. return status == "received";
  247. } else return false;
  248. };
  249. // Get show putaway
  250. const showPutaway = useMemo(() => {
  251. if (stockInLineInfo) {
  252. const status = stockInLineInfo.status;
  253. return status !== StockInStatus.PENDING && status !== StockInStatus.ESCALATED && status !== StockInStatus.REJECTED;
  254. }
  255. return false;
  256. }, [stockInLineInfo]);
  257. // Get is view only
  258. const viewOnly = useMemo(() => {
  259. if (stockInLineInfo) {
  260. if (stockInLineInfo.status) {
  261. const status = stockInLineInfo.status;
  262. const isViewOnly = status.toLowerCase() == StockInStatus.COMPLETED
  263. || status.toLowerCase() == StockInStatus.PARTIALLY_COMPLETED // TODO update DB
  264. || status.toLowerCase() == StockInStatus.REJECTED
  265. || (status.toLowerCase() == StockInStatus.ESCALATED && session?.id != stockInLineInfo.handlerId)
  266. if (showPutaway) { setTabIndex(1); } else { setTabIndex(0); }
  267. return isViewOnly;
  268. }
  269. }
  270. return true;
  271. }, [stockInLineInfo])
  272. const [openPutaway, setOpenPutaway] = useState(false);
  273. const onOpenPutaway = useCallback(() => {
  274. setOpenPutaway(true);
  275. }, []);
  276. const onClosePutaway = useCallback(() => {
  277. setOpenPutaway(false);
  278. }, []);
  279. // Stock In submission handler
  280. const onSubmitStockIn = useCallback<SubmitHandler<ModalFormInput>>(
  281. async (data, event) => {
  282. console.log("Stock In Submission:", event!.nativeEvent);
  283. // Extract only stock-in related fields
  284. const stockInData = {
  285. // quantity: data.quantity,
  286. // receiptDate: data.receiptDate,
  287. // batchNumber: data.batchNumber,
  288. // expiryDate: data.expiryDate,
  289. // warehouseId: data.warehouseId,
  290. // location: data.location,
  291. // unitCost: data.unitCost,
  292. data: data,
  293. // Add other stock-in specific fields from your form
  294. };
  295. console.log("Stock In Data:", stockInData);
  296. // Handle stock-in submission logic here
  297. // e.g., call API, update state, etc.
  298. },
  299. [],
  300. );
  301. // QC submission handler
  302. const onSubmitErrorQc = useCallback<SubmitErrorHandler<ModalFormInput>>(
  303. async (data, event) => {
  304. console.log("Error", data);
  305. }, []
  306. );
  307. // QC submission handler
  308. const onSubmitQc = useCallback<SubmitHandler<ModalFormInput>>(
  309. async (data, event) => {
  310. console.log("QC Submission:", event!.nativeEvent);
  311. console.log("Validating:", data.qcResult);
  312. // TODO: Move validation into QC page
  313. // if (errors.length > 0) {
  314. // alert(`未完成品檢: ${errors.map((err) => err[1].message)}`);
  315. // return;
  316. // }
  317. // Get QC data from the shared form context
  318. const qcAcceptLocal = data.qcDecision == 1;
  319. // const qcAccept = data.qcAccept;
  320. let acceptQtyLocal = Number(data.acceptQty);
  321. const qcResultsLocal = data.qcResult?.filter((qc) => qc.escalationLogId === undefined) || []; // Remove old QC data
  322. // const qcResults = data.qcResult as PurchaseQcResult[]; // qcItems;
  323. // const qcResults = viewOnly? data.qcResult as PurchaseQcResult[] : qcItems;
  324. // Validate QC data
  325. const validationErrors : string[] = [];
  326. // Check if failed items have failed quantity
  327. const failedItemsWithoutQty = qcResultsLocal.filter(item =>
  328. item.qcPassed === false && (!item.failQty || item.failQty <= 0)
  329. );
  330. if (failedItemsWithoutQty.length > 0) {
  331. validationErrors.push(`${t("Failed items must have failed quantity")}`);
  332. // validationErrors.push(`${t("Failed items must have failed quantity")}: ${failedItemsWithoutQty.map(item => item.code).join(', ')}`);
  333. }
  334. // Check if QC accept decision is made
  335. if (data.qcDecision === undefined) {
  336. // if (qcAccept === undefined) {
  337. validationErrors.push(t("QC decision is required"));
  338. }
  339. // Check if accept quantity is valid
  340. if (data.qcDecision == 2) {
  341. acceptQtyLocal = 0;
  342. } else {
  343. if (acceptQtyLocal === undefined || acceptQtyLocal <= 0) {
  344. validationErrors.push("Accept quantity must be greater than 0");
  345. }
  346. }
  347. // Check if dates are input
  348. // if (data.productionDate === undefined || data.productionDate == null) {
  349. // alert("請輸入生產日期!");
  350. // return;
  351. // }
  352. if (data.expiryDate === undefined || data.expiryDate == null) {
  353. alert("請輸入到期日!");
  354. return;
  355. }
  356. if (!qcResultsLocal.every((qc) => qc.qcPassed) && qcAcceptLocal && stockInLineInfo?.status != StockInStatus.ESCALATED) { //TODO: fix it please!
  357. validationErrors.push("有不合格檢查項目,無法收貨!");
  358. // submitDialogWithWarning(() => postStockInLineWithQc(qcData), t, {title:"有不合格檢查項目,確認接受收貨?",
  359. // confirmButtonText: t("confirm putaway"), html: ""});
  360. // return;
  361. }
  362. // Check if all QC items have results
  363. const itemsWithoutResult = qcResultsLocal.filter(item => item.qcPassed === undefined);
  364. if (itemsWithoutResult.length > 0 && stockInLineInfo?.status != StockInStatus.ESCALATED) { //TODO: fix it please!
  365. validationErrors.push(`${t("QC items without result")}`);
  366. // validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.code).join(', ')}`);
  367. }
  368. if (validationErrors.length > 0 && !skipQc) {
  369. console.error("QC Validation failed:", validationErrors);
  370. alert(`未完成品檢: ${validationErrors}`);
  371. return;
  372. }
  373. const isJobOrderSource = Boolean(stockInLineInfo?.jobOrderId) || printSource === "productionProcess";
  374. const qcData = {
  375. dnNo : data.dnNo? data.dnNo : "DN00000",
  376. // dnDate : data.dnDate? arrayToDateString(data.dnDate, "input") : dayjsToInputDateString(dayjs()),
  377. productionDate : data.productionDate ? (Array.isArray(data.productionDate) ? arrayToDateString(data.productionDate, "input") : data.productionDate) : undefined,
  378. expiryDate : data.expiryDate ? (Array.isArray(data.expiryDate) ? arrayToDateString(data.expiryDate, "input") : data.expiryDate) : undefined,
  379. receiptDate : data.receiptDate ? (Array.isArray(data.receiptDate) ? arrayToDateString(data.receiptDate, "input") : data.receiptDate) : undefined,
  380. qcAccept: qcAcceptLocal ? qcAcceptLocal : false,
  381. acceptQty: acceptQtyLocal ? acceptQtyLocal : 0,
  382. // For Job Order QC, allow updating received qty beyond demand/accepted.
  383. // Backend uses request.acceptedQty in QC flow, so we must send it explicitly.
  384. acceptedQty: (qcAcceptLocal && isJobOrderSource) ? (acceptQtyLocal ? acceptQtyLocal : 0) : stockInLineInfo?.acceptedQty,
  385. // qcResult: itemDetail.status != "escalated" ? qcResults.map(item => ({
  386. qcResult: qcResultsLocal.map(item => ({
  387. // id: item.id,
  388. qcItemId: item.qcItemId,
  389. // code: item.code,
  390. // qcDescription: item.qcDescription,
  391. qcPassed: item.qcPassed? item.qcPassed : false,
  392. failQty: (item.failQty && !item.qcPassed) ? item.failQty : 0,
  393. // failedQty: (typeof item.failedQty === "number" && !item.isPassed) ? item.failedQty : 0,
  394. remarks: item.remarks || '',
  395. })),
  396. };
  397. // const qcData = data;
  398. console.log("QC Data for submission:", qcData);
  399. let qcRes: StockInLine | undefined;
  400. if (data.qcDecision == 3) { // Escalate
  401. if (data.escalationLog?.handlerId == undefined) { alert("請選擇上報負責同事!"); return; }
  402. else if (data.escalationLog?.handlerId < 1) { alert("上報負責同事資料有誤"); return; }
  403. const escalationLog = {
  404. type : "qc",
  405. status : "pending", // TODO: update with supervisor decision
  406. reason : data.escalationLog?.reason,
  407. recordDate : dayjsToDateTimeString(dayjs()),
  408. handlerId : data.escalationLog?.handlerId,
  409. }
  410. console.log("Escalation Data for submission", escalationLog);
  411. setIsSubmitting(true);
  412. const resEscalate = await postStockInLine({...qcData, escalationLog});
  413. qcRes = Array.isArray(resEscalate.entity) ? resEscalate.entity[0] : (resEscalate.entity as StockInLine);
  414. } else {
  415. setIsSubmitting(true);
  416. const resNormal = await postStockInLine(qcData);
  417. qcRes = Array.isArray(resNormal.entity) ? resNormal.entity[0] : (resNormal.entity as StockInLine);
  418. }
  419. if (qcData.qcAccept) {
  420. // submitDialogWithWarning(onOpenPutaway, t, {title:"Save success, confirm to proceed?",
  421. // confirmButtonText: t("confirm putaway"), html: ""});
  422. // onOpenPutaway();
  423. const isJobOrderSource = (stockInLineInfo?.jobOrderId != null || printSource === "productionProcess");
  424. const isWipBom = isJobOrderSource && stockInLineInfo?.bomDescription === "WIP";
  425. const isFgBom = isJobOrderSource && stockInLineInfo?.bomDescription === "FG";
  426. const isFaItem = (stockInLineInfo?.itemNo ?? "").toUpperCase().includes("FA");
  427. const shouldAutoPutaway = isWipBom || isFgBom || isFaItem;
  428. if (shouldAutoPutaway) {
  429. // Auto putaway to default warehouse
  430. const loc = (itemLocationCode ?? "").trim().toUpperCase();
  431. const warehouseListForLookup =
  432. isFgBom && ((warehouse?.length ?? 0) === 0)
  433. ? await fetchWarehouseListClient()
  434. : (warehouse ?? []);
  435. const matchedWarehouse =
  436. isFgBom && loc.length > 0
  437. ? warehouseListForLookup.find((w) => (w.code ?? "").trim().toUpperCase().endsWith(loc))
  438. : undefined;
  439. const resolvedWarehouseId =
  440. (isFgBom ? matchedWarehouse?.id : undefined)
  441. ?? stockInLineInfo?.defaultWarehouseId
  442. ?? 1141;
  443. console.log("[AUTO_PUTAWAY_DEBUG]", {
  444. silId: stockInLineInfo?.id,
  445. bomDescription: stockInLineInfo?.bomDescription,
  446. isJobOrderSource,
  447. isWipBom,
  448. isFgBom,
  449. isFaItem,
  450. itemLocationCode,
  451. loc,
  452. warehouseCount: warehouse?.length,
  453. matchedWarehouse: matchedWarehouse ? { id: matchedWarehouse.id, code: matchedWarehouse.code } : null,
  454. defaultWarehouseId: stockInLineInfo?.defaultWarehouseId,
  455. resolvedWarehouseId,
  456. });
  457. // Get warehouse name from warehouse prop or use default
  458. // Create putaway data
  459. const putawayData = {
  460. id: stockInLineInfo?.id, // Include ID
  461. itemId: stockInLineInfo?.itemId, // Include Item ID
  462. purchaseOrderId: stockInLineInfo?.purchaseOrderId, // Include PO ID if exists
  463. purchaseOrderLineId: stockInLineInfo?.purchaseOrderLineId, // Include POL ID if exists
  464. acceptedQty: acceptQtyLocal, // Keep in sync with QC acceptQty
  465. acceptQty: acceptQtyLocal, // Putaway quantity
  466. warehouseId: resolvedWarehouseId,
  467. status: "received", // Use string like PutAwayModal
  468. productionDate: data.productionDate ? (Array.isArray(data.productionDate) ? arrayToDateString(data.productionDate, "input") : data.productionDate) : undefined,
  469. expiryDate: data.expiryDate ? (Array.isArray(data.expiryDate) ? arrayToDateString(data.expiryDate, "input") : data.expiryDate) : undefined,
  470. receiptDate: data.receiptDate ? (Array.isArray(data.receiptDate) ? arrayToDateString(data.receiptDate, "input") : data.receiptDate) : undefined,
  471. inventoryLotLines: [{
  472. warehouseId: resolvedWarehouseId,
  473. qty: acceptQtyLocal, // Putaway qty
  474. }],
  475. } as StockInLineEntry & ModalFormInput;
  476. console.log("[AUTO_PUTAWAY]", {
  477. isFgBom,
  478. itemLocationCode,
  479. loc,
  480. warehouseCount: warehouse?.length,
  481. matchedWarehouse: isFgBom ? warehouse?.find(w => (w.code ?? "").trim().toUpperCase().endsWith(loc)) : null,
  482. resolvedWarehouseId,
  483. });
  484. try {
  485. // Use updateStockInLine directly like PutAwayModal does
  486. const res = await updateStockInLine(putawayData);
  487. if (Boolean(res.id)) {
  488. console.log("Auto putaway completed for job order bom");
  489. }
  490. } catch (error) {
  491. console.error("Error during auto putaway:", error);
  492. alert(t("Auto putaway failed. Please complete putaway manually."));
  493. }
  494. }
  495. closeWithResult(qcRes);
  496. // setTabIndex(1); // Need to go Putaway tab?
  497. } else {
  498. closeWithResult(qcRes);
  499. }
  500. setIsSubmitting(false);
  501. msg("已更新來貨狀態");
  502. return ;
  503. },
  504. [
  505. stockInLineInfo,
  506. printSource,
  507. skipQc,
  508. warehouse,
  509. itemLocationCode,
  510. closeWithResult,
  511. t,
  512. ],
  513. );
  514. const postStockInLine = useCallback(async (args: ModalFormInput) => {
  515. const submitData = {
  516. ...stockInLineInfo, ...args
  517. } as StockInLineEntry & ModalFormInput;
  518. console.log("Submitting", submitData);
  519. const res = await updateStockInLine(submitData);
  520. return res;
  521. }, [stockInLineInfo])
  522. // Put away model
  523. const [pafRowModesModel, setPafRowModesModel] = useState<GridRowModesModel>({})
  524. const [pafRowSelectionModel, setPafRowSelectionModel] = useState<GridRowSelectionModel>([])
  525. const pafSubmitDisable = useMemo(() => {
  526. return Object.entries(pafRowModesModel).length > 0 || Object.entries(pafRowModesModel).some(([key, value], index) => value.mode === GridRowModes.Edit)
  527. }, [pafRowModesModel])
  528. // Putaway submission handler
  529. const onSubmitPutaway = useCallback<SubmitHandler<ModalFormInput>>(
  530. async (data, event) => {
  531. // Extract only putaway related fields
  532. const putawayData = {
  533. acceptQty: Number(data.acceptQty?? (stockInLineInfo?.demandQty?? (stockInLineInfo?.acceptedQty))), //TODO improve
  534. warehouseId: data.warehouseId,
  535. status: data.status, //TODO Fix it!
  536. // ...data,
  537. // dnDate : data.dnDate? arrayToDateString(data.dnDate, "input") : dayjsToInputDateString(dayjs()),
  538. productionDate : arrayToDateString(data.productionDate, "input"),
  539. expiryDate : arrayToDateString(data.expiryDate, "input"),
  540. receiptDate : arrayToDateString(data.receiptDate, "input"),
  541. // for putaway data
  542. inventoryLotLines: data.putAwayLines?.filter((line) => line._isNew !== false)
  543. // Add other putaway specific fields
  544. } as ModalFormInput;
  545. console.log("Putaway Data:", putawayData);
  546. console.log("DEBUG",data.putAwayLines);
  547. // if (data.putAwayLines!!.filter((line) => line._isNew !== false).length <= 0) {
  548. // alert("請新增上架資料!");
  549. // return;
  550. // }
  551. if (data.putAwayLines!!.filter((line) => /[^0-9]/.test(String(line.qty))).length > 0) { //TODO Improve
  552. alert("上架數量不正確!");
  553. return;
  554. }
  555. if (data.putAwayLines!!.reduce((acc, cur) => acc + Number(cur.qty), 0) > putawayData.acceptQty!!) {
  556. alert(`上架數量不能大於 ${putawayData.acceptQty}!`);
  557. return;
  558. }
  559. // Handle putaway submission logic here
  560. const res = await postStockInLine(putawayData);
  561. console.log("result ", res);
  562. // Close modal after successful putaway
  563. closeHandler({}, "backdropClick");
  564. },
  565. [closeHandler],
  566. );
  567. // Print handler
  568. useEffect(() => {
  569. if (!printerCombo.length) return;
  570. if (typeof window === "undefined") {
  571. setSelectedPrinter(printerCombo[0]);
  572. return;
  573. }
  574. const savedId = sessionStorage.getItem(printerStorageKey);
  575. const matched = savedId ? printerCombo.find(p => p.id === Number(savedId)) : undefined;
  576. setSelectedPrinter(matched ?? printerCombo[0]);
  577. }, [printerCombo, printerStorageKey]);
  578. const [isPrinting, setIsPrinting] = useState(false)
  579. const handlePrint = useCallback(async () => {
  580. // console.log("Print putaway documents");
  581. console.log("%c data", "background: white; color: red", formProps.watch("putAwayLines"));
  582. // Handle print logic here
  583. // window.print();
  584. // const postData = { stockInLineIds: [itemDetail.id]};
  585. // const response = await fetchPoQrcode(postData);
  586. // if (response) {
  587. // downloadFile(new Uint8Array(response.blobValue), response.filename)
  588. // }
  589. try {
  590. setIsPrinting(() => true)
  591. if ((formProps.watch("putAwayLines") ?? []).filter((line) => /[^0-9]/.test(String(line.printQty))).length > 0) { //TODO Improve
  592. alert("列印數量不正確!");
  593. return;
  594. }
  595. // Conditionally call different APIs based on source
  596. let response;
  597. if (printSource === "productionProcess") {
  598. // Use FG Stock In Label print API for production process
  599. const data: PrintFGStockInLabelRequest = {
  600. stockInLineId: stockInLineInfo?.id ?? 0,
  601. printerId: selectedPrinter.id,
  602. printQty: printQty
  603. }
  604. response = await printFGStockInLabel(data);
  605. } else {
  606. // Use stock-in print API (default)
  607. const data: PrintQrCodeForSilRequest = {
  608. stockInLineId: stockInLineInfo?.id ?? 0,
  609. printerId: selectedPrinter.id,
  610. printQty: printQty
  611. }
  612. response = await printQrCodeForSil(data);
  613. }
  614. if (response) {
  615. console.log(response)
  616. }
  617. if (typeof window !== 'undefined' && selectedPrinter) {
  618. sessionStorage.setItem(printerStorageKey, String(selectedPrinter.id));
  619. }
  620. } finally {
  621. setIsPrinting(() => false)
  622. }
  623. }, [stockInLineInfo?.id, pafRowSelectionModel, printQty, selectedPrinter, printSource]);
  624. // const checkQcIsPassed = useCallback((qcItems: PurchaseQcResult[]) => {
  625. // const isPassed = qcItems.every((qc) => qc.qcPassed);
  626. // console.log(isPassed)
  627. // if (isPassed) {
  628. // formProps.setValue("passingQty", acceptQty)
  629. // } else {
  630. // formProps.setValue("passingQty", 0)
  631. // }
  632. // return isPassed
  633. // }, [acceptQty, formProps])
  634. const printQrcode = useCallback(
  635. async () => {
  636. setIsPrinting(true);
  637. try {
  638. let response;
  639. if (printSource === "productionProcess") {
  640. // Use FG Stock In Label download API for production process
  641. if (!stockInLineInfo?.id) {
  642. console.error("Stock In Line ID is required for download");
  643. setIsPrinting(false);
  644. return;
  645. }
  646. const postData = { stockInLineId: stockInLineInfo.id };
  647. response = await fetchFGStockInLabel(postData);
  648. } else {
  649. const postData = { stockInLineIds: [stockInLineInfo?.id] };
  650. response = await fetchPoQrcode(postData);
  651. }
  652. if (response) {
  653. console.log(response);
  654. downloadFile(new Uint8Array(response.blobValue), response.filename!);
  655. }
  656. } catch (e) {
  657. console.log("%c Error downloading QR Code", "color:red", e);
  658. } finally {
  659. setIsPrinting(false);
  660. }
  661. },
  662. [stockInLineInfo, printSource],
  663. );
  664. return (
  665. <>
  666. <FormProvider {...formProps}>
  667. <Modal open={open} onClose={closeHandler}>
  668. <Box
  669. sx={{
  670. ...style,
  671. // padding: 2,
  672. maxHeight: "90vh",
  673. overflowY: "auto",
  674. marginLeft: 3,
  675. marginRight: 3,
  676. // overflow: "hidden",
  677. display: 'flex',
  678. flexDirection: 'column',
  679. }}
  680. >
  681. {(!isLoading && stockInLineInfo) ? (<>
  682. <Box sx={{ position: 'sticky', top: 0, bgcolor: 'background.paper',
  683. zIndex: 5, borderBottom: 2, borderColor: 'divider', width: "100%"}}>
  684. <Tabs
  685. value={tabIndex}
  686. onChange={handleTabChange}
  687. variant="scrollable"
  688. sx={{pl: 2, pr: 2, pt: 2}}
  689. >
  690. <Tab label={
  691. showPutaway ? t("dn and qc info") : t("qc processing")
  692. } iconPosition="end" />
  693. {showPutaway && <Tab label={t("putaway processing")} iconPosition="end" />}
  694. </Tabs>
  695. </Box>
  696. <Grid
  697. container
  698. justifyContent="flex-start"
  699. alignItems="flex-start"
  700. sx={{padding: 2}}
  701. >
  702. <Grid item xs={12}>
  703. {tabIndex === 0 &&
  704. <Box>
  705. <Grid item xs={12}>
  706. <Typography variant="subtitle1" display="block" marginBlockEnd={1} fontWeight={600}>
  707. {t("Delivery Detail")}
  708. </Typography>
  709. </Grid>
  710. {stockInLineInfo.jobOrderId ? (
  711. <FgStockInForm
  712. itemDetail={stockInLineInfo}
  713. disabled={viewOnly || showPutaway}
  714. compactFields={useCompactFields}
  715. />
  716. ) : (
  717. <StockInForm
  718. itemDetail={stockInLineInfo}
  719. disabled={viewOnly || showPutaway}
  720. compactFields={useCompactFields}
  721. />
  722. )}
  723. {skipQc === false && (
  724. <QcComponent
  725. itemDetail={stockInLineInfo}
  726. disabled={viewOnly || showPutaway}
  727. compactLayout={uiMode === "dashboard" || uiMode === "productionProcess"}
  728. denseLayout={useDenseQcLayout}
  729. />)
  730. }
  731. <Stack direction="row" justifyContent="flex-end" gap={1} sx={{pt:2}}>
  732. {(!viewOnly && !showPutaway) && (<Button
  733. id="Submit"
  734. type="button"
  735. variant="contained"
  736. color="primary"
  737. sx={{ mt: 1 }}
  738. onClick={formProps.handleSubmit(onSubmitQc, onSubmitErrorQc)}
  739. disabled={isSubmitting || isLoading}
  740. >
  741. {isSubmitting ? (t("submitting")) : (skipQc ? t("confirm") : t("confirm qc result"))}
  742. </Button>)}
  743. </Stack>
  744. </Box>
  745. }
  746. {tabIndex === 1 &&
  747. <Box>
  748. <PutAwayForm
  749. itemDetail={stockInLineInfo}
  750. warehouse={warehouse!}
  751. disabled={viewOnly}
  752. setRowModesModel={setPafRowModesModel}
  753. setRowSelectionModel={setPafRowSelectionModel}
  754. suggestedLocationCode={itemLocationCode || undefined}
  755. />
  756. </Box>
  757. }
  758. </Grid>
  759. </Grid>
  760. {tabIndex == 1 && (
  761. <Stack direction="row" justifyContent="flex-end" gap={1} sx={{m:3, mt:"auto"}}>
  762. <Autocomplete
  763. disableClearable
  764. options={labelPrinterCombo}
  765. getOptionLabel={(option) =>
  766. option.name || option.label || option.code || `Printer ${option.id}`
  767. }
  768. value={selectedPrinter}
  769. onChange={(_, newValue) => {
  770. if (newValue) setSelectedPrinter(newValue);
  771. }}
  772. renderInput={(params) => (
  773. <TextField
  774. {...params}
  775. variant="outlined"
  776. label={t("Printer")}
  777. sx={{ width: 300 }}
  778. inputProps={{ ...params.inputProps, readOnly: true }}
  779. />
  780. )}
  781. />
  782. <TextField
  783. variant="outlined"
  784. label={t("Print Qty")}
  785. defaultValue={printQty}
  786. onChange={(event) => {
  787. event.target.value = event.target.value.replace(/[^0-9]/g, '')
  788. setPrintQty(Number(event.target.value))
  789. }}
  790. sx={{ width: 300}}
  791. />
  792. <Button
  793. id="printButton"
  794. type="button"
  795. variant="contained"
  796. color="primary"
  797. sx={{ mt: 1 }}
  798. onClick={handlePrint}
  799. disabled={isPrinting || printerCombo.length <= 0 || pafSubmitDisable}
  800. >
  801. {isPrinting ? t("Printing") : t("print")}
  802. </Button>
  803. <Button
  804. id="demoPrint"
  805. type="button"
  806. variant="contained"
  807. color="primary"
  808. sx={{ mt: 1 }}
  809. onClick={printQrcode}
  810. disabled={isPrinting}
  811. >
  812. {isPrinting ? t("downloading") : t("download Qr Code")}
  813. </Button>
  814. </Stack>
  815. )}
  816. </>) : <LoadingComponent/>}
  817. </Box>
  818. </Modal>
  819. </FormProvider>
  820. </>
  821. );
  822. };
  823. export default QcStockInModal;