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.
 
 

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