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

QcStockInModal.tsx 32 KiB

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