FPSMS-frontend
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 

695 rader
20 KiB

  1. "use client";
  2. import {
  3. FooterPropsOverrides,
  4. GridActionsCellItem,
  5. GridCellParams,
  6. GridRowId,
  7. GridRowIdGetter,
  8. GridRowModel,
  9. GridRowModes,
  10. GridRowModesModel,
  11. GridToolbarContainer,
  12. useGridApiRef,
  13. } from "@mui/x-data-grid";
  14. import {
  15. Dispatch,
  16. MutableRefObject,
  17. SetStateAction,
  18. useCallback,
  19. useEffect,
  20. useMemo,
  21. useState,
  22. } from "react";
  23. import StyledDataGrid from "../StyledDataGrid";
  24. import { GridColDef } from "@mui/x-data-grid";
  25. import { Box, Button, Grid, Typography } from "@mui/material";
  26. import { useTranslation } from "react-i18next";
  27. import { Add } from "@mui/icons-material";
  28. import SaveIcon from "@mui/icons-material/Save";
  29. import DeleteIcon from "@mui/icons-material/Delete";
  30. import CancelIcon from "@mui/icons-material/Cancel";
  31. import FactCheckIcon from "@mui/icons-material/FactCheck";
  32. import ShoppingCartIcon from "@mui/icons-material/ShoppingCart";
  33. import { QcItemWithChecks } from "src/app/api/qc";
  34. import PlayArrowIcon from "@mui/icons-material/PlayArrow";
  35. import { PurchaseOrderLine, StockInLine } from "@/app/api/po";
  36. import {
  37. createStockInLine,
  38. PurchaseQcResult,
  39. } from "@/app/api/po/actions";
  40. import { useSearchParams } from "next/navigation";
  41. import {
  42. returnWeightUnit,
  43. calculateWeight,
  44. stockInLineStatusMap,
  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. interface ResultWithId {
  61. id: number;
  62. }
  63. interface Props {
  64. qc: QcItemWithChecks[];
  65. setRows: Dispatch<SetStateAction<PurchaseOrderLine[]>>;
  66. setProcessedQty: Dispatch<SetStateAction<number>>;
  67. itemDetail: PurchaseOrderLine;
  68. stockInLine: StockInLine[];
  69. warehouse: WarehouseResult[];
  70. }
  71. export type StockInLineEntryError = {
  72. [field in keyof StockInLine]?: string;
  73. };
  74. export type StockInLineRow = Partial<
  75. StockInLine & {
  76. isActive: boolean | undefined;
  77. _isNew: boolean;
  78. _error: StockInLineEntryError;
  79. } & ResultWithId
  80. >;
  81. class ProcessRowUpdateError extends Error {
  82. public readonly row: StockInLineRow;
  83. public readonly errors: StockInLineEntryError | undefined;
  84. constructor(
  85. row: StockInLineRow,
  86. message?: string,
  87. errors?: StockInLineEntryError
  88. ) {
  89. super(message);
  90. this.row = row;
  91. this.errors = errors;
  92. Object.setPrototypeOf(this, ProcessRowUpdateError.prototype);
  93. }
  94. }
  95. function PoInputGrid({
  96. qc,
  97. setRows,
  98. setProcessedQty,
  99. itemDetail,
  100. stockInLine,
  101. warehouse,
  102. }: Props) {
  103. console.log(itemDetail);
  104. const { t } = useTranslation("home");
  105. const apiRef = useGridApiRef();
  106. const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({});
  107. const getRowId = useCallback<GridRowIdGetter<StockInLineRow>>(
  108. (row) => row.id as number,
  109. []
  110. );
  111. console.log(stockInLine);
  112. const [entries, setEntries] = useState<StockInLineRow[]>(stockInLine || []);
  113. const [modalInfo, setModalInfo] = useState<StockInLine & {qcResult?: PurchaseQcResult[]}>();
  114. const [qcOpen, setQcOpen] = useState(false);
  115. const [escalOpen, setEscalOpen] = useState(false);
  116. const [stockInOpen, setStockInOpen] = useState(false);
  117. const [putAwayOpen, setPutAwayOpen] = useState(false);
  118. const [currQty, setCurrQty] = useState(() => {
  119. const total = entries.reduce(
  120. (acc, curr) => acc + (curr.acceptedQty || 0),
  121. 0
  122. );
  123. return total;
  124. });
  125. useEffect(() => {
  126. const completedList = entries.filter((e) => e.status === "completed");
  127. const processedQty = completedList.reduce(
  128. (acc, curr) => acc + (curr.acceptedQty || 0),
  129. 0
  130. );
  131. setProcessedQty(processedQty);
  132. }, [entries]);
  133. const handleDelete = useCallback(
  134. (id: GridRowId) => () => {
  135. setEntries((es) => es.filter((e) => getRowId(e) !== id));
  136. },
  137. [getRowId]
  138. );
  139. const handleStart = useCallback(
  140. (id: GridRowId, params: any) => () => {
  141. setRowModesModel((prev) => ({
  142. ...prev,
  143. [id]: { mode: GridRowModes.View },
  144. }));
  145. setTimeout(async () => {
  146. // post stock in line
  147. console.log("delayed");
  148. console.log(params);
  149. const oldId = params.row.id;
  150. console.log(oldId);
  151. console.log(params.row);
  152. const postData = {
  153. itemId: params.row.itemId,
  154. itemNo: params.row.itemNo,
  155. itemName: params.row.itemName,
  156. purchaseOrderId: params.row.purchaseOrderId,
  157. purchaseOrderLineId: params.row.purchaseOrderLineId,
  158. acceptedQty: params.row.acceptedQty,
  159. };
  160. const res = await createStockInLine(postData);
  161. console.log(res);
  162. setEntries((prev) =>
  163. prev.map((p) => (p.id === oldId ? (res.entity as StockInLine) : p))
  164. );
  165. // do post directly to test
  166. // openStartModal();
  167. }, 200);
  168. },
  169. [createStockInLine]
  170. );
  171. const fetchQcDefaultValue = useCallback(async () => {
  172. // const authHeader = axiosInstance.defaults.headers['Authorization'];
  173. // if (!authHeader) {
  174. // return; // Exit the function if the token is not set
  175. // }
  176. // console.log(authHeader)
  177. // console.log(NEXT_PUBLIC_API_URL)
  178. // const res = await axiosInstance.get<QcItemWithChecks[]>(`${NEXT_PUBLIC_API_URL}/qcResult/${itemDetail.id}`)
  179. // const res = await testFetch2(itemDetail.id)
  180. const res = await fetchQcResult(itemDetail.id);
  181. console.log(res);
  182. return res
  183. }, [axiosInstance]);
  184. const handleQC = useCallback(
  185. (id: GridRowId, params: any) => async () => {
  186. setRowModesModel((prev) => ({
  187. ...prev,
  188. [id]: { mode: GridRowModes.View },
  189. }));
  190. const qcResult = await fetchQcDefaultValue();
  191. console.log(qcResult)
  192. setModalInfo({
  193. ...params.row,
  194. qcResult: qcResult
  195. });
  196. // set default values
  197. setTimeout(() => {
  198. // open qc modal
  199. console.log("delayed");
  200. openQcModal();
  201. }, 200);
  202. },
  203. [fetchQcDefaultValue]
  204. );
  205. const handleEscalation = useCallback(
  206. (id: GridRowId, params: any) => () => {
  207. setRowModesModel((prev) => ({
  208. ...prev,
  209. [id]: { mode: GridRowModes.View },
  210. }));
  211. setModalInfo(params.row);
  212. setTimeout(() => {
  213. // open qc modal
  214. console.log("delayed");
  215. openEscalationModal();
  216. }, 200);
  217. },
  218. []
  219. );
  220. const handleStockIn = useCallback(
  221. (id: GridRowId, params: any) => () => {
  222. setRowModesModel((prev) => ({
  223. ...prev,
  224. [id]: { mode: GridRowModes.View },
  225. }));
  226. setModalInfo(params.row);
  227. setTimeout(() => {
  228. // open stock in modal
  229. openStockInModal();
  230. // return the record with its status as pending
  231. // update layout
  232. console.log("delayed");
  233. }, 200);
  234. },
  235. []
  236. );
  237. const handlePutAway = useCallback(
  238. (id: GridRowId, params: any) => () => {
  239. setRowModesModel((prev) => ({
  240. ...prev,
  241. [id]: { mode: GridRowModes.View },
  242. }));
  243. setModalInfo(params.row);
  244. setTimeout(() => {
  245. // open stock in modal
  246. openPutAwayModal();
  247. // return the record with its status as pending
  248. // update layout
  249. console.log("delayed");
  250. }, 200);
  251. },
  252. []
  253. );
  254. const printQrcode = useCallback(
  255. async (row: any) => {
  256. console.log(row.id);
  257. const postData = { stockInLineIds: [row.id] };
  258. // const postData = { stockInLineIds: [42,43,44] };
  259. const response = await fetchPoQrcode(postData);
  260. if (response) {
  261. console.log(response);
  262. downloadFile(new Uint8Array(response.blobValue), response.filename!!);
  263. }
  264. },
  265. [fetchPoQrcode, downloadFile]
  266. );
  267. const handleQrCode = useCallback(
  268. (id: GridRowId, params: any) => () => {
  269. setRowModesModel((prev) => ({
  270. ...prev,
  271. [id]: { mode: GridRowModes.View },
  272. }));
  273. setModalInfo(params.row);
  274. setTimeout(() => {
  275. // open stock in modal
  276. // openPutAwayModal();
  277. // return the record with its status as pending
  278. // update layout
  279. console.log("delayed");
  280. printQrcode(params.row);
  281. }, 200);
  282. },
  283. []
  284. );
  285. const closeQcModal = useCallback(() => {
  286. setQcOpen(false);
  287. }, []);
  288. const openQcModal = useCallback(() => {
  289. setQcOpen(true);
  290. }, []);
  291. const closeStockInModal = useCallback(() => {
  292. setStockInOpen(false);
  293. }, []);
  294. const openStockInModal = useCallback(() => {
  295. setStockInOpen(true);
  296. }, []);
  297. const closePutAwayModal = useCallback(() => {
  298. setPutAwayOpen(false);
  299. }, []);
  300. const openPutAwayModal = useCallback(() => {
  301. setPutAwayOpen(true);
  302. }, []);
  303. const closeEscalationModal = useCallback(() => {
  304. setEscalOpen(false);
  305. }, []);
  306. const openEscalationModal = useCallback(() => {
  307. setEscalOpen(true);
  308. }, []);
  309. const columns = useMemo<GridColDef[]>(
  310. () => [
  311. {
  312. field: "itemNo",
  313. flex: 0.8,
  314. },
  315. {
  316. field: "itemName",
  317. flex: 1,
  318. },
  319. {
  320. field: "acceptedQty",
  321. headerName: "qty",
  322. flex: 0.5,
  323. type: "number",
  324. editable: true,
  325. // replace with tooltip + content
  326. },
  327. {
  328. field: "uom",
  329. headerName: "uom",
  330. flex: 0.5,
  331. renderCell: (params) => {
  332. return params.row.uom.code;
  333. },
  334. },
  335. {
  336. field: "weight",
  337. headerName: "weight",
  338. flex: 0.5,
  339. renderCell: (params) => {
  340. const weight = calculateWeight(
  341. params.row.acceptedQty,
  342. params.row.uom
  343. );
  344. const weightUnit = returnWeightUnit(params.row.uom);
  345. return `${weight} ${weightUnit}`;
  346. },
  347. },
  348. {
  349. field: "status",
  350. flex: 0.5,
  351. // editable: true,
  352. },
  353. {
  354. field: "actions",
  355. type: "actions",
  356. headerName: "start | qc | escalation | stock in | putaway | delete",
  357. flex: 1,
  358. cellClassName: "actions",
  359. getActions: (params) => {
  360. console.log(params.row.status);
  361. const status = params.row.status.toLowerCase();
  362. return [
  363. <GridActionsCellItem
  364. icon={<PlayArrowIcon />}
  365. label="start"
  366. sx={{
  367. color: "primary.main",
  368. // marginRight: 1,
  369. }}
  370. disabled={!(stockInLineStatusMap[status] === 0)}
  371. // set _isNew to false after posting
  372. // or check status
  373. onClick={handleStart(params.row.id, params)}
  374. color="inherit"
  375. key="edit"
  376. />,
  377. <GridActionsCellItem
  378. icon={<FactCheckIcon />}
  379. label="qc"
  380. sx={{
  381. color: "primary.main",
  382. // marginRight: 1,
  383. }}
  384. disabled={stockInLineStatusMap[status] < 1}
  385. // set _isNew to false after posting
  386. // or check status
  387. onClick={handleQC(params.row.id, params)}
  388. color="inherit"
  389. key="edit"
  390. />,
  391. <GridActionsCellItem
  392. icon={<NotificationImportantIcon />}
  393. label="escalation"
  394. sx={{
  395. color: "primary.main",
  396. // marginRight: 1,
  397. }}
  398. disabled={
  399. stockInLineStatusMap[status] <= 0 ||
  400. stockInLineStatusMap[status] >= 5
  401. }
  402. // set _isNew to false after posting
  403. // or check status
  404. onClick={handleEscalation(params.row.id, params)}
  405. color="inherit"
  406. key="edit"
  407. />,
  408. <GridActionsCellItem
  409. icon={<ShoppingCartIcon />}
  410. label="stockin"
  411. sx={{
  412. color: "primary.main",
  413. // marginRight: 1,
  414. }}
  415. disabled={stockInLineStatusMap[status] !== 6}
  416. // set _isNew to false after posting
  417. // or check status
  418. onClick={handleStockIn(params.row.id, params)}
  419. color="inherit"
  420. key="edit"
  421. />,
  422. <GridActionsCellItem
  423. icon={<ShoppingCartIcon />}
  424. label="putaway"
  425. sx={{
  426. color: "primary.main",
  427. // marginRight: 1,
  428. }}
  429. disabled={stockInLineStatusMap[status] < 7}
  430. // set _isNew to false after posting
  431. // or check status
  432. onClick={handlePutAway(params.row.id, params)}
  433. color="inherit"
  434. key="edit"
  435. />,
  436. <GridActionsCellItem
  437. icon={<QrCodeIcon />}
  438. label="putaway"
  439. sx={{
  440. color: "primary.main",
  441. // marginRight: 1,
  442. }}
  443. disabled={stockInLineStatusMap[status] !== 8}
  444. // set _isNew to false after posting
  445. // or check status
  446. onClick={handleQrCode(params.row.id, params)}
  447. color="inherit"
  448. key="edit"
  449. />,
  450. <GridActionsCellItem
  451. icon={<DeleteIcon />}
  452. label="Delete"
  453. sx={{
  454. color: "error.main",
  455. }}
  456. disabled={stockInLineStatusMap[status] !== 0}
  457. // disabled={Boolean(params.row.status)}
  458. onClick={handleDelete(params.row.id)}
  459. color="inherit"
  460. key="edit"
  461. />,
  462. ];
  463. },
  464. },
  465. ],
  466. []
  467. );
  468. const addRow = useCallback(() => {
  469. console.log(itemDetail);
  470. const newEntry = {
  471. id: Date.now(),
  472. _isNew: true,
  473. itemId: itemDetail.itemId,
  474. purchaseOrderId: itemDetail.purchaseOrderId,
  475. purchaseOrderLineId: itemDetail.id,
  476. itemNo: itemDetail.itemNo,
  477. itemName: itemDetail.itemName,
  478. acceptedQty: itemDetail.qty - currQty, // this bug
  479. uom: itemDetail.uom,
  480. status: "draft",
  481. };
  482. setEntries((e) => [...e, newEntry]);
  483. setRowModesModel((model) => ({
  484. ...model,
  485. [getRowId(newEntry)]: {
  486. mode: GridRowModes.Edit,
  487. // fieldToFocus: "projectId",
  488. },
  489. }));
  490. }, [currQty, getRowId]);
  491. const validation = useCallback(
  492. (
  493. newRow: GridRowModel<StockInLineRow>
  494. // rowModel: GridRowSelectionModel
  495. ): StockInLineEntryError | undefined => {
  496. const error: StockInLineEntryError = {};
  497. console.log(newRow);
  498. console.log(currQty);
  499. if (newRow.acceptedQty && newRow.acceptedQty > itemDetail.qty) {
  500. error["acceptedQty"] = "qty cannot be greater than remaining qty";
  501. }
  502. return Object.keys(error).length > 0 ? error : undefined;
  503. },
  504. [currQty]
  505. );
  506. const processRowUpdate = useCallback(
  507. (
  508. newRow: GridRowModel<StockInLineRow>,
  509. originalRow: GridRowModel<StockInLineRow>
  510. ) => {
  511. const errors = validation(newRow); // change to validation
  512. if (errors) {
  513. throw new ProcessRowUpdateError(
  514. originalRow,
  515. "validation error",
  516. errors
  517. );
  518. }
  519. const { _isNew, _error, ...updatedRow } = newRow;
  520. const rowToSave = {
  521. ...updatedRow,
  522. } satisfies StockInLineRow;
  523. const newEntries = entries.map((e) =>
  524. getRowId(e) === getRowId(originalRow) ? rowToSave : e
  525. );
  526. setEntries(newEntries);
  527. //update remaining qty
  528. const total = newEntries.reduce(
  529. (acc, curr) => acc + (curr.acceptedQty || 0),
  530. 0
  531. );
  532. setCurrQty(total);
  533. return rowToSave;
  534. },
  535. [getRowId, entries]
  536. );
  537. const onProcessRowUpdateError = useCallback(
  538. (updateError: ProcessRowUpdateError) => {
  539. const errors = updateError.errors;
  540. const oldRow = updateError.row;
  541. apiRef.current.updateRows([{ ...oldRow, _error: errors }]);
  542. },
  543. [apiRef]
  544. );
  545. // useEffect(() => {
  546. // const total = entries.reduce(
  547. // (acc, curr) => acc + (curr.acceptedQty || 0),
  548. // 0
  549. // );
  550. // setDefaultQty(itemDetail.qty - total);
  551. // }, [entries]);
  552. const footer = (
  553. <Box display="flex" gap={2} alignItems="center">
  554. <Button
  555. disableRipple
  556. variant="outlined"
  557. startIcon={<Add />}
  558. disabled={itemDetail.qty - currQty <= 0}
  559. onClick={addRow}
  560. size="small"
  561. >
  562. {t("Record pol")}
  563. </Button>
  564. </Box>
  565. );
  566. return (
  567. <>
  568. <StyledDataGrid
  569. getRowId={getRowId}
  570. apiRef={apiRef}
  571. autoHeight
  572. sx={{
  573. "--DataGrid-overlayHeight": "100px",
  574. ".MuiDataGrid-row .MuiDataGrid-cell.hasError": {
  575. border: "1px solid",
  576. borderColor: "error.main",
  577. },
  578. ".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": {
  579. border: "1px solid",
  580. borderColor: "warning.main",
  581. },
  582. }}
  583. disableColumnMenu
  584. editMode="row"
  585. rows={entries}
  586. rowModesModel={rowModesModel}
  587. onRowModesModelChange={setRowModesModel}
  588. processRowUpdate={processRowUpdate}
  589. onProcessRowUpdateError={onProcessRowUpdateError}
  590. columns={columns}
  591. isCellEditable={(params) => {
  592. const status = params.row.status.toLowerCase();
  593. return (
  594. stockInLineStatusMap[status] >= 0 ||
  595. stockInLineStatusMap[status] <= 1
  596. );
  597. }}
  598. getCellClassName={(params: GridCellParams<StockInLineRow>) => {
  599. let classname = "";
  600. if (params.row._error) {
  601. classname = "hasError";
  602. }
  603. return classname;
  604. }}
  605. slots={{
  606. footer: FooterToolbar,
  607. noRowsOverlay: NoRowsOverlay,
  608. }}
  609. slotProps={{
  610. footer: { child: footer },
  611. }}
  612. />
  613. <>
  614. <PoQcStockInModal
  615. type={"qc"}
  616. setEntries={setEntries}
  617. qc={qc}
  618. open={qcOpen}
  619. onClose={closeQcModal}
  620. itemDetail={modalInfo!!}
  621. />
  622. </>
  623. <>
  624. <PoQcStockInModal
  625. type={"escalation"}
  626. setEntries={setEntries}
  627. // qc={qc}
  628. open={escalOpen}
  629. onClose={closeEscalationModal}
  630. itemDetail={modalInfo!!}
  631. />
  632. </>
  633. <>
  634. <PoQcStockInModal
  635. type={"stockIn"}
  636. setEntries={setEntries}
  637. // qc={qc}
  638. open={stockInOpen}
  639. onClose={closeStockInModal}
  640. itemDetail={modalInfo!!}
  641. />
  642. </>
  643. <>
  644. <PoQcStockInModal
  645. type={"putaway"}
  646. setEntries={setEntries}
  647. open={putAwayOpen}
  648. warehouse={warehouse}
  649. onClose={closePutAwayModal}
  650. itemDetail={modalInfo!!}
  651. />
  652. </>
  653. </>
  654. );
  655. }
  656. const NoRowsOverlay: React.FC = () => {
  657. const { t } = useTranslation("home");
  658. return (
  659. <Box
  660. display="flex"
  661. justifyContent="center"
  662. alignItems="center"
  663. height="100%"
  664. >
  665. <Typography variant="caption">{t("Add some entries!")}</Typography>
  666. </Box>
  667. );
  668. };
  669. const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => {
  670. return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>;
  671. };
  672. export default PoInputGrid;