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

QcStockInModal.tsx 32 KiB

5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
5ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
5ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
2ヶ月前
3ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
5ヶ月前
5ヶ月前
4ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
1ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
1ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822
  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;