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

1146 行
33 KiB

  1. "use client";
  2. import {
  3. FooterPropsOverrides,
  4. GridCellParams,
  5. GridRowId,
  6. GridRowIdGetter,
  7. GridRowModel,
  8. GridRowModes,
  9. GridRowModesModel,
  10. GridToolbarContainer,
  11. GridValidRowModel,
  12. useGridApiRef,
  13. } from "@mui/x-data-grid";
  14. import {
  15. Dispatch,
  16. MutableRefObject,
  17. SetStateAction,
  18. useCallback,
  19. useEffect,
  20. useMemo,
  21. useRef,
  22. useState,
  23. } from "react";
  24. import StyledDataGrid from "../StyledDataGrid";
  25. import { GridColDef } from "@mui/x-data-grid";
  26. import { Box, Button, Grid, Typography, useMediaQuery, useTheme } from "@mui/material";
  27. import { useTranslation } from "react-i18next";
  28. import { Add } from "@mui/icons-material";
  29. import SaveIcon from "@mui/icons-material/Save";
  30. import DeleteIcon from "@mui/icons-material/Delete";
  31. import CancelIcon from "@mui/icons-material/Cancel";
  32. import FactCheckIcon from "@mui/icons-material/FactCheck";
  33. import ShoppingCartIcon from "@mui/icons-material/ShoppingCart";
  34. // import { QcItemWithChecks } from "src/app/api/qc";
  35. import PlayArrowIcon from "@mui/icons-material/PlayArrow";
  36. import { PurchaseOrderLine } from "@/app/api/po";
  37. import { StockInLine } from "@/app/api/stockIn";
  38. import { createStockInLine, deleteStockInLine, updateStockInLine, QcResult } from "@/app/api/stockIn/actions";
  39. import { usePathname, useSearchParams } from "next/navigation";
  40. import {
  41. returnWeightUnit,
  42. calculateWeight,
  43. stockInLineStatusMap,
  44. arrayToDateString,
  45. } from "@/app/utils/formatUtil";
  46. // import PoQcStockInModal from "./PoQcStockInModal";
  47. import NotificationImportantIcon from "@mui/icons-material/NotificationImportant";
  48. import { WarehouseResult } from "@/app/api/warehouse";
  49. import LooksOneIcon from "@mui/icons-material/LooksOne";
  50. import LooksTwoIcon from "@mui/icons-material/LooksTwo";
  51. import Looks3Icon from "@mui/icons-material/Looks3";
  52. import axiosInstance from "@/app/(main)/axios/axiosInstance";
  53. // import axios, { AxiosRequestConfig } from "axios";
  54. import { BASE_API_URL, NEXT_PUBLIC_API_URL } from "@/config/api";
  55. import qs from "qs";
  56. import QrCodeIcon from "@mui/icons-material/QrCode";
  57. import { downloadFile } from "@/app/utils/commonUtil";
  58. import { fetchPoQrcode } from "@/app/api/pdf/actions";
  59. import { fetchQcResult } from "@/app/api/qc/actions";
  60. import DoDisturbIcon from "@mui/icons-material/DoDisturb";
  61. import { useSession } from "next-auth/react";
  62. // import { SessionWithTokens } from "src/config/authConfig";
  63. import QcStockInModal from "../Qc/QcStockInModal";
  64. import { decimalFormatter } from "@/app/utils/formatUtil";
  65. import { PrinterCombo } from "@/app/api/settings/printer";
  66. import { EscalationResult } from "@/app/api/escalation";
  67. import { fetchEscalationLogsByStockInLines } from "@/app/api/escalation/actions";
  68. import { SessionWithTokens } from "@/config/authConfig";
  69. import { EscalationCombo } from "@/app/api/user";
  70. import { deleteDialog } from "../Swal/CustomAlerts";
  71. import StockInLineRowActions from "./StockInLineRowActions";
  72. import { StockQtyRoundMode, needsPoQcStockQtyRound } from "./stockQtyRound";
  73. import { formatQtyWithPurchaseUom } from "./poPurchaseUom";
  74. // 3 buttons after QC (view + print QR + delete): 176*3 + gap + cell padding
  75. const ACTIONS_COLUMN_WIDTH = 580;
  76. const PURCHASE_QTY_COLUMN_WIDTH = 96;
  77. const UOM_COLUMN_WIDTH = 124;
  78. const STOCK_QTY_COLUMN_WIDTH = 110;
  79. const STOCK_IN_ROW_HEIGHT = 58;
  80. /** Extra table width is shared by text columns; qty / status / actions stay tight. */
  81. const COLUMN_GROW: Record<string, number> = {
  82. dnNo: 1,
  83. productLotNo: 1,
  84. uom: 1.5,
  85. stockQty: 1,
  86. stockUom: 1.5,
  87. };
  88. /** Tighter horizontal padding for narrow data columns (headers unchanged). */
  89. const COMPACT_STOCK_IN_CELL_FIELDS = [
  90. "dnNo",
  91. "productLotNo",
  92. "purchaseAcceptedQty",
  93. "uom",
  94. "stockUom",
  95. "status",
  96. ] as const;
  97. function canDeleteStockInLine(sil: StockInLineRow): boolean {
  98. if (sil._isNew || sil.status === "draft") {
  99. return true;
  100. }
  101. const hasPutAway = (sil.putAwayLines ?? []).some(
  102. (p) => Number(p.stockQty ?? p.qty ?? 0) > 0,
  103. );
  104. if (hasPutAway) return false;
  105. const status = (sil.status ?? "").toLowerCase();
  106. return status !== "completed" && status !== "partially_completed";
  107. }
  108. interface ResultWithId {
  109. id: number;
  110. }
  111. interface Props {
  112. // qc: QcItemWithChecks[];
  113. setRows: Dispatch<SetStateAction<PurchaseOrderLine[]>>;
  114. setStockInLine: Dispatch<SetStateAction<StockInLine[]>>;
  115. setProcessedQty: Dispatch<SetStateAction<number>>;
  116. itemDetail: PurchaseOrderLine;
  117. stockInLine: StockInLine[];
  118. warehouse: WarehouseResult[];
  119. fetchPoDetail: (poId: string, preserveDnNo?: boolean, preferredPolId?: number) => void;
  120. handleMailTemplateForStockInLine: (stockInLineId: number) => void;
  121. printerCombo: PrinterCombo[];
  122. }
  123. export type StockInLineEntryError = {
  124. [field in keyof StockInLine]?: string;
  125. };
  126. export type StockInLineRow = Partial<
  127. StockInLine & {
  128. isActive: boolean | undefined;
  129. _isNew: boolean;
  130. _error: StockInLineEntryError;
  131. } & ResultWithId
  132. >;
  133. class ProcessRowUpdateError extends Error {
  134. public readonly row: StockInLineRow;
  135. public readonly errors: StockInLineEntryError | undefined;
  136. constructor(
  137. row: StockInLineRow,
  138. message?: string,
  139. errors?: StockInLineEntryError,
  140. ) {
  141. super(message);
  142. this.row = row;
  143. this.errors = errors;
  144. Object.setPrototypeOf(this, ProcessRowUpdateError.prototype);
  145. }
  146. }
  147. /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.5 | 2026-09-18 */
  148. function PoInputGrid({
  149. // qc,
  150. setRows,
  151. setStockInLine,
  152. setProcessedQty,
  153. itemDetail,
  154. stockInLine,
  155. warehouse,
  156. fetchPoDetail,
  157. handleMailTemplateForStockInLine,
  158. printerCombo,
  159. }: Props) {
  160. const { t } = useTranslation("purchaseOrder");
  161. const theme = useTheme();
  162. /** Narrow phones: hide low-priority columns. */
  163. const isCompact = useMediaQuery(theme.breakpoints.down("md"), { noSsr: true });
  164. const apiRef = useGridApiRef();
  165. const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({});
  166. const getRowId = useCallback<GridRowIdGetter<StockInLineRow>>(
  167. (row) => row.id as number,
  168. [],
  169. );
  170. const [entries, setEntries] = useState<StockInLineRow[]>(stockInLine || []);
  171. useEffect(() => {
  172. setEntries(stockInLine);
  173. }, [stockInLine])
  174. const [modalInfo, setModalInfo] = useState<
  175. StockInLine & { qcResult?: QcResult[] } & { escalationResult?: EscalationResult[] }
  176. >();
  177. const pathname = usePathname()
  178. const searchParams = useSearchParams();
  179. const [qcOpen, setQcOpen] = useState(false);
  180. const [escalOpen, setEscalOpen] = useState(false);
  181. const [stockInOpen, setStockInOpen] = useState(false);
  182. const [putAwayOpen, setPutAwayOpen] = useState(false);
  183. const [rejectOpen, setRejectOpen] = useState(false);
  184. const [btnIsLoading, setBtnIsLoading] = useState(false);
  185. const [isDeleting, setIsDeleting] = useState(false);
  186. const deleteInFlightRef = useRef(false);
  187. const roundInFlightRef = useRef(false);
  188. const [roundingSilId, setRoundingSilId] = useState<number | null>(null);
  189. const [currQty, setCurrQty] = useState(() => {
  190. const total = entries.reduce(
  191. // remaining qty (M18 unit)
  192. (acc, curr) => acc + (curr.purchaseAcceptedQty || 0),
  193. 0,
  194. );
  195. return total;
  196. });
  197. const { data: session } = useSession();
  198. const sessionToken = session as SessionWithTokens | null;
  199. useEffect(() => {
  200. const completedList = entries.filter(
  201. (e) => stockInLineStatusMap[e.status!] >= 8,
  202. );
  203. const processedQty = completedList.reduce(
  204. (acc, curr) => acc + (curr.acceptedQty || 0),
  205. 0,
  206. );
  207. setProcessedQty(processedQty);
  208. }, [entries, setProcessedQty]);
  209. const handleDelete = useCallback(
  210. (id: GridRowId) => () => {
  211. setEntries((es) => es.filter((e) => getRowId(e) !== id));
  212. },
  213. [getRowId],
  214. );
  215. const handleSoftDelete = useCallback(
  216. (row: StockInLineRow) => {
  217. if (deleteInFlightRef.current || isDeleting) return;
  218. if (
  219. needsPoQcStockQtyRound(
  220. row.purchaseOrderLineId,
  221. row.status,
  222. row.acceptedQty,
  223. )
  224. ) {
  225. alert("請先在換算庫存數量選擇向上或向下取整");
  226. return;
  227. }
  228. const rowId = row.id as number;
  229. const isDraft = row._isNew || row.status === "draft";
  230. const doDelete = async () => {
  231. if (deleteInFlightRef.current) return;
  232. deleteInFlightRef.current = true;
  233. setIsDeleting(true);
  234. try {
  235. if (isDraft) {
  236. handleDelete(rowId)();
  237. return;
  238. }
  239. await deleteStockInLine(rowId);
  240. await fetchPoDetail(
  241. String(itemDetail.purchaseOrderId),
  242. true,
  243. itemDetail.id,
  244. );
  245. } catch (error) {
  246. console.error("Failed to delete stock in line:", error);
  247. alert(t("Cannot delete put away record"));
  248. } finally {
  249. setIsDeleting(false);
  250. deleteInFlightRef.current = false;
  251. }
  252. };
  253. void deleteDialog(doDelete, t);
  254. },
  255. [fetchPoDetail, handleDelete, isDeleting, itemDetail.id, itemDetail.purchaseOrderId, t],
  256. );
  257. const handleRoundStockQty = useCallback(
  258. (row: StockInLineRow, mode: StockQtyRoundMode) => {
  259. if (roundInFlightRef.current) return;
  260. const silId = row.id;
  261. const itemId = row.itemId;
  262. const purchaseQty = Number(row.purchaseAcceptedQty ?? 0);
  263. if (!silId || !itemId || purchaseQty <= 0) return;
  264. const doRound = async () => {
  265. if (roundInFlightRef.current) return;
  266. roundInFlightRef.current = true;
  267. setRoundingSilId(silId);
  268. try {
  269. const res = await updateStockInLine({
  270. id: silId,
  271. itemId,
  272. purchaseOrderLineId: row.purchaseOrderLineId,
  273. acceptedQty: purchaseQty,
  274. dnNo: row.dnNo,
  275. productLotNo: row.productLotNo,
  276. stockQtyRoundMode: mode,
  277. stockQtyRoundSource: "CREATE",
  278. });
  279. if (res) {
  280. await fetchPoDetail(
  281. String(itemDetail.purchaseOrderId),
  282. true,
  283. itemDetail.id,
  284. );
  285. }
  286. } catch (error) {
  287. console.error("Failed to round stock qty:", error);
  288. alert(t("Please choose a rounding method"));
  289. } finally {
  290. setRoundingSilId(null);
  291. roundInFlightRef.current = false;
  292. }
  293. };
  294. void doRound();
  295. },
  296. [fetchPoDetail, itemDetail.id, itemDetail.purchaseOrderId, t],
  297. );
  298. const closeQcModal = useCallback(() => {
  299. setQcOpen(false);
  300. }, []);
  301. const openQcModal = useCallback(() => {
  302. setQcOpen(true);
  303. }, []);
  304. const closeStockInModal = useCallback(() => {
  305. setStockInOpen(false);
  306. }, []);
  307. const openStockInModal = useCallback(() => {
  308. setStockInOpen(true);
  309. }, []);
  310. const closePutAwayModal = useCallback(() => {
  311. setPutAwayOpen(false);
  312. }, []);
  313. const openPutAwayModal = useCallback(() => {
  314. setPutAwayOpen(true);
  315. }, []);
  316. const closeEscalationModal = useCallback(() => {
  317. setEscalOpen(false);
  318. }, []);
  319. const openEscalationModal = useCallback(() => {
  320. setEscalOpen(true);
  321. }, []);
  322. const closeRejectModal = useCallback(() => {
  323. setRejectOpen(false);
  324. }, []);
  325. const openRejectModal = useCallback(() => {
  326. setRejectOpen(true);
  327. }, []);
  328. const handleStart = useCallback( // NOTE: Seems unused!!!!!!!!
  329. (id: GridRowId, params: any) => () => {
  330. setBtnIsLoading(true);
  331. setRowModesModel((prev) => ({
  332. ...prev,
  333. [id]: { mode: GridRowModes.View },
  334. }));
  335. setTimeout(async () => {
  336. // post stock in line
  337. const oldId = params.row.id;
  338. const postData = {
  339. itemId: params.row.itemId,
  340. itemNo: params.row.itemNo,
  341. itemName: params.row.itemName,
  342. // purchaseOrderId: params.row.purchaseOrderId,
  343. purchaseOrderLineId: params.row.purchaseOrderLineId,
  344. // For PO-origin, backend expects M18 qty and converts it to stock qty.
  345. acceptedQty: params.row.purchaseAcceptedQty ?? params.row.acceptedQty,
  346. };
  347. const res = await createStockInLine(postData);
  348. console.log(res);
  349. setEntries((prev) =>
  350. prev.map((p) => (p.id === oldId ? (res.entity as StockInLine) : p)),
  351. );
  352. setStockInLine(
  353. (prev) =>
  354. prev.map((p) =>
  355. p.id === oldId ? (res.entity as StockInLine) : p,
  356. ) as StockInLine[],
  357. );
  358. setBtnIsLoading(false);
  359. // do post directly to test
  360. // openStartModal();
  361. }, 200);
  362. },
  363. [setStockInLine],
  364. );
  365. const fetchQcDefaultValue = useCallback(async (stockInLineId: GridRowId) => {
  366. return await fetchQcResult(stockInLineId as number);
  367. }, []);
  368. // const handleQC = useCallback( // UNUSED NOW!
  369. // (id: GridRowId, params: any) => async () => {
  370. // setBtnIsLoading(true);
  371. // setRowModesModel((prev) => ({
  372. // ...prev,
  373. // [id]: { mode: GridRowModes.View },
  374. // }));
  375. // const qcResult = await fetchQcDefaultValue(id);
  376. // // console.log(params.row);
  377. // console.log("Fetched QC Result:", qcResult);
  378. // setModalInfo({
  379. // ...params.row,
  380. // qcResult: qcResult,
  381. // });
  382. // // set default values
  383. // setTimeout(() => {
  384. // // open qc modal
  385. // console.log("delayed");
  386. // openQcModal();
  387. // setBtnIsLoading(false);
  388. // }, 200);
  389. // },
  390. // [fetchQcDefaultValue, openQcModal],
  391. // );
  392. const [newOpen, setNewOpen] = useState(false);
  393. const stockInLineIdFromNext = searchParams.get("stockInLineId");
  394. const poLineId = searchParams.get("poLineId");
  395. const patchQuery = useCallback(
  396. (mutate: (params: URLSearchParams) => void) => {
  397. if (typeof window === "undefined") return;
  398. const params = new URLSearchParams(window.location.search);
  399. mutate(params);
  400. const qs = params.toString();
  401. window.history.replaceState(
  402. window.history.state,
  403. "",
  404. qs ? `${pathname}?${qs}` : pathname,
  405. );
  406. },
  407. [pathname],
  408. );
  409. const getLiveStockInLineId = useCallback((): string | null => {
  410. if (typeof window !== "undefined") {
  411. return new URLSearchParams(window.location.search).get("stockInLineId");
  412. }
  413. return stockInLineIdFromNext;
  414. }, [stockInLineIdFromNext]);
  415. const closeNewModal = useCallback((updatedStockInLine?: StockInLine) => {
  416. patchQuery((params) => {
  417. params.delete("stockInLineId");
  418. });
  419. setNewOpen(false);
  420. if (updatedStockInLine?.id != null) {
  421. setEntries((prev) =>
  422. prev.map((e) => (e.id === updatedStockInLine.id ? { ...e, ...updatedStockInLine } : e))
  423. );
  424. setStockInLine((prev) =>
  425. (prev || []).map((p) => (p.id === updatedStockInLine.id ? { ...p, ...updatedStockInLine } : p))
  426. );
  427. }
  428. }, [patchQuery, setStockInLine]);
  429. // Open modal
  430. const openNewModal = useCallback(() => {
  431. setNewOpen(() => true);
  432. }, []);
  433. // Button handler to update the URL and open the modal
  434. const handleNewQC = useCallback(
  435. (id: GridRowId, params: any) => async() => {
  436. if (!params?.row) return;
  437. if (
  438. needsPoQcStockQtyRound(
  439. params.row.purchaseOrderLineId,
  440. params.row.status,
  441. params.row.acceptedQty,
  442. )
  443. ) {
  444. alert("請先在換算庫存數量選擇向上或向下取整");
  445. return;
  446. }
  447. setRowModesModel((prev) => ({
  448. ...prev,
  449. [id]: { mode: GridRowModes.View },
  450. }));
  451. setModalInfo(() => ({
  452. ...params.row,
  453. receivedQty: itemDetail.receivedQty,
  454. }));
  455. // Avoid router.replace — it scrolls the page to top
  456. patchQuery((params) => {
  457. params.set("stockInLineId", id.toString());
  458. });
  459. openNewModal();
  460. },
  461. [openNewModal, patchQuery, itemDetail.receivedQty],
  462. );
  463. // Open modal if `stockInLineId` exists in the live URL (and belongs to current grid)
  464. const [firstCheckForSil, setFirstCheckForSil] = useState(false);
  465. useEffect(() => {
  466. setFirstCheckForSil(false);
  467. }, [itemDetail.id]);
  468. useEffect(() => {
  469. if (!itemDetail || firstCheckForSil) return;
  470. const liveStockInLineId = getLiveStockInLineId();
  471. if (!liveStockInLineId) {
  472. setFirstCheckForSil(true);
  473. return;
  474. }
  475. const row = apiRef.current.getRow(Number(liveStockInLineId));
  476. if (!row) {
  477. // Stale query from another POL: drop it once current entries are known
  478. if (
  479. entries.length > 0 &&
  480. !entries.some((e) => String(e.id) === String(liveStockInLineId))
  481. ) {
  482. patchQuery((params) => {
  483. params.delete("stockInLineId");
  484. });
  485. setFirstCheckForSil(true);
  486. }
  487. return;
  488. }
  489. setFirstCheckForSil(true);
  490. void handleNewQC(liveStockInLineId, { row })();
  491. }, [
  492. stockInLineIdFromNext,
  493. poLineId,
  494. itemDetail,
  495. firstCheckForSil,
  496. entries,
  497. handleNewQC,
  498. getLiveStockInLineId,
  499. patchQuery,
  500. ]);
  501. const handleEscalation = useCallback(
  502. (id: GridRowId, params: any) => () => {
  503. // setBtnIsLoading(true);
  504. setRowModesModel((prev) => ({
  505. ...prev,
  506. [id]: { mode: GridRowModes.View },
  507. }));
  508. setModalInfo(params.row);
  509. setTimeout(() => {
  510. // open qc modal
  511. console.log("delayed");
  512. openEscalationModal();
  513. // setBtnIsLoading(false);
  514. }, 200);
  515. },
  516. [openEscalationModal],
  517. );
  518. const handleReject = useCallback(
  519. (id: GridRowId, params: any) => () => {
  520. setRowModesModel((prev) => ({
  521. ...prev,
  522. [id]: { mode: GridRowModes.View },
  523. }));
  524. setModalInfo(params.row);
  525. setTimeout(() => {
  526. // open stock in modal
  527. // openPutAwayModal();
  528. // return the record with its status as pending
  529. // update layout
  530. console.log("delayed");
  531. openRejectModal();
  532. // printQrcode(params.row);
  533. }, 200);
  534. },
  535. [openRejectModal],
  536. );
  537. const handleStockIn = useCallback(
  538. (id: GridRowId, params: any) => () => {
  539. // setBtnIsLoading(true);
  540. setRowModesModel((prev) => ({
  541. ...prev,
  542. [id]: { mode: GridRowModes.View },
  543. }));
  544. setModalInfo(params.row);
  545. setTimeout(() => {
  546. // open stock in modal
  547. openStockInModal();
  548. // return the record with its status as pending
  549. // update layout
  550. console.log("delayed");
  551. // setBtnIsLoading(false);
  552. }, 200);
  553. },
  554. [openStockInModal],
  555. );
  556. const handlePutAway = useCallback(
  557. (id: GridRowId, params: any) => () => {
  558. // setBtnIsLoading(true);
  559. setRowModesModel((prev) => ({
  560. ...prev,
  561. [id]: { mode: GridRowModes.View },
  562. }));
  563. setModalInfo(params.row);
  564. setTimeout(() => {
  565. // open stock in modal
  566. openPutAwayModal();
  567. // return the record with its status as pending
  568. // update layout
  569. console.log("delayed");
  570. // setBtnIsLoading(false);
  571. }, 200);
  572. },
  573. [openPutAwayModal],
  574. );
  575. const printQrcode = useCallback(
  576. async (row: any) => {
  577. setBtnIsLoading(true);
  578. console.log(row.id);
  579. const postData = { stockInLineIds: [row.id] };
  580. // const postData = { stockInLineIds: [42,43,44] };
  581. const response = await fetchPoQrcode(postData);
  582. if (response) {
  583. console.log(response);
  584. downloadFile(new Uint8Array(response.blobValue), response.filename!);
  585. }
  586. setBtnIsLoading(false);
  587. },
  588. [],
  589. );
  590. const getButtonSx = (sil: StockInLineRow) => {
  591. const status = sil?.status?.toLowerCase();
  592. let btnSx = { label: "", color: "" };
  593. switch (status) {
  594. case "received":
  595. btnSx = { label: t("view putaway"), color: "secondary.main" };
  596. break;
  597. case "escalated":
  598. if (sessionToken?.id == sil?.handlerId) {
  599. btnSx = { label: t("escalation processing"), color: "warning.main" };
  600. break;
  601. }
  602. btnSx = { label: t("qc processing"), color: "success.main" };
  603. break;
  604. case "rejected":
  605. case "partially_completed":
  606. case "completed":
  607. btnSx = { label: t("view stockin"), color: "info.main" };
  608. break;
  609. default:
  610. btnSx = { label: t("qc processing"), color: "success.main" };
  611. }
  612. return btnSx;
  613. };
  614. const columnVisibilityModel = useMemo(
  615. () => ({
  616. uom: !isCompact,
  617. stockUom: !isCompact,
  618. }),
  619. [isCompact],
  620. );
  621. // const handleQrCode = useCallback(
  622. // (id: GridRowId, params: any) => () => {
  623. // setRowModesModel((prev) => ({
  624. // ...prev,
  625. // [id]: { mode: GridRowModes.View },
  626. // }));
  627. // setModalInfo(params.row);
  628. // setTimeout(() => {
  629. // // open stock in modal
  630. // // openPutAwayModal();
  631. // // return the record with its status as pending
  632. // // update layout
  633. // console.log("delayed");
  634. // printQrcode(params.row);
  635. // }, 200);
  636. // },
  637. // [printQrcode],
  638. // );
  639. const columns = useMemo<GridColDef[]>(() => {
  640. const baseColumns: GridColDef[] = [
  641. {
  642. field: "dnNo",
  643. headerName: t("dnNo"),
  644. width: 100,
  645. minWidth: 100,
  646. },
  647. {
  648. field: "receiptDate",
  649. headerName: t("receiptDate"),
  650. width: 125,
  651. renderCell: (params) => arrayToDateString(params.value),
  652. },
  653. {
  654. field: "productLotNo",
  655. headerName: t("productLotNo"),
  656. width: 110,
  657. minWidth: 110,
  658. },
  659. {
  660. field: "purchaseAcceptedQty",
  661. headerName: t("acceptedQty"),
  662. width: PURCHASE_QTY_COLUMN_WIDTH,
  663. minWidth: PURCHASE_QTY_COLUMN_WIDTH,
  664. flex: 0,
  665. align: "right",
  666. headerAlign: "right",
  667. type: "number",
  668. renderCell: (params) => {
  669. const qty = params.row.purchaseAcceptedQty ?? 0;
  670. return formatQtyWithPurchaseUom(qty, itemDetail.uom);
  671. },
  672. },
  673. {
  674. field: "uom",
  675. headerName: t("uom"),
  676. width: UOM_COLUMN_WIDTH,
  677. minWidth: UOM_COLUMN_WIDTH,
  678. flex: 0,
  679. renderCell: () => {
  680. const text = itemDetail.uom?.udfudesc ?? "-";
  681. return (
  682. <Box
  683. sx={{
  684. overflow: "hidden",
  685. textOverflow: "ellipsis",
  686. whiteSpace: "nowrap",
  687. width: "100%",
  688. }}
  689. title={text}
  690. >
  691. {text}
  692. </Box>
  693. );
  694. },
  695. },
  696. {
  697. field: "stockQty",
  698. headerName: t("Stock In Qty"),
  699. width: STOCK_QTY_COLUMN_WIDTH,
  700. minWidth: STOCK_QTY_COLUMN_WIDTH,
  701. flex: 0,
  702. type: "number",
  703. align: "left",
  704. headerAlign: "left",
  705. renderCell: (params) => {
  706. const stockQty = Number(params.row.acceptedQty ?? 0);
  707. return decimalFormatter.format(stockQty);
  708. },
  709. },
  710. {
  711. field: "stockUom",
  712. headerName: t("Stock UoM"),
  713. width: UOM_COLUMN_WIDTH,
  714. minWidth: UOM_COLUMN_WIDTH,
  715. flex: 0,
  716. renderCell: () => {
  717. const text = itemDetail.stockUom.stockUomDesc ?? "-";
  718. return (
  719. <Box
  720. sx={{
  721. overflow: "hidden",
  722. textOverflow: "ellipsis",
  723. whiteSpace: "nowrap",
  724. width: "100%",
  725. }}
  726. title={text}
  727. >
  728. {text}
  729. </Box>
  730. );
  731. },
  732. },
  733. {
  734. field: "status",
  735. headerName: t("Status"),
  736. width: 88,
  737. renderCell: (params) => {
  738. const status = params.row.status;
  739. return (
  740. <span
  741. style={{
  742. color:
  743. status == "escalated"
  744. ? "red"
  745. : status == "rejected" || status == "partially_completed"
  746. ? "orange"
  747. : "inherit",
  748. }}
  749. >
  750. {t(`${params.row.status}`)}
  751. </span>
  752. );
  753. },
  754. },
  755. {
  756. field: "actions",
  757. headerName: "操作",
  758. width: ACTIONS_COLUMN_WIDTH,
  759. minWidth: ACTIONS_COLUMN_WIDTH,
  760. flex: 0,
  761. sortable: false,
  762. filterable: false,
  763. disableColumnMenu: true,
  764. cellClassName: "actions",
  765. renderCell: (params) => {
  766. const data = params.row as StockInLineRow;
  767. const btnSx = getButtonSx(data);
  768. const status = (data.status ?? "").toLowerCase();
  769. const canEmail =
  770. status === "rejected" || status === "partially_completed";
  771. const canPrint = status === "received";
  772. const canDelete = canDeleteStockInLine(data);
  773. const needsStockQtyRound = needsPoQcStockQtyRound(
  774. data.purchaseOrderLineId,
  775. data.status,
  776. data.acceptedQty,
  777. );
  778. return (
  779. <StockInLineRowActions
  780. btnSx={btnSx}
  781. onPrimaryClick={() => {
  782. void handleNewQC(params.row.id, params)();
  783. }}
  784. canEmail={canEmail}
  785. canPrint={canPrint}
  786. canDelete={canDelete}
  787. onEmail={() =>
  788. handleMailTemplateForStockInLine(params.row.id as number)
  789. }
  790. onPrint={() => printQrcode(params.row)}
  791. onDelete={() => handleSoftDelete(data)}
  792. btnIsLoading={btnIsLoading}
  793. isDeleting={isDeleting}
  794. needsStockQtyRound={needsStockQtyRound}
  795. stockQty={Number(data.acceptedQty ?? 0)}
  796. isRounding={roundingSilId === data.id}
  797. onRound={(mode) => handleRoundStockQty(data, mode)}
  798. />
  799. );
  800. },
  801. },
  802. ];
  803. return baseColumns.map((col) => {
  804. const grow = COLUMN_GROW[col.field] ?? 0;
  805. if (grow > 0) {
  806. return {
  807. ...col,
  808. flex: grow,
  809. minWidth: col.minWidth ?? col.width,
  810. width: undefined,
  811. };
  812. }
  813. return {
  814. ...col,
  815. flex: 0,
  816. width: col.width,
  817. minWidth: col.minWidth ?? col.width,
  818. };
  819. });
  820. }, [
  821. t,
  822. itemDetail,
  823. handleNewQC,
  824. handleMailTemplateForStockInLine,
  825. printQrcode,
  826. handleSoftDelete,
  827. handleRoundStockQty,
  828. roundingSilId,
  829. btnIsLoading,
  830. isDeleting,
  831. sessionToken?.id,
  832. ]);
  833. const unsortableColumns = useMemo(() =>
  834. columns.map(column => ({ ...column, sortable: false }))
  835. , [columns]);
  836. const addRow = useCallback(() => {
  837. console.log(itemDetail);
  838. const newEntry = {
  839. id: Date.now(),
  840. _isNew: true,
  841. itemId: itemDetail.itemId,
  842. purchaseOrderId: itemDetail.purchaseOrderId,
  843. purchaseOrderLineId: itemDetail.id,
  844. itemNo: itemDetail.itemNo,
  845. itemName: itemDetail.itemName,
  846. // User inputs qty in M18 unit; backend will convert to stock unit on create.
  847. purchaseAcceptedQty: itemDetail.qty - currQty,
  848. uom: itemDetail.uom,
  849. status: "draft",
  850. };
  851. setEntries((e) => [...e, newEntry]);
  852. setRowModesModel((model) => ({
  853. ...model,
  854. [getRowId(newEntry)]: {
  855. mode: GridRowModes.Edit,
  856. // fieldToFocus: "projectId",
  857. },
  858. }));
  859. }, [currQty, getRowId, itemDetail]);
  860. const validation = useCallback(
  861. (
  862. newRow: GridRowModel<StockInLineRow>,
  863. // rowModel: GridRowSelectionModel
  864. ): StockInLineEntryError | undefined => {
  865. const error: StockInLineEntryError = {};
  866. console.log(newRow);
  867. console.log(currQty);
  868. if (
  869. newRow.purchaseAcceptedQty &&
  870. newRow.purchaseAcceptedQty > itemDetail.qty
  871. ) {
  872. error["purchaseAcceptedQty"] = t(
  873. "qty cannot be greater than remaining qty",
  874. );
  875. }
  876. return Object.keys(error).length > 0 ? error : undefined;
  877. },
  878. [currQty, itemDetail.qty, t],
  879. );
  880. const processRowUpdate = useCallback(
  881. (
  882. newRow: GridRowModel<StockInLineRow>,
  883. originalRow: GridRowModel<StockInLineRow>,
  884. ) => {
  885. const errors = validation(newRow); // change to validation
  886. if (errors) {
  887. throw new ProcessRowUpdateError(
  888. originalRow,
  889. "validation error",
  890. errors,
  891. );
  892. }
  893. const { _isNew, _error, ...updatedRow } = newRow;
  894. const rowToSave = {
  895. ...updatedRow,
  896. } satisfies StockInLineRow;
  897. const newEntries = entries.map((e) =>
  898. getRowId(e) === getRowId(originalRow) ? rowToSave : e,
  899. );
  900. setStockInLine(newEntries as StockInLine[]);
  901. console.log("triggered");
  902. setEntries(newEntries);
  903. //update remaining qty
  904. const total = newEntries.reduce(
  905. (acc, curr) => acc + (curr.purchaseAcceptedQty || 0),
  906. 0,
  907. );
  908. setCurrQty(total);
  909. return rowToSave;
  910. },
  911. [validation, entries, setStockInLine, getRowId],
  912. );
  913. const onProcessRowUpdateError = useCallback(
  914. (updateError: ProcessRowUpdateError) => {
  915. const errors = updateError.errors;
  916. const oldRow = updateError.row;
  917. apiRef.current.updateRows([{ ...oldRow, _error: errors }]);
  918. },
  919. [apiRef],
  920. );
  921. const footer = (
  922. <>
  923. {/* <Box display="flex" gap={2} alignItems="center">
  924. <Button
  925. disableRipple
  926. variant="outlined"
  927. startIcon={<Add />}
  928. disabled={itemDetail.qty - currQty <= 0}
  929. onClick={addRow}
  930. size="small"
  931. >
  932. {t("Record pol")}
  933. </Button>
  934. </Box> */}
  935. </>
  936. );
  937. const getRowHeight = useCallback(() => STOCK_IN_ROW_HEIGHT, []);
  938. return (
  939. <>
  940. <Box
  941. sx={{
  942. width: "100%",
  943. maxWidth: "100%",
  944. minWidth: 0,
  945. overflowX: "auto",
  946. WebkitOverflowScrolling: "touch",
  947. pb: 1,
  948. }}
  949. >
  950. <StyledDataGrid
  951. getRowId={getRowId}
  952. apiRef={apiRef}
  953. autoHeight
  954. getRowHeight={getRowHeight}
  955. columnVisibilityModel={columnVisibilityModel}
  956. sx={{
  957. width: "100%",
  958. minWidth: 0,
  959. "--DataGrid-overlayHeight": "100px",
  960. ".MuiDataGrid-row .MuiDataGrid-cell.hasError": {
  961. border: "1px solid",
  962. borderColor: "error.main",
  963. },
  964. ".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": {
  965. border: "1px solid",
  966. borderColor: "warning.main",
  967. },
  968. "& .MuiDataGrid-cell.actions": {
  969. overflow: "visible",
  970. alignItems: "center",
  971. py: 0.5,
  972. lineHeight: "normal",
  973. },
  974. "& .MuiDataGrid-cell[data-field='stockQty']": {
  975. overflow: "visible",
  976. alignItems: "center",
  977. justifyContent: "flex-start",
  978. py: 0.5,
  979. px: 0.75,
  980. },
  981. "& .MuiDataGrid-cell[data-field='actions']": {
  982. py: 0.5,
  983. px: 1,
  984. },
  985. "& .MuiDataGrid-columnHeader[data-field='purchaseAcceptedQty']": {
  986. whiteSpace: "normal",
  987. lineHeight: 1.2,
  988. px: 0.5,
  989. },
  990. "& .MuiDataGrid-cell[data-field='purchaseAcceptedQty']": {
  991. px: 0.5,
  992. },
  993. "& .MuiDataGrid-columnHeader[data-field='uom']": {
  994. whiteSpace: "nowrap",
  995. px: 0.75,
  996. },
  997. "& .MuiDataGrid-cell[data-field='uom']": {
  998. px: 0.75,
  999. },
  1000. "& .MuiDataGrid-columnHeader[data-field='stockQty']": {
  1001. px: 0.75,
  1002. },
  1003. ...Object.fromEntries(
  1004. COMPACT_STOCK_IN_CELL_FIELDS.flatMap((field) => [
  1005. [
  1006. `& .MuiDataGrid-cell[data-field="${field}"]`,
  1007. { px: 1 },
  1008. ],
  1009. [
  1010. `& .MuiDataGrid-columnHeader[data-field="${field}"]`,
  1011. { px: 1 },
  1012. ],
  1013. ]),
  1014. ),
  1015. }}
  1016. disableColumnMenu
  1017. editMode="row"
  1018. rows={entries}
  1019. rowModesModel={rowModesModel}
  1020. onRowModesModelChange={setRowModesModel}
  1021. processRowUpdate={processRowUpdate}
  1022. onProcessRowUpdateError={onProcessRowUpdateError}
  1023. columns={unsortableColumns}
  1024. isCellEditable={(params) => {
  1025. const status = params.row.status.toLowerCase();
  1026. return (
  1027. stockInLineStatusMap[status] >= 0 ||
  1028. stockInLineStatusMap[status] <= 1
  1029. );
  1030. }}
  1031. getCellClassName={(params: GridCellParams<StockInLineRow>) => {
  1032. let classname = "";
  1033. if (params.row._error) {
  1034. classname = "hasError";
  1035. }
  1036. return classname;
  1037. }}
  1038. slots={{
  1039. footer: FooterToolbar,
  1040. noRowsOverlay: NoRowsOverlay,
  1041. }}
  1042. slotProps={{
  1043. footer: { child: footer },
  1044. }}
  1045. />
  1046. </Box>
  1047. {/* {modalInfo !== undefined && ( */}
  1048. <>
  1049. <QcStockInModal
  1050. session={sessionToken}
  1051. open={newOpen}
  1052. onClose={closeNewModal}
  1053. // itemDetail={modalInfo}
  1054. inputDetail={modalInfo}
  1055. warehouse={warehouse}
  1056. printerCombo={printerCombo}
  1057. printSource="stockIn"
  1058. />
  1059. </>
  1060. {/* )
  1061. } */}
  1062. </>
  1063. );
  1064. }
  1065. const NoRowsOverlay: React.FC = () => {
  1066. const { t } = useTranslation("purchaseOrder");
  1067. return (
  1068. <Box
  1069. display="flex"
  1070. justifyContent="center"
  1071. alignItems="center"
  1072. height="100%"
  1073. >
  1074. <Typography variant="caption">{t("Add some entries!")}</Typography>
  1075. </Box>
  1076. );
  1077. };
  1078. const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => {
  1079. return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>;
  1080. };
  1081. export default PoInputGrid;