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

QcStockInModal.tsx 34 KiB

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