FPSMS-frontend
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 

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