FPSMS-frontend
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 

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