FPSMS-frontend
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 

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