From 6b3f7d024e3c31dfe178c7f2d315b325ddc459d5 Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Tue, 13 May 2025 17:23:50 +0800 Subject: [PATCH] partial po flow + rough schedule collapse --- src/app/(main)/po/edit/page.tsx | 32 ++ src/app/(main)/po/page.tsx | 48 ++ src/app/api/po/actions.ts | 65 +++ src/app/api/po/index.ts | 55 +++ src/app/api/qc/index.ts | 12 + src/app/utils/formatUtil.ts | 11 + .../InputDataGrid/InputDataGrid.tsx | 1 - .../NavigationContent/NavigationContent.tsx | 2 +- src/components/PoDetail/PoDetail.tsx | 154 +++++++ src/components/PoDetail/PoDetailLoading.tsx | 40 ++ src/components/PoDetail/PoDetailWrapper.tsx | 55 +++ src/components/PoDetail/PoInfoCard.tsx | 32 ++ src/components/PoDetail/PoInputGrid.tsx | 435 ++++++++++++++++++ src/components/PoDetail/PoQcModal.tsx | 141 ++++++ src/components/PoDetail/QcForm.tsx | 215 +++++++++ src/components/PoDetail/QcSelect.tsx | 78 ++++ src/components/PoDetail/TwoLineCell.tsx | 24 + src/components/PoDetail/index.ts | 1 + src/components/PoSearch/PoSearch.tsx | 93 ++++ src/components/PoSearch/PoSearchLoading.tsx | 40 ++ src/components/PoSearch/PoSearchWrapper.tsx | 35 ++ src/components/PoSearch/index.ts | 1 + src/components/QcItemSave/QcItemDetails.tsx | 113 ++--- .../RoughScheduleDetail/ViewByFGDetails.tsx | 69 ++- .../SearchResults/EditableSearchResults.tsx | 201 +++++--- .../SearchResults/SearchResults.tsx | 4 +- .../SearchResults/TempInputGridForMockUp.tsx | 419 +++++++++++++++++ 27 files changed, 2228 insertions(+), 148 deletions(-) create mode 100644 src/app/(main)/po/edit/page.tsx create mode 100644 src/app/(main)/po/page.tsx create mode 100644 src/app/api/po/actions.ts create mode 100644 src/app/api/po/index.ts create mode 100644 src/app/api/qc/index.ts create mode 100644 src/components/PoDetail/PoDetail.tsx create mode 100644 src/components/PoDetail/PoDetailLoading.tsx create mode 100644 src/components/PoDetail/PoDetailWrapper.tsx create mode 100644 src/components/PoDetail/PoInfoCard.tsx create mode 100644 src/components/PoDetail/PoInputGrid.tsx create mode 100644 src/components/PoDetail/PoQcModal.tsx create mode 100644 src/components/PoDetail/QcForm.tsx create mode 100644 src/components/PoDetail/QcSelect.tsx create mode 100644 src/components/PoDetail/TwoLineCell.tsx create mode 100644 src/components/PoDetail/index.ts create mode 100644 src/components/PoSearch/PoSearch.tsx create mode 100644 src/components/PoSearch/PoSearchLoading.tsx create mode 100644 src/components/PoSearch/PoSearchWrapper.tsx create mode 100644 src/components/PoSearch/index.ts create mode 100644 src/components/SearchResults/TempInputGridForMockUp.tsx diff --git a/src/app/(main)/po/edit/page.tsx b/src/app/(main)/po/edit/page.tsx new file mode 100644 index 0000000..7214ce1 --- /dev/null +++ b/src/app/(main)/po/edit/page.tsx @@ -0,0 +1,32 @@ +import { SearchParams } from "@/app/utils/fetchUtil"; +import { TypeEnum } from "@/app/utils/typeEnum"; +import CreateProductMaterial from "@/components/CreateItem"; +import PoDetail from "@/components/PoDetail"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import { Typography } from "@mui/material"; +import isString from "lodash/isString"; +import { notFound } from "next/navigation"; + +type Props = {} & SearchParams; + +const PoEdit: React.FC = async ({ searchParams }) => { + const type = "po"; + const { t } = await getServerI18n(type); + console.log(searchParams["id"]) + const id = isString(searchParams["id"]) + ? parseInt(searchParams["id"]) + : undefined; + console.log(id) + if (!id) { + notFound(); + } + return ( + <> + {/* {t("Create Material")} */} + + + + + ); +}; +export default PoEdit; diff --git a/src/app/(main)/po/page.tsx b/src/app/(main)/po/page.tsx new file mode 100644 index 0000000..21f4eb9 --- /dev/null +++ b/src/app/(main)/po/page.tsx @@ -0,0 +1,48 @@ +import { preloadClaims } from "@/app/api/claims"; +import ClaimSearch from "@/components/ClaimSearch"; +import PoSearch from "@/components/PoSearch"; +import { getServerI18n } from "@/i18n"; +import Add from "@mui/icons-material/Add"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { Metadata } from "next"; +import Link from "next/link"; +import { Suspense } from "react"; + +export const metadata: Metadata = { + title: "Purchase Order", +}; + +const production: React.FC = async () => { + const { t } = await getServerI18n("claims"); +// preloadClaims(); + + return ( + <> + + + {t("Purchase Order")} + + {/* */} + + }> + + + + ); +}; + +export default production; diff --git a/src/app/api/po/actions.ts b/src/app/api/po/actions.ts new file mode 100644 index 0000000..3a30825 --- /dev/null +++ b/src/app/api/po/actions.ts @@ -0,0 +1,65 @@ +"use server"; +import { BASE_API_URL } from "@/config/api"; +// import { ServerFetchError, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; +import { revalidateTag } from "next/cache"; +import { cache } from "react"; +import { PoResult, StockInLine } from "."; +import { serverFetchJson } from "@/app/utils/fetchUtil"; +// import { BASE_API_URL } from "@/config/api"; + +export interface PostStockInLiineResponse { + id: number | null; + name: string; + code: string; + message: string | null; + errorPosition: string | keyof T; + entity: StockInLine | StockInLine[] +} + + +export interface StockInLineEntry { + id?: number + itemId: number + purchaseOrderId: number + purchaseOrderLineId: number + acceptedQty: number + status?: string +} + +export interface PurchaseQcCheck { + qcCheckId: number; + qty: number; +} + +export interface PurchaseQCInput { + sampleRate: number; + sampleWeight: number; + totalWeight: number; + qcCheck: PurchaseQcCheck[]; +} + +export const testFetch = cache(async (id: number) => { + return serverFetchJson(`${BASE_API_URL}/po/detail/${id}`, { + next: { tags: ["po"] }, + }); +}); + +export const createStockInLine = async (data: StockInLineEntry) => { + const stockInLine = await serverFetchJson>(`${BASE_API_URL}/stockInLine/create`, { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }); + return stockInLine +} + +export const updateStockInLine = async (data: StockInLineEntry) => { + const stockInLine = await serverFetchJson>(`${BASE_API_URL}/stockInLine/update`, { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }); + return stockInLine +} + + diff --git a/src/app/api/po/index.ts b/src/app/api/po/index.ts new file mode 100644 index 0000000..534bdb4 --- /dev/null +++ b/src/app/api/po/index.ts @@ -0,0 +1,55 @@ +import { cache } from "react"; +import "server-only"; +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; + +export interface PoResult { + id: number + code: string + orderDate: string + estimatedArrivalDate: string + completedDate: string + status: string + pol?: PurchaseOrderLine[] + } + +export interface PurchaseOrderLine { + id: number + purchaseOrderId: number + itemId: number + itemNo: string + itemName: string + qty: number + price: number + status: string + stockInLine: StockInLine[] +} + +export interface StockInLine { + id: number + stockInId: number + purchaseOrderId?: number + purchaseOrderLineId: number + itemId: number + itemNo: string + itemName: string + demandQty: number + acceptedQty: number + price: number + priceUnit: string + productDate: string + shelfLifeDate: string + status: string +} + + export const fetchPoList = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/po/list`, { + next: { tags: ["po"] }, + }); + }); + + export const fetchPoWithStockInLines = cache(async (id: number) => { + return serverFetchJson(`${BASE_API_URL}/po/detail/${id}`, { + next: { tags: ["po"] }, + }); + }); \ No newline at end of file diff --git a/src/app/api/qc/index.ts b/src/app/api/qc/index.ts new file mode 100644 index 0000000..cb6f244 --- /dev/null +++ b/src/app/api/qc/index.ts @@ -0,0 +1,12 @@ +import { cache } from "react"; +import "server-only"; + +export interface QcItemWithChecks { + id: number; + code: string; + name: string; + itemId: number; + lowerLimit: number; + upperLimit: number; + description: string; + } diff --git a/src/app/utils/formatUtil.ts b/src/app/utils/formatUtil.ts index c2e38ea..e73f663 100644 --- a/src/app/utils/formatUtil.ts +++ b/src/app/utils/formatUtil.ts @@ -7,3 +7,14 @@ export const moneyFormatter = new Intl.NumberFormat("en-HK", { style: "currency", currency: "HKD", }); + +export const stockInLineStatusMap: { [status: string]: number } = { + draft: 0, + pending: 1, + qc: 2, + determine1: 3, + determine2: 4, + determine3: 5, + receiving: 6, + completed: 7, +}; \ No newline at end of file diff --git a/src/components/InputDataGrid/InputDataGrid.tsx b/src/components/InputDataGrid/InputDataGrid.tsx index 6166dd0..dac888c 100644 --- a/src/components/InputDataGrid/InputDataGrid.tsx +++ b/src/components/InputDataGrid/InputDataGrid.tsx @@ -62,7 +62,6 @@ export type TableRow = Partial< >; export interface InputDataGridProps { - // needAdd: boolean | undefined; apiRef: MutableRefObject checkboxSelection: false | undefined; _formKey: keyof T; diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index ad9be3c..2146c9a 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -47,7 +47,7 @@ const NavigationContent: React.FC = () => { { icon: , label: "Purchase Order", - path: "", + path: "/po", }, { icon: , diff --git a/src/components/PoDetail/PoDetail.tsx b/src/components/PoDetail/PoDetail.tsx new file mode 100644 index 0000000..00c78bd --- /dev/null +++ b/src/components/PoDetail/PoDetail.tsx @@ -0,0 +1,154 @@ +"use client"; + +import { fetchPoWithStockInLines, PoResult, PurchaseOrderLine, StockInLine } from "@/app/api/po"; +import { + Box, + Button, + Collapse, + Grid, + IconButton, + Paper, + Stack, + Tab, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Tabs, + TabsProps, + TextField, + Typography, +} from "@mui/material"; +import { useTranslation } from "react-i18next"; +// import InputDataGrid, { TableRow } from "../InputDataGrid/InputDataGrid"; +import { GridColDef, GridRowModel, useGridApiRef } from "@mui/x-data-grid"; +import { testFetch } from "@/app/api/po/actions"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { FormProvider, useForm } from "react-hook-form"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; +import InputDataGrid, { + TableRow as InputTableRow, +} from "../InputDataGrid/InputDataGrid"; +import PoInputGrid from "./PoInputGrid"; +import { QcItemWithChecks } from "@/app/api/qc"; +import { useSearchParams } from "next/navigation"; + +type Props = { + po: PoResult; + qc: QcItemWithChecks[] +}; + +type EntryError = + | { + [field in keyof StockInLine]?: string; + } + | undefined; + +// type PolRow = TableRow, EntryError>; +const PoDetail: React.FC = ({ + po, + // poLine, + qc + }) => { + const { t } = useTranslation(); + const apiRef = useGridApiRef(); + const [rows, setRows] = useState(po.pol || []); + const params = useSearchParams() + + function Row(props: { row: PurchaseOrderLine }) { + const { row } = props; + const [open, setOpen] = useState(false); + + return ( + <> + *": { borderBottom: "unset" }, color: "black" }}> + + setOpen(!open)} + > + {open ? : } + + + {row.itemNo} + {row.itemName} + {row.qty} + {/* {row.uom} */} + {row.price} + {/* {row.expiryDate} */} + {row.status} + + + + + + + + {/* */} + + + + + +
+
+
+
+ + ); + } + + return ( + <> + + + + {po.code} + + + + + + + + {/* for the collapse button */} + {t("itemNo")} + {t("itemName")} + {t("qty")} + {t("price")} + {/* {t("expiryDate")} */} + {t("status")} + {/* {"add icon button"} */} + + + + {rows.map((row) => ( + + ))} + +
+
+
+
+ + ); +}; +export default PoDetail; diff --git a/src/components/PoDetail/PoDetailLoading.tsx b/src/components/PoDetail/PoDetailLoading.tsx new file mode 100644 index 0000000..2c13844 --- /dev/null +++ b/src/components/PoDetail/PoDetailLoading.tsx @@ -0,0 +1,40 @@ +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const PoDetailLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default PoDetailLoading; diff --git a/src/components/PoDetail/PoDetailWrapper.tsx b/src/components/PoDetail/PoDetailWrapper.tsx new file mode 100644 index 0000000..83cef35 --- /dev/null +++ b/src/components/PoDetail/PoDetailWrapper.tsx @@ -0,0 +1,55 @@ +import { fetchAllItems } from "@/app/api/settings/item"; +// import ItemsSearch from "./ItemsSearch"; +// import ItemsSearchLoading from "./ItemsSearchLoading"; +import { SearchParams } from "@/app/utils/fetchUtil"; +import { TypeEnum } from "@/app/utils/typeEnum"; +import { notFound } from "next/navigation"; +import { fetchPoWithStockInLines, PoResult } from "@/app/api/po"; +import PoDetailLoading from "./PoDetailLoading"; +import PoDetail from "./PoDetail"; +import { QcItemWithChecks } from "@/app/api/qc"; + +interface SubComponents { + Loading: typeof PoDetailLoading; +} + +type Props = { + id: number; +}; + +const PoDetailWrapper: React.FC & SubComponents = async ({ id }) => { + const [ + poWithStockInLine + ] = await Promise.all([ + fetchPoWithStockInLines(id) + ]) + // const poWithStockInLine = await fetchPoWithStockInLines(id) + console.log(poWithStockInLine) + const qc: QcItemWithChecks[] = [ // just qc + { + id: 1, + code: "code1", + name: "name1", + itemId: 1, + lowerLimit: 1, + upperLimit: 3, + description: 'desc', + }, + { + id: 2, + code: "code2", + name: "name2", + itemId: 1, + lowerLimit: 1, + upperLimit: 3, + description: 'desc', + }, + ] + + + return ; +}; + +PoDetailWrapper.Loading = PoDetailLoading; + +export default PoDetailWrapper; diff --git a/src/components/PoDetail/PoInfoCard.tsx b/src/components/PoDetail/PoInfoCard.tsx new file mode 100644 index 0000000..92c71ac --- /dev/null +++ b/src/components/PoDetail/PoInfoCard.tsx @@ -0,0 +1,32 @@ +import { + Box, + Button, + Card, + CardContent, + Grid, + Stack, + Tab, + Tabs, + TabsProps, + Typography, +} from "@mui/material"; +type Props = { + // id?: number +}; + +const PoInfoCard: React.FC = async ( + { + // id + } +) => { + return ( + <> + + + + + + + ); +}; +export default PoInfoCard; diff --git a/src/components/PoDetail/PoInputGrid.tsx b/src/components/PoDetail/PoInputGrid.tsx new file mode 100644 index 0000000..adf8492 --- /dev/null +++ b/src/components/PoDetail/PoInputGrid.tsx @@ -0,0 +1,435 @@ +"use client"; +import { + FooterPropsOverrides, + GridActionsCellItem, + GridCellParams, + GridRowId, + GridRowIdGetter, + GridRowModel, + GridRowModes, + GridRowModesModel, + GridToolbarContainer, + useGridApiRef, +} from "@mui/x-data-grid"; +import { + Dispatch, + MutableRefObject, + SetStateAction, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; +import StyledDataGrid from "../StyledDataGrid"; +import { GridColDef } from "@mui/x-data-grid"; +import { Box, Button, Grid, Typography } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { Add } from "@mui/icons-material"; +import SaveIcon from "@mui/icons-material/Save"; +import DeleteIcon from "@mui/icons-material/Delete"; +import CancelIcon from "@mui/icons-material/Cancel"; +import FactCheckIcon from "@mui/icons-material/FactCheck"; +import ShoppingCartIcon from "@mui/icons-material/ShoppingCart"; +import PoQcModal from "./PoQcModal"; +import { QcItemWithChecks } from "src/app/api/qc"; +import PlayArrowIcon from "@mui/icons-material/PlayArrow"; +import { PurchaseOrderLine, StockInLine } from "@/app/api/po"; +import { createStockInLine, testFetch } from "@/app/api/po/actions"; +import { useSearchParams } from "next/navigation"; +import { stockInLineStatusMap } from "@/app/utils/formatUtil"; + +interface ResultWithId { + id: number; +} + +interface Props { + qc: QcItemWithChecks[]; + setRows: Dispatch>; + itemDetail: PurchaseOrderLine; + stockInLine: StockInLine[]; +} + +export type StockInLineEntryError = { + [field in keyof StockInLine]?: string; +}; + +export type StockInLineRow = Partial< + StockInLine & { + isActive: boolean | undefined; + _isNew: boolean; + _error: StockInLineEntryError; + } & ResultWithId +>; + +class ProcessRowUpdateError extends Error { + public readonly row: StockInLineRow; + public readonly errors: StockInLineEntryError | undefined; + constructor(row: StockInLineRow, message?: string, errors?: StockInLineEntryError) { + super(message); + this.row = row; + this.errors = errors; + + Object.setPrototypeOf(this, ProcessRowUpdateError.prototype); + } +} + +function PoInputGrid({ qc, setRows, itemDetail, stockInLine }: Props) { + const { t } = useTranslation("home"); + const apiRef = useGridApiRef(); + const [rowModesModel, setRowModesModel] = useState({}); + const getRowId = useCallback>( + (row) => row.id as number, + [] + ); + console.log(stockInLine); + const [entries, setEntries] = useState(stockInLine || []); + const [modalInfo, setModalInfo] = useState() + const [qcOpen, setQcOpen] = useState(false); + const [defaultQty, setDefaultQty] = useState(() => { + const total = entries.reduce((acc, curr) => acc + (curr.acceptedQty || 0), 0); + return itemDetail.qty - total; + }); + + const params = useSearchParams() + const refetchData = useCallback(async () => { + const id = parseInt(params.get("id")!!) + const res = await testFetch(id) + const pol = res.pol!! + console.log(pol) + setRows(pol); + }, [params]) + + const handleDelete = useCallback( + (id: GridRowId) => () => { + setEntries((es) => es.filter((e) => getRowId(e) !== id)); + }, + [getRowId] + ); + const handleStart = useCallback( + (id: GridRowId, params: any) => () => { + setRowModesModel((prev) => ({ + ...prev, + [id]: { mode: GridRowModes.View }, + })); + setTimeout(async () => { + // post stock in line + console.log("delayed"); + console.log(params); + const oldId = params.row.id + console.log(oldId) + const postData = { + itemId: params.row.itemId, + itemNo: params.row.itemNo, + itemName: params.row.itemName, + purchaseOrderId: params.row.purchaseOrderId, + purchaseOrderLineId: params.row.purchaseOrderLineId, + acceptedQty: params.row.acceptedQty, + } + const res = await createStockInLine(postData) + console.log(res) + // setEntries((prev) => prev.map((p) => p.id === oldId ? res.entity : p)) + // do post directly to test + // openStartModal(); + }, 200); + }, + [] + ); + const handleQC = useCallback( + (id: GridRowId, params: any) => () => { + setRowModesModel((prev) => ({ + ...prev, + [id]: { mode: GridRowModes.View }, + })); + setModalInfo(params.row) + setTimeout(() => { + // open qc modal + console.log("delayed"); + openQcModal(); + }, 200); + }, + [] + ); + const handleStockIn = useCallback( + (id: GridRowId) => () => { + setRowModesModel((prev) => ({ + ...prev, + [id]: { mode: GridRowModes.View }, + })); + setTimeout(() => { + // open stock in modal + // return the record with its status as pending + // update layout + console.log("delayed"); + }, 200); + }, + [] + ); + + const closeQcModal = useCallback(() => { + setQcOpen(false); + }, []); + const openQcModal = useCallback(() => { + setQcOpen(true); + }, []); + + const columns = useMemo( + () => [ + { + field: "itemNo", + flex: 1, + }, + { + field: "itemName", + flex: 1, + }, + { + field: "acceptedQty", + headerName: "qty", + flex: 0.5, + type: "number", + editable: true, + // replace with tooltip + content + }, + { + field: "status", + flex: 0.5, + editable: true + }, + { + field: "actions", + type: "actions", + headerName: "start | qc | stock in | delete", + flex: 1, + cellClassName: "actions", + getActions: (params) => { + // const stockInLineStatusMap: { [status: string]: number } = { + // draft: 0, + // pending: 1, + // qc: 2, + // determine1: 3, + // determine2: 4, + // determine3: 5, + // receiving: 6, + // completed: 7, + // }; + console.log(params.row.status); + const status = params.row.status.toLowerCase() + return [ + } + label="start" + sx={{ + color: "primary.main", + }} + disabled={!(stockInLineStatusMap[status] === 0)} + // set _isNew to false after posting + // or check status + onClick={handleStart(params.row.id, params)} + color="inherit" + key="edit" + />, + } + label="qc" + sx={{ + color: "primary.main", + }} + disabled={stockInLineStatusMap[status] <= 0 || stockInLineStatusMap[status] >= 6} + // set _isNew to false after posting + // or check status + onClick={handleQC(params.row.id, params)} + color="inherit" + key="edit" + />, + } + label="stockin" + sx={{ + color: "primary.main", + }} + disabled={stockInLineStatusMap[status] !== 6} + // set _isNew to false after posting + // or check status + onClick={handleStockIn(params.row.id)} + color="inherit" + key="edit" + />, + } + label="Delete" + sx={{ + color: "error.main", + }} + disabled={stockInLineStatusMap[status] !== 0} + // disabled={Boolean(params.row.status)} + onClick={handleDelete(params.row.id)} + color="inherit" + key="edit" + />, + ]; + }, + }, + ], + [] + ); + + const addRow = useCallback(() => { + const newEntry = { + id: Date.now(), + _isNew: true, + itemId: itemDetail.itemId, + purchaseOrderId: itemDetail.purchaseOrderId, + purchaseOrderLineId: itemDetail.id, + itemNo: itemDetail.itemNo, + itemName: itemDetail.itemName, + acceptedQty: defaultQty, + status: "draft", + }; + setEntries((e) => [...e, newEntry]); + setRowModesModel((model) => ({ + ...model, + [getRowId(newEntry)]: { + mode: GridRowModes.Edit, + // fieldToFocus: "projectId", + }, + })); + }, [getRowId]); + const validation = useCallback( + ( + newRow: GridRowModel + // rowModel: GridRowSelectionModel + ): StockInLineEntryError | undefined => { + const error: StockInLineEntryError = {}; + console.log(newRow); + console.log(defaultQty); + if (newRow.acceptedQty && newRow.acceptedQty > defaultQty) { + error["acceptedQty"] = "qty cannot be greater than remaining qty"; + } + return Object.keys(error).length > 0 ? error : undefined; + }, + [defaultQty] + ); + const processRowUpdate = useCallback( + (newRow: GridRowModel, originalRow: GridRowModel) => { + const errors = validation(newRow); // change to validation + if (errors) { + throw new ProcessRowUpdateError( + originalRow, + "validation error", + errors + ); + } + const { _isNew, _error, ...updatedRow } = newRow; + const rowToSave = { + ...updatedRow, + } satisfies StockInLineRow; + const newEntries = entries.map((e) => + getRowId(e) === getRowId(originalRow) ? rowToSave : e + ); + setEntries(newEntries); + //update remaining qty + const total = newEntries.reduce((acc, curr) => acc + (curr.acceptedQty || 0), 0); + setDefaultQty(itemDetail.qty - total); + return rowToSave; + }, + [getRowId, entries] + ); + + const onProcessRowUpdateError = useCallback( + (updateError: ProcessRowUpdateError) => { + const errors = updateError.errors; + const oldRow = updateError.row; + + apiRef.current.updateRows([{ ...oldRow, _error: errors }]); + }, + [apiRef] + ); + + useEffect(() => { + const total = entries.reduce((acc, curr) => acc + (curr.acceptedQty || 0), 0); + setDefaultQty(itemDetail.qty - total); + }, [entries]); + const footer = ( + + + + ); + return ( + <> + ) => { + let classname = ""; + if (params.row._error) { + classname = "hasError"; + } + return classname; + }} + slots={{ + footer: FooterToolbar, + noRowsOverlay: NoRowsOverlay, + }} + slotProps={{ + footer: { child: footer }, + }} + /> + <> + + + + ); +} +const NoRowsOverlay: React.FC = () => { + const { t } = useTranslation("home"); + return ( + + {t("Add some entries!")} + + ); +}; + +const FooterToolbar: React.FC = ({ child }) => { + return {child}; +}; +export default PoInputGrid; diff --git a/src/components/PoDetail/PoQcModal.tsx b/src/components/PoDetail/PoQcModal.tsx new file mode 100644 index 0000000..71fdd3d --- /dev/null +++ b/src/components/PoDetail/PoQcModal.tsx @@ -0,0 +1,141 @@ +"use client"; + +import { PurchaseQCInput, StockInLineEntry, updateStockInLine } from "@/app/api/po/actions"; +import { Box, Button, Modal, ModalProps, Stack } from "@mui/material"; +import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; +import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import QcForm from "./QcForm"; +import { QcItemWithChecks } from "@/app/api/qc"; +import { Check } from "@mui/icons-material"; +import { StockInLine } from "@/app/api/po"; +import { useSearchParams } from "next/navigation"; +import { stockInLineStatusMap } from "@/app/utils/formatUtil"; +import { StockInLineRow } from "./PoInputGrid"; +// type: + + +interface CommonProps extends Omit { + setEntries: Dispatch> + itemDetail: StockInLine; + qc?: QcItemWithChecks[]; + warehouse?: any[]; +} +interface QcProps extends CommonProps { + qc: QcItemWithChecks[]; +} +interface StockInProps extends CommonProps { + // naming + warehouse: any[]; +} + +type Props = QcProps | StockInProps; + +const style = { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + bgcolor: "background.paper", + pt: 5, + px: 5, + pb: 10, + width: { xs: "80%", sm: "80%", md: "80%" }, +}; +const PoQcModal: React.FC = ({ + setEntries, + open, + onClose, + itemDetail, + qc, + warehouse, + }) => { + console.log(itemDetail) + const [serverError, setServerError] = useState(""); + const { t } = useTranslation(); + const params = useSearchParams() + console.log(params.get("id")) + const [defaultValues, setDefaultValues] = useState({}); + const formProps = useForm({ + defaultValues: defaultValues ? defaultValues : {}, + }); + const errors = formProps.formState.errors; + const closeHandler = useCallback>( + (...args) => { + onClose?.(...args); + // reset(); + }, + [onClose] + ); + + useEffect(() => { + setDefaultValues({}); + }, []); + + const onSubmit = useCallback>( + async (data, event) => { + let hasErrors = false; + console.log(errors); + console.log(data); + console.log(itemDetail); + try { + if (hasErrors) { + setServerError(t("An error has occurred. Please try again later.")); + return false; + } + // do post update stock in line + // const reqStatus = stockInLineStatusMap + const args: StockInLineEntry = { + id: itemDetail.id, + purchaseOrderId: parseInt(params.get("id")!!), + purchaseOrderLineId: itemDetail.purchaseOrderLineId, + itemId: itemDetail.itemId, + acceptedQty: itemDetail.acceptedQty, + status: "receiving", + } + console.log(args) + const res = await updateStockInLine(args) + // this.res.entity = list of entity + for (const inLine in res.entity as StockInLine[]) { + + } + + console.log(res) + // if (res) + } catch (e) { + // server error + setServerError(t("An error has occurred. Please try again later.")); + console.log(e); + } + }, + [t, itemDetail] + ); + + return ( + <> + + + + {qc && } + + + + + + + + ); +}; +export default PoQcModal; diff --git a/src/components/PoDetail/QcForm.tsx b/src/components/PoDetail/QcForm.tsx new file mode 100644 index 0000000..c210746 --- /dev/null +++ b/src/components/PoDetail/QcForm.tsx @@ -0,0 +1,215 @@ +"use client"; + +import { PurchaseQcCheck, PurchaseQCInput } from "@/app/api/po/actions"; +import { + Box, + Card, + CardContent, + Grid, + Stack, + TextField, + Tooltip, + Typography, +} from "@mui/material"; +import { useFormContext } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import StyledDataGrid from "../StyledDataGrid"; +import { useCallback, useMemo } from "react"; +import { + GridColDef, + GridRowIdGetter, + GridRowModel, + useGridApiContext, + GridRenderCellParams, + GridRenderEditCellParams, + useGridApiRef, +} from "@mui/x-data-grid"; +import InputDataGrid from "../InputDataGrid"; +import { TableRow } from "../InputDataGrid/InputDataGrid"; +import TwoLineCell from "./TwoLineCell"; +import QcSelect from "./QcSelect"; +import { QcItemWithChecks } from "@/app/api/qc"; +import { GridEditInputCell } from "@mui/x-data-grid"; +import { StockInLine } from "@/app/api/po"; + +interface Props { + itemDetail: StockInLine; + qc: QcItemWithChecks[]; +} +type EntryError = + | { + [field in keyof PurchaseQcCheck]?: string; + } + | undefined; + +type PoQcRow = TableRow, EntryError>; + +const QcForm: React.FC = ({ + qc, + itemDetail, + }) => { + const { t } = useTranslation(); + const apiRef = useGridApiRef(); + const { + register, + formState: { errors, defaultValues, touchedFields }, + watch, + control, + setValue, + getValues, + reset, + resetField, + setError, + clearErrors, + } = useFormContext(); + console.log(itemDetail) + const columns = useMemo( + () => [ + { + field: "qcCheckId", + headerName: "qc Check", + flex: 1, + editable: true, + valueFormatter(params) { + const row = params.id ? params.api.getRow(params.id) : null; + if (!row) { + return null; + } + const Qc = qc.find((q) => q.id === row.qcCheckId); + return Qc ? `${Qc.code} - ${Qc.name}` : t("Please select QC"); + }, + renderCell(params: GridRenderCellParams) { + console.log(params.value); + return {params.formattedValue}; + }, + renderEditCell(params: GridRenderEditCellParams) { + const errorMessage = + params.row._error?.[params.field as keyof PurchaseQcCheck]; + console.log(errorMessage); + const content = ( + { + await params.api.setEditCellValue({ + id: params.id, + field: "qcCheckId", + value: qcCheckId, + }); + }} + /> + ); + return errorMessage ? ( + + {content} + + ) : ( + content + ); + }, + }, + { + field: "qty", + headerName: "qty", + flex: 1, + editable: true, + type: "number", + renderEditCell(params: GridRenderEditCellParams) { + const errorMessage = + params.row._error?.[params.field as keyof PurchaseQcCheck]; + const content = ; + return errorMessage ? ( + + {content} + + ) : ( + content + ); + }, + }, + ], + [] + ); + const validationTest = useCallback( + (newRow: GridRowModel): EntryError => { + const error: EntryError = {}; + const { qcCheckId, qty } = newRow; + if (!qcCheckId || qcCheckId <= 0) { + error["qcCheckId"] = "select qc"; + } + if (!qty || qty <= 0) { + error["qty"] = "enter a qty"; + } + return Object.keys(error).length > 0 ? error : undefined; + }, + [] + ); + return ( + + + + {t("Qc Detail")} + + + + + + + + + + + + + + + + + apiRef={apiRef} + checkboxSelection={false} + _formKey={"qcCheck"} + columns={columns} + validateRow={validationTest} + /> + + + + ); +}; +export default QcForm; diff --git a/src/components/PoDetail/QcSelect.tsx b/src/components/PoDetail/QcSelect.tsx new file mode 100644 index 0000000..7207021 --- /dev/null +++ b/src/components/PoDetail/QcSelect.tsx @@ -0,0 +1,78 @@ +import React, { useCallback, useMemo } from "react"; +import { + Autocomplete, + Box, + Checkbox, + Chip, + ListSubheader, + MenuItem, + TextField, + Tooltip, +} from "@mui/material"; +import { QcItemWithChecks } from "@/app/api/qc"; +import { useTranslation } from "react-i18next"; + +interface CommonProps { + allQcs: QcItemWithChecks[]; + error?: boolean; +} + +interface SingleAutocompleteProps extends CommonProps { + value: number | string | undefined; + onQcSelect: (qcCheckId: number) => void | Promise; + // multiple: false; +} + +type Props = SingleAutocompleteProps; + +const QcSelect: React.FC = ({ allQcs, value, error, onQcSelect }) => { + const { t } = useTranslation("home"); + const filteredQc = useMemo(() => { + // do filtering here if any + return allQcs; + }, []); + const options = useMemo(() => { + return [ + { + value: -1, // think think sin + label: t("None"), + group: "default", + }, + ...filteredQc.map((q) => ({ + value: q.id, + label: `${q.code} - ${q.name}`, + group: "existing", + })), + ]; + }, [filteredQc]); + + const currentValue = options.find((o) => o.value === value) || options[0]; + + const onChange = useCallback( + ( + event: React.SyntheticEvent, + newValue: { value: number; group: string } | { value: number }[] + ) => { + const singleNewVal = newValue as { + value: number; + group: string; + }; + onQcSelect(singleNewVal.value); + }, + [onQcSelect] + ); + + return ( + option.label} + options={options} + renderInput={(params) => } + /> + ); +}; +export default QcSelect; diff --git a/src/components/PoDetail/TwoLineCell.tsx b/src/components/PoDetail/TwoLineCell.tsx new file mode 100644 index 0000000..f32e56a --- /dev/null +++ b/src/components/PoDetail/TwoLineCell.tsx @@ -0,0 +1,24 @@ +import { Box, Tooltip } from "@mui/material"; +import React from "react"; + +const TwoLineCell: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return ( + + + {children} + + + ); +}; + +export default TwoLineCell; diff --git a/src/components/PoDetail/index.ts b/src/components/PoDetail/index.ts new file mode 100644 index 0000000..5365696 --- /dev/null +++ b/src/components/PoDetail/index.ts @@ -0,0 +1 @@ +export { default } from "./PoDetailWrapper"; diff --git a/src/components/PoSearch/PoSearch.tsx b/src/components/PoSearch/PoSearch.tsx new file mode 100644 index 0000000..87cc49e --- /dev/null +++ b/src/components/PoSearch/PoSearch.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { PoResult } from "@/app/api/po"; +import { useCallback, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useRouter, useSearchParams } from "next/navigation"; +import SearchBox, { Criterion } from "../SearchBox"; +import SearchResults, { Column } from "../SearchResults"; +import { EditNote } from "@mui/icons-material"; + +type Props = { + po: PoResult[]; +}; +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const PoSearch: React.FC = ({ po }) => { + const [filteredPo, setFilteredPo] = useState(po); + const { t } = useTranslation("po"); + const router = useRouter(); + + const searchCriteria: Criterion[] = useMemo(() => { + var searchCriteria: Criterion[] = [ + { label: t("Code"), paramName: "code", type: "text" }, + // { label: t("Name"), paramName: "name", type: "text" }, + ]; + return searchCriteria; + }, [t, po]); + + + const onDetailClick = useCallback( + (po: PoResult) => { + router.push(`/po/edit?id=${po.id}`); + }, + [router] + ); + + const onDeleteClick = useCallback( + (po: PoResult) => {}, + [router] + ); + + const columns = useMemo[]>( + () => [ + { + name: "id", + label: t("Details"), + onClick: onDetailClick, + buttonIcon: , + }, + { + name: "code", + label: t("Code"), + }, + // { + // name: "name", + // label: t("Name"), + // }, + // { + // name: "action", + // label: t(""), + // buttonIcon: , + // onClick: onDeleteClick, + // }, + ], + [filteredPo] + ); + + const onReset = useCallback(() => { + setFilteredPo(po); + }, [po]); + + return ( + <> + { + setFilteredPo( + po.filter((p) => { + return ( + p.code.toLowerCase().includes(query.code.toLowerCase()) + // p.name.toLowerCase().includes(query.name.toLowerCase()) + ); + }) + ); + }} + onReset={onReset} + /> + items={filteredPo} columns={columns}/> + + ); +}; +export default PoSearch; diff --git a/src/components/PoSearch/PoSearchLoading.tsx b/src/components/PoSearch/PoSearchLoading.tsx new file mode 100644 index 0000000..cab2a37 --- /dev/null +++ b/src/components/PoSearch/PoSearchLoading.tsx @@ -0,0 +1,40 @@ +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const PoSearchLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default PoSearchLoading; diff --git a/src/components/PoSearch/PoSearchWrapper.tsx b/src/components/PoSearch/PoSearchWrapper.tsx new file mode 100644 index 0000000..8d3f5b4 --- /dev/null +++ b/src/components/PoSearch/PoSearchWrapper.tsx @@ -0,0 +1,35 @@ +import { fetchAllItems } from "@/app/api/settings/item"; +// import ItemsSearch from "./ItemsSearch"; +// import ItemsSearchLoading from "./ItemsSearchLoading"; +import { SearchParams } from "@/app/utils/fetchUtil"; +import { TypeEnum } from "@/app/utils/typeEnum"; +import { notFound } from "next/navigation"; +import PoSearchLoading from "./PoSearchLoading"; +import PoSearch from "./PoSearch"; +import { fetchPoList, PoResult } from "@/app/api/po"; + +interface SubComponents { + Loading: typeof PoSearchLoading; +} + +type Props = { + // type: TypeEnum; +}; + +const PoSearchWrapper: React.FC & SubComponents = async ( + { + // type, + } +) => { + const [ + po + ] = await Promise.all([ + fetchPoList() + ]); + + return ; +}; + +PoSearchWrapper.Loading = PoSearchLoading; + +export default PoSearchWrapper; diff --git a/src/components/PoSearch/index.ts b/src/components/PoSearch/index.ts new file mode 100644 index 0000000..4ac7af9 --- /dev/null +++ b/src/components/PoSearch/index.ts @@ -0,0 +1 @@ +export { default } from "./PoSearchWrapper"; diff --git a/src/components/QcItemSave/QcItemDetails.tsx b/src/components/QcItemSave/QcItemDetails.tsx index 838b72c..fd51e06 100644 --- a/src/components/QcItemSave/QcItemDetails.tsx +++ b/src/components/QcItemSave/QcItemDetails.tsx @@ -1,58 +1,63 @@ -import { SaveQcItemInputs } from "@/app/api/settings/qcItem/actions" -import { Box, Card, CardContent, Grid, Stack, TextField, Typography } from "@mui/material" -import { useFormContext } from "react-hook-form" -import { useTranslation } from "react-i18next" +import { SaveQcItemInputs } from "@/app/api/settings/qcItem/actions"; +import { + Box, + Card, + CardContent, + Grid, + Stack, + TextField, + Typography, +} from "@mui/material"; +import { useFormContext } from "react-hook-form"; +import { useTranslation } from "react-i18next"; const QcItemDetails = () => { + const { t } = useTranslation(); + const { register } = useFormContext(); - const { t } = useTranslation() - const { - register - } = useFormContext() + return ( + + + + + {t("Qc Item Details")} + + + + + + + + + + + + + + + + ); +}; - return ( - - - - - {t("Qc Item Details")} - - - - - - - - - - - - - - - - ) -} - -export default QcItemDetails \ No newline at end of file +export default QcItemDetails; diff --git a/src/components/RoughScheduleDetail/ViewByFGDetails.tsx b/src/components/RoughScheduleDetail/ViewByFGDetails.tsx index edfdc24..0a3a4de 100644 --- a/src/components/RoughScheduleDetail/ViewByFGDetails.tsx +++ b/src/components/RoughScheduleDetail/ViewByFGDetails.tsx @@ -22,6 +22,7 @@ import EditableSearchResults, {Column} from "@/components/SearchResults/Editable type Props = { apiRef: MutableRefObject + isEdit: Boolean }; type EntryError = | { @@ -34,9 +35,11 @@ export type FGRecord = { code: string; name: string; inStockQty: number; - productionQty: number; + productionQty?: number; + purchaseQty?: number } + const ViewByFGDetails: React.FC = ({ apiRef, isEdit }) => { const { t, @@ -58,35 +61,70 @@ const ViewByFGDetails: React.FC = ({ apiRef, isEdit }) => { '2025-05-17', ]; + const fakeRecordLine = useMemo( + () => [ + [ + { id: 1, code: "mt1", name: "material 1", inStockQty: 10, purchaseQty: 1 }, + { id: 2, code: "mt2", name: "material 2", inStockQty: 20, purchaseQty: 199 }, + ], + [ + { id: 3, code: "mt3", name: "material 3", inStockQty: 30, purchaseQty: 3 }, + { id: 4, code: "mt4", name: "material 4", inStockQty: 40, purchaseQty: 499 }, + ], + [ + { id: 5, code: "mt5", name: "material 5", inStockQty: 50, purchaseQty: 5 }, + { id: 6, code: "mt6", name: "material 6", inStockQty: 60, purchaseQty: 699 }, + ], + [ + { id: 7, code: "mt7", name: "material 7", inStockQty: 70, purchaseQty: 7 }, + { id: 8, code: "mt8", name: "material 8", inStockQty: 80, purchaseQty: 899 }, + ], + [ + { id: 9, code: "mt9", name: "material 9", inStockQty: 90, purchaseQty: 9 }, + { id: 10, code: "mt10", name: "material 10", inStockQty: 100, purchaseQty: 999 }, + ], + [ + { id: 11, code: "mt11", name: "material 11", inStockQty: 110, purchaseQty: 11 }, + { id: 12, code: "mt12", name: "material 12", inStockQty: 120, purchaseQty: 1299 }, + ], + [ + { id: 13, code: "mt13", name: "material 13", inStockQty: 130, purchaseQty: 13 }, + { id: 14, code: "mt14", name: "material 14", inStockQty: 140, purchaseQty: 1499 }, + ], + ], + [] + ); + + const fakeRecords = useMemo( () => [ [ - { id: 1, code: "fg1", name: "finished good 1", inStockQty: 10, productionQty: 1 }, - { id: 2, code: "fg2", name: "finished good 2", inStockQty: 20, productionQty: 199 }, + { id: 1, code: "fg1", name: "finished good 1", inStockQty: 10, productionQty: 1, lines: [{ id: 1, code: "mt1", name: "material 1", inStockQty: 10, purchaseQty: 1 }] }, + { id: 2, code: "fg2", name: "finished good 2", inStockQty: 20, productionQty: 199, lines: [{ id: 2, code: "mt2", name: "material 2", inStockQty: 20, purchaseQty: 199 }] }, ], [ - { id: 3, code: "fg3", name: "finished good 3", inStockQty: 30, productionQty: 3 }, - { id: 4, code: "fg4", name: "finished good 4", inStockQty: 40, productionQty: 499 }, + { id: 3, code: "fg3", name: "finished good 3", inStockQty: 30, productionQty: 3, lines: [{ id: 3, code: "mt3", name: "material 3", inStockQty: 30, purchaseQty: 3 }] }, + { id: 4, code: "fg4", name: "finished good 4", inStockQty: 40, productionQty: 499, lines: [{ id: 4, code: "mt4", name: "material 4", inStockQty: 40, purchaseQty: 499 }] }, ], [ - { id: 5, code: "fg5", name: "finished good 5", inStockQty: 50, productionQty: 5 }, - { id: 6, code: "fg6", name: "finished good 6", inStockQty: 60, productionQty: 699 }, + { id: 5, code: "fg5", name: "finished good 5", inStockQty: 50, productionQty: 5, lines: [{ id: 5, code: "mt5", name: "material 5", inStockQty: 50, purchaseQty: 5 }] }, + { id: 6, code: "fg6", name: "finished good 6", inStockQty: 60, productionQty: 699, lines: [{ id: 6, code: "mt6", name: "material 6", inStockQty: 60, purchaseQty: 699 }] }, ], [ - { id: 7, code: "fg7", name: "finished good 7", inStockQty: 70, productionQty: 7 }, - { id: 8, code: "fg8", name: "finished good 8", inStockQty: 80, productionQty: 899 }, + { id: 7, code: "fg7", name: "finished good 7", inStockQty: 70, productionQty: 7, lines: [{ id: 7, code: "mt7", name: "material 7", inStockQty: 70, purchaseQty: 7 }] }, + { id: 8, code: "fg8", name: "finished good 8", inStockQty: 80, productionQty: 899, lines: [{ id: 8, code: "mt8", name: "material 8", inStockQty: 80, purchaseQty: 899 }] }, ], [ - { id: 9, code: "fg9", name: "finished good 9", inStockQty: 90, productionQty: 9 }, - { id: 10, code: "fg10", name: "finished good 10", inStockQty: 100, productionQty: 999 }, + { id: 9, code: "fg9", name: "finished good 9", inStockQty: 90, productionQty: 9, lines: [{ id: 9, code: "mt9", name: "material 9", inStockQty: 90, purchaseQty: 9 }] }, + { id: 10, code: "fg10", name: "finished good 10", inStockQty: 100, productionQty: 999, lines: [{ id: 10, code: "mt10", name: "material 10", inStockQty: 100, purchaseQty: 999 }] }, ], [ - { id: 11, code: "fg11", name: "finished good 11", inStockQty: 110, productionQty: 11 }, - { id: 12, code: "fg12", name: "finished good 12", inStockQty: 120, productionQty: 1299 }, + { id: 11, code: "fg11", name: "finished good 11", inStockQty: 110, productionQty: 11, lines: [{ id: 11, code: "mt11", name: "material 11", inStockQty: 110, purchaseQty: 11 }] }, + { id: 12, code: "fg12", name: "finished good 12", inStockQty: 120, productionQty: 1299, lines: [{ id: 12, code: "mt12", name: "material 12", inStockQty: 120, purchaseQty: 1299 }] }, ], [ - { id: 13, code: "fg13", name: "finished good 13", inStockQty: 130, productionQty: 13 }, - { id: 14, code: "fg14", name: "finished good 14", inStockQty: 140, productionQty: 1499 }, + { id: 13, code: "fg13", name: "finished good 13", inStockQty: 130, productionQty: 13, lines: [{ id: 13, code: "mt13", name: "material 13", inStockQty: 130, purchaseQty: 13 }] }, + { id: 14, code: "fg14", name: "finished good 14", inStockQty: 140, productionQty: 1499, lines: [{ id: 14, code: "mt14", name: "material 14", inStockQty: 140, purchaseQty: 1499 }] }, ], ], [] @@ -135,6 +173,7 @@ const ViewByFGDetails: React.FC = ({ apiRef, isEdit }) => { items={fakeRecords[index]} // Use the corresponding records for the day + isMockUp={true} columns={columns} setPagingController={setPagingController} pagingController={pagingController} diff --git a/src/components/SearchResults/EditableSearchResults.tsx b/src/components/SearchResults/EditableSearchResults.tsx index 83cdd44..eb36f4e 100644 --- a/src/components/SearchResults/EditableSearchResults.tsx +++ b/src/components/SearchResults/EditableSearchResults.tsx @@ -16,6 +16,10 @@ import CancelIcon from "@mui/icons-material/Close"; import DeleteIcon from "@mui/icons-material/Delete"; import TextField from "@mui/material/TextField"; import MultiSelect from "@/components/SearchBox/MultiSelect"; +import { Collapse } from "@mui/material"; +import TempInputGridForMockUp from "./TempInputGridForMockUp"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; export interface ResultWithId { id: string | number; @@ -41,6 +45,7 @@ export type Column = interface Props { items: T[], + isMockUp?: Boolean, columns: Column[], noWrapper?: boolean, setPagingController: (value: { pageNum: number; pageSize: number; totalCount: number }) => void, @@ -50,6 +55,7 @@ interface Props { function EditableSearchResults({ items, + isMockUp, columns, noWrapper, pagingController, @@ -62,7 +68,7 @@ function EditableSearchResults({ const [rowsPerPage, setRowsPerPage] = useState(10); const [editingRowId, setEditingRowId] = useState(null); const [editedItems, setEditedItems] = useState(items); - + console.log(items) useEffect(()=>{ setEditedItems(items) },[items]) @@ -116,6 +122,124 @@ function EditableSearchResults({ } },[isEdit]) + function Row(props: { row: T }) { + const { row } = props; + const [open, setOpen] = useState(false); + console.log(row) + console.log(row.lines) + return ( + <> + + { + !isHideButton && + {(editingRowId === row.id) ? ( + <> + handleSaveClick(row)}> + + + setEditingRowId(null)}> + + + setOpen(!open)} + > + {open ? : } + + + ) : ( + <> + handleEditClick(row.id as number)}> + + + handleDeleteClick(row.id as number)}> + + + setOpen(!open)} + > + {open ? : } + + + )} + + + } + {columns.map((column, idx) => { + console.log(column) + const columnName = column.field; + return ( + + {editingRowId === row.id ? ( + (() => { + switch (column.type) { + case 'input': + console.log(column.type) + return ( + handleInputChange(row.id as number, columnName, e.target.value)} + /> + ); + case 'multi-select': + return ( + handleInputChange(row.id as number, columnName, selectedValues)} + /> + ); + case 'read-only': + return ( + + {row[columnName] as string} + + ); + default: + return null; // Handle any default case if needed + } + })() + ) : ( + column.renderCell ? + column.renderCell(row) + : + isEdit && handleEditClick(row.id as number)}> + {row[columnName] as string} + + )} + + ); + })} + + + + + + + + + + + + +
+
+
+
+ + ) + } + const table = ( <> @@ -132,80 +256,7 @@ function EditableSearchResults({ {(isAutoPaging ? editedItems.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) : editedItems).map((item) => ( - - { - !isHideButton && - {(editingRowId === item.id) ? ( - <> - handleSaveClick(item)}> - - - setEditingRowId(null)}> - - - - ) : ( - <> - handleEditClick(item.id as number)}> - - - handleDeleteClick(item.id as number)}> - - - - )} - - } - {columns.map((column, idx) => { - const columnName = column.field; - - return ( - - {editingRowId === item.id ? ( - (() => { - switch (column.type) { - case 'input': - return ( - handleInputChange(item.id as number, columnName, e.target.value)} - /> - ); - case 'multi-select': - return ( - handleInputChange(item.id as number, columnName, selectedValues)} - /> - ); - case 'read-only': - return ( - - {item[columnName] as string} - - ); - default: - return null; // Handle any default case if needed - } - })() - ) : ( - column.renderCell ? - column.renderCell(item) - : - isEdit && handleEditClick(item.id as number)}> - {item[columnName] as string} - - )} - - ); - })} - + ))} diff --git a/src/components/SearchResults/SearchResults.tsx b/src/components/SearchResults/SearchResults.tsx index 40cb22d..75cdc78 100644 --- a/src/components/SearchResults/SearchResults.tsx +++ b/src/components/SearchResults/SearchResults.tsx @@ -36,7 +36,7 @@ interface Props { items: T[], columns: Column[], noWrapper?: boolean, - setPagingController: (value: (((prevState: { pageNum: number; pageSize: number; totalCount: number }) => { + setPagingController?: (value: (((prevState: { pageNum: number; pageSize: number; totalCount: number }) => { pageNum: number; pageSize: number; totalCount: number @@ -163,7 +163,7 @@ function SearchResults({ >; + // itemDetail: PurchaseOrderLine; + stockInLine: any[]; +} + +export type StockInLineEntryError = { + [field in keyof any]?: string; +}; + +export type StockInLineRow = Partial< + any & { + isActive: boolean | undefined; + _isNew: boolean; + _error: StockInLineEntryError; + } & ResultWithId +>; + +class ProcessRowUpdateError extends Error { + public readonly row: StockInLineRow; + public readonly errors: StockInLineEntryError | undefined; + constructor(row: StockInLineRow, message?: string, errors?: StockInLineEntryError) { + super(message); + this.row = row; + this.errors = errors; + + Object.setPrototypeOf(this, ProcessRowUpdateError.prototype); + } +} + +function TempInputGridForMockUp({ stockInLine }: Props) { + const { t } = useTranslation("home"); + const apiRef = useGridApiRef(); + const [rowModesModel, setRowModesModel] = useState({}); + const getRowId = useCallback>( + (row) => row.id as number, + [] + ); + console.log(stockInLine); + const [entries, setEntries] = useState(stockInLine || []); + const [modalInfo, setModalInfo] = useState() + const [qcOpen, setQcOpen] = useState(false); + // const [defaultQty, setDefaultQty] = useState(() => { + // const total = entries.reduce((acc, curr) => acc + (curr.acceptedQty || 0), 0); + // return itemDetail.qty - total; + // }); + + const params = useSearchParams() + + const handleDelete = useCallback( + (id: GridRowId) => () => { + setEntries((es) => es.filter((e) => getRowId(e) !== id)); + }, + [getRowId] + ); + const handleStart = useCallback( + (id: GridRowId, params: any) => () => { + setRowModesModel((prev) => ({ + ...prev, + [id]: { mode: GridRowModes.View }, + })); + setTimeout(async () => { + // post stock in line + console.log("delayed"); + console.log(params); + const oldId = params.row.id + console.log(oldId) + const postData = { + itemId: params.row.itemId, + itemNo: params.row.itemNo, + itemName: params.row.itemName, + purchaseOrderId: params.row.purchaseOrderId, + purchaseOrderLineId: params.row.purchaseOrderLineId, + acceptedQty: params.row.acceptedQty, + } + const res = await createStockInLine(postData) + console.log(res) + // setEntries((prev) => prev.map((p) => p.id === oldId ? res.entity : p)) + // do post directly to test + // openStartModal(); + }, 200); + }, + [] + ); + const handleQC = useCallback( + (id: GridRowId, params: any) => () => { + setRowModesModel((prev) => ({ + ...prev, + [id]: { mode: GridRowModes.View }, + })); + setModalInfo(params.row) + setTimeout(() => { + // open qc modal + console.log("delayed"); + openQcModal(); + }, 200); + }, + [] + ); + const handleStockIn = useCallback( + (id: GridRowId) => () => { + setRowModesModel((prev) => ({ + ...prev, + [id]: { mode: GridRowModes.View }, + })); + setTimeout(() => { + // open stock in modal + // return the record with its status as pending + // update layout + console.log("delayed"); + }, 200); + }, + [] + ); + + const closeQcModal = useCallback(() => { + setQcOpen(false); + }, []); + const openQcModal = useCallback(() => { + setQcOpen(true); + }, []); + + const columns = useMemo( + () => [ + { + field: "code", + flex: 1, + }, + { + field: "name", + flex: 1, + }, + { + field: "inStockQty", + headerName: "inStockQty", + flex: 0.5, + type: "number", + editable: true, + // replace with tooltip + content + }, + { + field: "purchaseQty", + headerName: "purchaseQty", + flex: 0.5, + editable: true + }, + // { + // field: "actions", + // type: "actions", + // headerName: "start | qc | stock in | delete", + // flex: 1, + // cellClassName: "actions", + // getActions: (params) => { + // // const stockInLineStatusMap: { [status: string]: number } = { + // // draft: 0, + // // pending: 1, + // // qc: 2, + // // determine1: 3, + // // determine2: 4, + // // determine3: 5, + // // receiving: 6, + // // completed: 7, + // // }; + // console.log(params.row.status); + // const status = params.row.status.toLowerCase() + // return [ + // } + // label="start" + // sx={{ + // color: "primary.main", + // }} + // disabled={!(stockInLineStatusMap[status] === 0)} + // // set _isNew to false after posting + // // or check status + // onClick={handleStart(params.row.id, params)} + // color="inherit" + // key="edit" + // />, + // } + // label="qc" + // sx={{ + // color: "primary.main", + // }} + // disabled={stockInLineStatusMap[status] <= 0 || stockInLineStatusMap[status] >= 6} + // // set _isNew to false after posting + // // or check status + // onClick={handleQC(params.row.id, params)} + // color="inherit" + // key="edit" + // />, + // } + // label="stockin" + // sx={{ + // color: "primary.main", + // }} + // disabled={stockInLineStatusMap[status] !== 6} + // // set _isNew to false after posting + // // or check status + // onClick={handleStockIn(params.row.id)} + // color="inherit" + // key="edit" + // />, + // } + // label="Delete" + // sx={{ + // color: "error.main", + // }} + // disabled={stockInLineStatusMap[status] !== 0} + // // disabled={Boolean(params.row.status)} + // onClick={handleDelete(params.row.id)} + // color="inherit" + // key="edit" + // />, + // ]; + // }, + // }, + ], + [] + ); + + // const addRow = useCallback(() => { + // const newEntry = { + // id: Date.now(), + // _isNew: true, + // itemId: itemDetail.itemId, + // purchaseOrderId: itemDetail.purchaseOrderId, + // purchaseOrderLineId: itemDetail.id, + // itemNo: itemDetail.itemNo, + // itemName: itemDetail.itemName, + // acceptedQty: defaultQty, + // status: "draft", + // }; + // setEntries((e) => [...e, newEntry]); + // setRowModesModel((model) => ({ + // ...model, + // [getRowId(newEntry)]: { + // mode: GridRowModes.Edit, + // // fieldToFocus: "projectId", + // }, + // })); + // }, [getRowId]); + const validation = useCallback( + ( + newRow: GridRowModel + // rowModel: GridRowSelectionModel + ): StockInLineEntryError | undefined => { + const error: StockInLineEntryError = {}; + console.log(newRow); + // if (newRow.acceptedQty && newRow.acceptedQty > defaultQty) { + // error["acceptedQty"] = "qty cannot be greater than remaining qty"; + // } + return Object.keys(error).length > 0 ? error : undefined; + }, + [] + ); + const processRowUpdate = useCallback( + (newRow: GridRowModel, originalRow: GridRowModel) => { + const errors = validation(newRow); // change to validation + if (errors) { + throw new ProcessRowUpdateError( + originalRow, + "validation error", + errors + ); + } + const { _isNew, _error, ...updatedRow } = newRow; + const rowToSave = { + ...updatedRow, + } satisfies StockInLineRow; + const newEntries = entries.map((e) => + getRowId(e) === getRowId(originalRow) ? rowToSave : e + ); + setEntries(newEntries); + //update remaining qty + const total = newEntries.reduce((acc, curr) => acc + (curr.acceptedQty || 0), 0); + // setDefaultQty(itemDetail.qty - total); + return rowToSave; + }, + [getRowId, entries] + ); + + const onProcessRowUpdateError = useCallback( + (updateError: ProcessRowUpdateError) => { + const errors = updateError.errors; + const oldRow = updateError.row; + + apiRef.current.updateRows([{ ...oldRow, _error: errors }]); + }, + [apiRef] + ); + + // useEffect(() => { + // const total = entries.reduce((acc, curr) => acc + (curr.acceptedQty || 0), 0); + // setDefaultQty(itemDetail.qty - total); + // }, [entries]); + + // const footer = ( + // + // + // + // ); + return ( + <> + ) => { + let classname = ""; + if (params.row._error) { + classname = "hasError"; + } + return classname; + }} + // slots={{ + // footer: FooterToolbar, + // noRowsOverlay: NoRowsOverlay, + // }} + // slotProps={{ + // footer: { child: footer }, + // }} + /> + + ); +} +const NoRowsOverlay: React.FC = () => { + const { t } = useTranslation("home"); + return ( + + {t("Add some entries!")} + + ); +}; + +const FooterToolbar: React.FC = ({ child }) => { + return {child}; +}; +export default TempInputGridForMockUp;