| @@ -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<Props> = 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 ( | |||
| <> | |||
| {/* <Typography variant="h4">{t("Create Material")}</Typography> */} | |||
| <I18nProvider namespaces={[type]}> | |||
| <PoDetail id={id} /> | |||
| </I18nProvider> | |||
| </> | |||
| ); | |||
| }; | |||
| export default PoEdit; | |||
| @@ -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 ( | |||
| <> | |||
| <Stack | |||
| direction="row" | |||
| justifyContent="space-between" | |||
| flexWrap="wrap" | |||
| rowGap={2} | |||
| > | |||
| <Typography variant="h4" marginInlineEnd={2}> | |||
| {t("Purchase Order")} | |||
| </Typography> | |||
| {/* <Button | |||
| variant="contained" | |||
| startIcon={<Add />} | |||
| LinkComponent={Link} | |||
| href="/po/create" | |||
| > | |||
| {t("Create Po")} | |||
| </Button> */} | |||
| </Stack> | |||
| <Suspense fallback={<PoSearch.Loading />}> | |||
| <PoSearch /> | |||
| </Suspense> | |||
| </> | |||
| ); | |||
| }; | |||
| export default production; | |||
| @@ -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<T> { | |||
| 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<PoResult>(`${BASE_API_URL}/po/detail/${id}`, { | |||
| next: { tags: ["po"] }, | |||
| }); | |||
| }); | |||
| export const createStockInLine = async (data: StockInLineEntry) => { | |||
| const stockInLine = await serverFetchJson<PostStockInLiineResponse<StockInLineEntry>>(`${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<PostStockInLiineResponse<StockInLineEntry>>(`${BASE_API_URL}/stockInLine/update`, { | |||
| method: "POST", | |||
| body: JSON.stringify(data), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }); | |||
| return stockInLine | |||
| } | |||
| @@ -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<PoResult[]>(`${BASE_API_URL}/po/list`, { | |||
| next: { tags: ["po"] }, | |||
| }); | |||
| }); | |||
| export const fetchPoWithStockInLines = cache(async (id: number) => { | |||
| return serverFetchJson<PoResult>(`${BASE_API_URL}/po/detail/${id}`, { | |||
| next: { tags: ["po"] }, | |||
| }); | |||
| }); | |||
| @@ -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; | |||
| } | |||
| @@ -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, | |||
| }; | |||
| @@ -62,7 +62,6 @@ export type TableRow<V, E> = Partial< | |||
| >; | |||
| export interface InputDataGridProps<T, V, E> { | |||
| // needAdd: boolean | undefined; | |||
| apiRef: MutableRefObject<GridApiCommunity> | |||
| checkboxSelection: false | undefined; | |||
| _formKey: keyof T; | |||
| @@ -47,7 +47,7 @@ const NavigationContent: React.FC = () => { | |||
| { | |||
| icon: <RequestQuote />, | |||
| label: "Purchase Order", | |||
| path: "", | |||
| path: "/po", | |||
| }, | |||
| { | |||
| icon: <RequestQuote />, | |||
| @@ -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<Partial<StockInLine>, EntryError>; | |||
| const PoDetail: React.FC<Props> = ({ | |||
| po, | |||
| // poLine, | |||
| qc | |||
| }) => { | |||
| const { t } = useTranslation(); | |||
| const apiRef = useGridApiRef(); | |||
| const [rows, setRows] = useState<PurchaseOrderLine[]>(po.pol || []); | |||
| const params = useSearchParams() | |||
| function Row(props: { row: PurchaseOrderLine }) { | |||
| const { row } = props; | |||
| const [open, setOpen] = useState(false); | |||
| return ( | |||
| <> | |||
| <TableRow sx={{ "& > *": { borderBottom: "unset" }, color: "black" }}> | |||
| <TableCell> | |||
| <IconButton | |||
| aria-label="expand row" | |||
| size="small" | |||
| onClick={() => setOpen(!open)} | |||
| > | |||
| {open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />} | |||
| </IconButton> | |||
| </TableCell> | |||
| <TableCell align="left">{row.itemNo}</TableCell> | |||
| <TableCell align="left">{row.itemName}</TableCell> | |||
| <TableCell align="left">{row.qty}</TableCell> | |||
| {/* <TableCell align="left">{row.uom}</TableCell> */} | |||
| <TableCell align="left">{row.price}</TableCell> | |||
| {/* <TableCell align="left">{row.expiryDate}</TableCell> */} | |||
| <TableCell align="left">{row.status}</TableCell> | |||
| </TableRow> | |||
| <TableRow> | |||
| <TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}> | |||
| <Collapse in={open} timeout="auto" unmountOnExit> | |||
| <Table> | |||
| <TableBody> | |||
| <TableRow> | |||
| {/* <Button | |||
| onClick={()=> { | |||
| console.log(row) | |||
| console.log(row.stockInLine) | |||
| }} | |||
| >console log</Button> */} | |||
| <TableCell> | |||
| <PoInputGrid | |||
| qc={qc} | |||
| setRows={setRows} | |||
| itemDetail={row} | |||
| stockInLine={row.stockInLine} | |||
| /> | |||
| </TableCell> | |||
| </TableRow> | |||
| </TableBody> | |||
| </Table> | |||
| </Collapse> | |||
| </TableCell> | |||
| </TableRow> | |||
| </> | |||
| ); | |||
| } | |||
| return ( | |||
| <> | |||
| <Stack | |||
| spacing={2} | |||
| // component="form" | |||
| // onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | |||
| > | |||
| <Grid> | |||
| <Typography mb={2} variant="h4"> | |||
| {po.code} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid> | |||
| <TableContainer component={Paper}> | |||
| <Table aria-label="collapsible table"> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableCell /> {/* for the collapse button */} | |||
| <TableCell>{t("itemNo")}</TableCell> | |||
| <TableCell align="left">{t("itemName")}</TableCell> | |||
| <TableCell align="left">{t("qty")}</TableCell> | |||
| <TableCell align="left">{t("price")}</TableCell> | |||
| {/* <TableCell align="left">{t("expiryDate")}</TableCell> */} | |||
| <TableCell align="left">{t("status")}</TableCell> | |||
| {/* <TableCell align="left">{"add icon button"}</TableCell> */} | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| {rows.map((row) => ( | |||
| <Row key={row.id} row={row} /> | |||
| ))} | |||
| </TableBody> | |||
| </Table> | |||
| </TableContainer> | |||
| </Grid> | |||
| </Stack> | |||
| </> | |||
| ); | |||
| }; | |||
| export default PoDetail; | |||
| @@ -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 ( | |||
| <> | |||
| <Card> | |||
| <CardContent> | |||
| <Stack spacing={2}> | |||
| <Skeleton variant="rounded" height={60} /> | |||
| <Skeleton variant="rounded" height={60} /> | |||
| <Skeleton variant="rounded" height={60} /> | |||
| <Skeleton | |||
| variant="rounded" | |||
| height={50} | |||
| width={100} | |||
| sx={{ alignSelf: "flex-end" }} | |||
| /> | |||
| </Stack> | |||
| </CardContent> | |||
| </Card> | |||
| <Card> | |||
| <CardContent> | |||
| <Stack spacing={2}> | |||
| <Skeleton variant="rounded" height={40} /> | |||
| <Skeleton variant="rounded" height={40} /> | |||
| <Skeleton variant="rounded" height={40} /> | |||
| <Skeleton variant="rounded" height={40} /> | |||
| </Stack> | |||
| </CardContent> | |||
| </Card> | |||
| </> | |||
| ); | |||
| }; | |||
| export default PoDetailLoading; | |||
| @@ -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<Props> & 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 <PoDetail po={poWithStockInLine} qc={qc} />; | |||
| }; | |||
| PoDetailWrapper.Loading = PoDetailLoading; | |||
| export default PoDetailWrapper; | |||
| @@ -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<Props> = async ( | |||
| { | |||
| // id | |||
| } | |||
| ) => { | |||
| return ( | |||
| <> | |||
| <Card> | |||
| <CardContent> | |||
| </CardContent> | |||
| </Card> | |||
| </> | |||
| ); | |||
| }; | |||
| export default PoInfoCard; | |||
| @@ -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<SetStateAction<PurchaseOrderLine[]>>; | |||
| 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<GridRowModesModel>({}); | |||
| const getRowId = useCallback<GridRowIdGetter<StockInLineRow>>( | |||
| (row) => row.id as number, | |||
| [] | |||
| ); | |||
| console.log(stockInLine); | |||
| const [entries, setEntries] = useState<StockInLineRow[]>(stockInLine || []); | |||
| const [modalInfo, setModalInfo] = useState<StockInLine>() | |||
| 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<GridColDef[]>( | |||
| () => [ | |||
| { | |||
| 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 [ | |||
| <GridActionsCellItem | |||
| icon={<PlayArrowIcon />} | |||
| 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" | |||
| />, | |||
| <GridActionsCellItem | |||
| icon={<FactCheckIcon />} | |||
| 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" | |||
| />, | |||
| <GridActionsCellItem | |||
| icon={<ShoppingCartIcon />} | |||
| 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" | |||
| />, | |||
| <GridActionsCellItem | |||
| icon={<DeleteIcon />} | |||
| 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<StockInLineRow> | |||
| // 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<StockInLineRow>, originalRow: GridRowModel<StockInLineRow>) => { | |||
| 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 = ( | |||
| <Box display="flex" gap={2} alignItems="center"> | |||
| <Button | |||
| disableRipple | |||
| variant="outlined" | |||
| startIcon={<Add />} | |||
| disabled={defaultQty <= 0} | |||
| onClick={addRow} | |||
| size="small" | |||
| > | |||
| {t("Record pol")} | |||
| </Button> | |||
| </Box> | |||
| ); | |||
| return ( | |||
| <> | |||
| <StyledDataGrid | |||
| getRowId={getRowId} | |||
| apiRef={apiRef} | |||
| autoHeight | |||
| sx={{ | |||
| "--DataGrid-overlayHeight": "100px", | |||
| ".MuiDataGrid-row .MuiDataGrid-cell.hasError": { | |||
| border: "1px solid", | |||
| borderColor: "error.main", | |||
| }, | |||
| ".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": { | |||
| border: "1px solid", | |||
| borderColor: "warning.main", | |||
| }, | |||
| }} | |||
| disableColumnMenu | |||
| editMode="row" | |||
| rows={entries} | |||
| rowModesModel={rowModesModel} | |||
| onRowModesModelChange={setRowModesModel} | |||
| processRowUpdate={processRowUpdate} | |||
| onProcessRowUpdateError={onProcessRowUpdateError} | |||
| columns={columns} | |||
| getCellClassName={(params: GridCellParams<StockInLineRow>) => { | |||
| let classname = ""; | |||
| if (params.row._error) { | |||
| classname = "hasError"; | |||
| } | |||
| return classname; | |||
| }} | |||
| slots={{ | |||
| footer: FooterToolbar, | |||
| noRowsOverlay: NoRowsOverlay, | |||
| }} | |||
| slotProps={{ | |||
| footer: { child: footer }, | |||
| }} | |||
| /> | |||
| <> | |||
| <PoQcModal | |||
| setEntries={setEntries} | |||
| qc={qc} | |||
| open={qcOpen} | |||
| onClose={closeQcModal} | |||
| itemDetail={modalInfo!!} | |||
| /> | |||
| </> | |||
| </> | |||
| ); | |||
| } | |||
| const NoRowsOverlay: React.FC = () => { | |||
| const { t } = useTranslation("home"); | |||
| return ( | |||
| <Box | |||
| display="flex" | |||
| justifyContent="center" | |||
| alignItems="center" | |||
| height="100%" | |||
| > | |||
| <Typography variant="caption">{t("Add some entries!")}</Typography> | |||
| </Box> | |||
| ); | |||
| }; | |||
| const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => { | |||
| return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>; | |||
| }; | |||
| export default PoInputGrid; | |||
| @@ -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<ModalProps, "children"> { | |||
| setEntries: Dispatch<SetStateAction<StockInLineRow[]>> | |||
| 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<Props> = ({ | |||
| 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<PurchaseQCInput>({ | |||
| defaultValues: defaultValues ? defaultValues : {}, | |||
| }); | |||
| const errors = formProps.formState.errors; | |||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
| (...args) => { | |||
| onClose?.(...args); | |||
| // reset(); | |||
| }, | |||
| [onClose] | |||
| ); | |||
| useEffect(() => { | |||
| setDefaultValues({}); | |||
| }, []); | |||
| const onSubmit = useCallback<SubmitHandler<PurchaseQCInput & {}>>( | |||
| 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 ( | |||
| <> | |||
| <Modal open={open} onClose={closeHandler}> | |||
| <FormProvider {...formProps}> | |||
| <Box | |||
| sx={style} | |||
| component="form" | |||
| onSubmit={formProps.handleSubmit(onSubmit)} | |||
| > | |||
| {qc && <QcForm qc={qc} itemDetail={itemDetail} />} | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| <Button | |||
| name="submit" | |||
| variant="contained" | |||
| startIcon={<Check />} | |||
| type="submit" | |||
| // disabled={submitDisabled} | |||
| > | |||
| {t("submit")} | |||
| </Button> | |||
| </Stack> | |||
| </Box> | |||
| </FormProvider> | |||
| </Modal> | |||
| </> | |||
| ); | |||
| }; | |||
| export default PoQcModal; | |||
| @@ -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<Partial<PurchaseQcCheck>, EntryError>; | |||
| const QcForm: React.FC<Props> = ({ | |||
| qc, | |||
| itemDetail, | |||
| }) => { | |||
| const { t } = useTranslation(); | |||
| const apiRef = useGridApiRef(); | |||
| const { | |||
| register, | |||
| formState: { errors, defaultValues, touchedFields }, | |||
| watch, | |||
| control, | |||
| setValue, | |||
| getValues, | |||
| reset, | |||
| resetField, | |||
| setError, | |||
| clearErrors, | |||
| } = useFormContext<PurchaseQCInput>(); | |||
| console.log(itemDetail) | |||
| const columns = useMemo<GridColDef[]>( | |||
| () => [ | |||
| { | |||
| field: "qcCheckId", | |||
| headerName: "qc Check", | |||
| flex: 1, | |||
| editable: true, | |||
| valueFormatter(params) { | |||
| const row = params.id ? params.api.getRow<PoQcRow>(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<PoQcRow, number>) { | |||
| console.log(params.value); | |||
| return <TwoLineCell>{params.formattedValue}</TwoLineCell>; | |||
| }, | |||
| renderEditCell(params: GridRenderEditCellParams<PoQcRow, number>) { | |||
| const errorMessage = | |||
| params.row._error?.[params.field as keyof PurchaseQcCheck]; | |||
| console.log(errorMessage); | |||
| const content = ( | |||
| <QcSelect | |||
| allQcs={qc} | |||
| value={params.row.qcCheckId} | |||
| onQcSelect={async (qcCheckId) => { | |||
| await params.api.setEditCellValue({ | |||
| id: params.id, | |||
| field: "qcCheckId", | |||
| value: qcCheckId, | |||
| }); | |||
| }} | |||
| /> | |||
| ); | |||
| return errorMessage ? ( | |||
| <Tooltip title={t(errorMessage)}> | |||
| <Box width="100%">{content}</Box> | |||
| </Tooltip> | |||
| ) : ( | |||
| content | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| field: "qty", | |||
| headerName: "qty", | |||
| flex: 1, | |||
| editable: true, | |||
| type: "number", | |||
| renderEditCell(params: GridRenderEditCellParams<PoQcRow>) { | |||
| const errorMessage = | |||
| params.row._error?.[params.field as keyof PurchaseQcCheck]; | |||
| const content = <GridEditInputCell {...params} />; | |||
| return errorMessage ? ( | |||
| <Tooltip title={t(errorMessage)}> | |||
| <Box width="100%">{content}</Box> | |||
| </Tooltip> | |||
| ) : ( | |||
| content | |||
| ); | |||
| }, | |||
| }, | |||
| ], | |||
| [] | |||
| ); | |||
| const validationTest = useCallback( | |||
| (newRow: GridRowModel<PoQcRow>): 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 ( | |||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | |||
| <Grid item xs={12}> | |||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||
| {t("Qc Detail")} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid | |||
| container | |||
| justifyContent="flex-start" | |||
| alignItems="flex-start" | |||
| spacing={2} | |||
| sx={{ mt: 0.5 }} | |||
| > | |||
| <Grid item xs={4}> | |||
| <TextField | |||
| label={t("sampleRate")} | |||
| fullWidth | |||
| {...register("sampleRate", { | |||
| required: "sampleRate required!", | |||
| })} | |||
| error={Boolean(errors.sampleRate)} | |||
| helperText={errors.sampleRate?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={4}> | |||
| <TextField | |||
| label={t("sampleWeight")} | |||
| fullWidth | |||
| {...register("sampleWeight", { | |||
| required: "sampleWeight required!", | |||
| })} | |||
| error={Boolean(errors.sampleWeight)} | |||
| helperText={errors.sampleWeight?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={4}> | |||
| <TextField | |||
| label={t("totalWeight")} | |||
| fullWidth | |||
| {...register("totalWeight", { | |||
| required: "totalWeight required!", | |||
| })} | |||
| error={Boolean(errors.totalWeight)} | |||
| helperText={errors.totalWeight?.message} | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| <Grid | |||
| container | |||
| justifyContent="flex-start" | |||
| alignItems="flex-start" | |||
| spacing={2} | |||
| sx={{ mt: 0.5 }} | |||
| > | |||
| <Grid item xs={12}> | |||
| <InputDataGrid<PurchaseQCInput, PurchaseQcCheck, EntryError> | |||
| apiRef={apiRef} | |||
| checkboxSelection={false} | |||
| _formKey={"qcCheck"} | |||
| columns={columns} | |||
| validateRow={validationTest} | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| </Grid> | |||
| ); | |||
| }; | |||
| export default QcForm; | |||
| @@ -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<void>; | |||
| // multiple: false; | |||
| } | |||
| type Props = SingleAutocompleteProps; | |||
| const QcSelect: React.FC<Props> = ({ 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 ( | |||
| <Autocomplete | |||
| noOptionsText={t("No Qc")} | |||
| disableClearable | |||
| fullWidth | |||
| value={currentValue} | |||
| onChange={onChange} | |||
| getOptionLabel={(option) => option.label} | |||
| options={options} | |||
| renderInput={(params) => <TextField {...params} error={error} />} | |||
| /> | |||
| ); | |||
| }; | |||
| export default QcSelect; | |||
| @@ -0,0 +1,24 @@ | |||
| import { Box, Tooltip } from "@mui/material"; | |||
| import React from "react"; | |||
| const TwoLineCell: React.FC<{ children: React.ReactNode }> = ({ children }) => { | |||
| return ( | |||
| <Tooltip title={children}> | |||
| <Box | |||
| sx={{ | |||
| whiteSpace: "normal", | |||
| overflow: "hidden", | |||
| textOverflow: "ellipsis", | |||
| display: "-webkit-box", | |||
| WebkitLineClamp: 2, | |||
| WebkitBoxOrient: "vertical", | |||
| lineHeight: "22px", | |||
| }} | |||
| > | |||
| {children} | |||
| </Box> | |||
| </Tooltip> | |||
| ); | |||
| }; | |||
| export default TwoLineCell; | |||
| @@ -0,0 +1 @@ | |||
| export { default } from "./PoDetailWrapper"; | |||
| @@ -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<Omit<PoResult, "id">>; | |||
| type SearchParamNames = keyof SearchQuery; | |||
| const PoSearch: React.FC<Props> = ({ po }) => { | |||
| const [filteredPo, setFilteredPo] = useState<PoResult[]>(po); | |||
| const { t } = useTranslation("po"); | |||
| const router = useRouter(); | |||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => { | |||
| var searchCriteria: Criterion<SearchParamNames>[] = [ | |||
| { 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<Column<PoResult>[]>( | |||
| () => [ | |||
| { | |||
| name: "id", | |||
| label: t("Details"), | |||
| onClick: onDetailClick, | |||
| buttonIcon: <EditNote />, | |||
| }, | |||
| { | |||
| name: "code", | |||
| label: t("Code"), | |||
| }, | |||
| // { | |||
| // name: "name", | |||
| // label: t("Name"), | |||
| // }, | |||
| // { | |||
| // name: "action", | |||
| // label: t(""), | |||
| // buttonIcon: <GridDeleteIcon />, | |||
| // onClick: onDeleteClick, | |||
| // }, | |||
| ], | |||
| [filteredPo] | |||
| ); | |||
| const onReset = useCallback(() => { | |||
| setFilteredPo(po); | |||
| }, [po]); | |||
| return ( | |||
| <> | |||
| <SearchBox | |||
| criteria={searchCriteria} | |||
| onSearch={(query) => { | |||
| setFilteredPo( | |||
| po.filter((p) => { | |||
| return ( | |||
| p.code.toLowerCase().includes(query.code.toLowerCase()) | |||
| // p.name.toLowerCase().includes(query.name.toLowerCase()) | |||
| ); | |||
| }) | |||
| ); | |||
| }} | |||
| onReset={onReset} | |||
| /> | |||
| <SearchResults<PoResult> items={filteredPo} columns={columns}/> | |||
| </> | |||
| ); | |||
| }; | |||
| export default PoSearch; | |||
| @@ -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 ( | |||
| <> | |||
| <Card> | |||
| <CardContent> | |||
| <Stack spacing={2}> | |||
| <Skeleton variant="rounded" height={60} /> | |||
| <Skeleton variant="rounded" height={60} /> | |||
| <Skeleton variant="rounded" height={60} /> | |||
| <Skeleton | |||
| variant="rounded" | |||
| height={50} | |||
| width={100} | |||
| sx={{ alignSelf: "flex-end" }} | |||
| /> | |||
| </Stack> | |||
| </CardContent> | |||
| </Card> | |||
| <Card> | |||
| <CardContent> | |||
| <Stack spacing={2}> | |||
| <Skeleton variant="rounded" height={40} /> | |||
| <Skeleton variant="rounded" height={40} /> | |||
| <Skeleton variant="rounded" height={40} /> | |||
| <Skeleton variant="rounded" height={40} /> | |||
| </Stack> | |||
| </CardContent> | |||
| </Card> | |||
| </> | |||
| ); | |||
| }; | |||
| export default PoSearchLoading; | |||
| @@ -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<Props> & SubComponents = async ( | |||
| { | |||
| // type, | |||
| } | |||
| ) => { | |||
| const [ | |||
| po | |||
| ] = await Promise.all([ | |||
| fetchPoList() | |||
| ]); | |||
| return <PoSearch po={po} />; | |||
| }; | |||
| PoSearchWrapper.Loading = PoSearchLoading; | |||
| export default PoSearchWrapper; | |||
| @@ -0,0 +1 @@ | |||
| export { default } from "./PoSearchWrapper"; | |||
| @@ -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<SaveQcItemInputs>(); | |||
| const { t } = useTranslation() | |||
| const { | |||
| register | |||
| } = useFormContext<SaveQcItemInputs>() | |||
| return ( | |||
| <Card sx={{ display: "block" }}> | |||
| <CardContent component={Stack} spacing={4}> | |||
| <Box> | |||
| <Typography variant={"overline"} display={"block"} marginBlockEnd={1}> | |||
| {t("Qc Item Details")} | |||
| </Typography> | |||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Code")} | |||
| fullWidth | |||
| {...register("code", { | |||
| required: "Code required!", | |||
| maxLength: 30, | |||
| })} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Name")} | |||
| fullWidth | |||
| {...register("name", { | |||
| required: "Name required!", | |||
| maxLength: 30, | |||
| })} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <TextField | |||
| label={t("Description")} | |||
| // multiline | |||
| fullWidth | |||
| {...register("description", { | |||
| maxLength: 100, | |||
| })} | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| </Box> | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| }; | |||
| return ( | |||
| <Card sx={{ display: "block" }}> | |||
| <CardContent component={Stack} spacing={4}> | |||
| <Box> | |||
| <Typography variant={"overline"} display={"block"} marginBlockEnd={1}> | |||
| {t("Qc Item Details")} | |||
| </Typography> | |||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Code")} | |||
| fullWidth | |||
| {...register("code", { | |||
| required: "Code required!", | |||
| maxLength: 30 | |||
| })} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Name")} | |||
| fullWidth | |||
| {...register("name", { | |||
| required: "Name required!", | |||
| maxLength: 30 | |||
| })} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <TextField | |||
| label={t("Description")} | |||
| // multiline | |||
| fullWidth | |||
| {...register("description", { | |||
| maxLength: 100 | |||
| })} | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| </Box> | |||
| </CardContent> | |||
| </Card> | |||
| ) | |||
| } | |||
| export default QcItemDetails | |||
| export default QcItemDetails; | |||
| @@ -22,6 +22,7 @@ import EditableSearchResults, {Column} from "@/components/SearchResults/Editable | |||
| type Props = { | |||
| apiRef: MutableRefObject<GridApiCommunity> | |||
| 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<Props> = ({ apiRef, isEdit }) => { | |||
| const { | |||
| t, | |||
| @@ -58,35 +61,70 @@ const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit }) => { | |||
| '2025-05-17', | |||
| ]; | |||
| const fakeRecordLine = useMemo<FGRecord[][]>( | |||
| () => [ | |||
| [ | |||
| { 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<FGRecord[][]>( | |||
| () => [ | |||
| [ | |||
| { 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<Props> = ({ apiRef, isEdit }) => { | |||
| </Typography> | |||
| <EditableSearchResults<FGRecord> | |||
| items={fakeRecords[index]} // Use the corresponding records for the day | |||
| isMockUp={true} | |||
| columns={columns} | |||
| setPagingController={setPagingController} | |||
| pagingController={pagingController} | |||
| @@ -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<T extends ResultWithId> = | |||
| interface Props<T extends ResultWithId> { | |||
| items: T[], | |||
| isMockUp?: Boolean, | |||
| columns: Column<T>[], | |||
| noWrapper?: boolean, | |||
| setPagingController: (value: { pageNum: number; pageSize: number; totalCount: number }) => void, | |||
| @@ -50,6 +55,7 @@ interface Props<T extends ResultWithId> { | |||
| function EditableSearchResults<T extends ResultWithId>({ | |||
| items, | |||
| isMockUp, | |||
| columns, | |||
| noWrapper, | |||
| pagingController, | |||
| @@ -62,7 +68,7 @@ function EditableSearchResults<T extends ResultWithId>({ | |||
| const [rowsPerPage, setRowsPerPage] = useState(10); | |||
| const [editingRowId, setEditingRowId] = useState<number | null>(null); | |||
| const [editedItems, setEditedItems] = useState<T[]>(items); | |||
| console.log(items) | |||
| useEffect(()=>{ | |||
| setEditedItems(items) | |||
| },[items]) | |||
| @@ -116,6 +122,124 @@ function EditableSearchResults<T extends ResultWithId>({ | |||
| } | |||
| },[isEdit]) | |||
| function Row(props: { row: T }) { | |||
| const { row } = props; | |||
| const [open, setOpen] = useState(false); | |||
| console.log(row) | |||
| console.log(row.lines) | |||
| return ( | |||
| <> | |||
| <TableRow hover tabIndex={-1} key={row.id}> | |||
| { | |||
| !isHideButton && <TableCell> | |||
| {(editingRowId === row.id) ? ( | |||
| <> | |||
| <IconButton disabled={!isEdit} onClick={() => handleSaveClick(row)}> | |||
| <SaveIcon/> | |||
| </IconButton> | |||
| <IconButton disabled={!isEdit} onClick={() => setEditingRowId(null)}> | |||
| <CancelIcon/> | |||
| </IconButton> | |||
| <IconButton | |||
| aria-label="expand row" | |||
| size="small" | |||
| onClick={() => setOpen(!open)} | |||
| > | |||
| {open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />} | |||
| </IconButton> | |||
| </> | |||
| ) : ( | |||
| <> | |||
| <IconButton disabled={!isEdit} | |||
| onClick={() => handleEditClick(row.id as number)}> | |||
| <EditIcon/> | |||
| </IconButton> | |||
| <IconButton disabled={!isEdit} | |||
| onClick={() => handleDeleteClick(row.id as number)}> | |||
| <DeleteIcon/> | |||
| </IconButton> | |||
| <IconButton | |||
| aria-label="expand row" | |||
| size="small" | |||
| onClick={() => setOpen(!open)} | |||
| > | |||
| {open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />} | |||
| </IconButton> | |||
| </> | |||
| )} | |||
| </TableCell> | |||
| } | |||
| {columns.map((column, idx) => { | |||
| console.log(column) | |||
| const columnName = column.field; | |||
| return ( | |||
| <TableCell key={`${columnName.toString()}-${idx}`}> | |||
| {editingRowId === row.id ? ( | |||
| (() => { | |||
| switch (column.type) { | |||
| case 'input': | |||
| console.log(column.type) | |||
| return ( | |||
| <TextField | |||
| hiddenLabel={true} | |||
| fullWidth | |||
| defaultValue={row[columnName] as string} | |||
| onChange={(e) => handleInputChange(row.id as number, columnName, e.target.value)} | |||
| /> | |||
| ); | |||
| case 'multi-select': | |||
| return ( | |||
| <MultiSelect | |||
| //label={column.label} | |||
| options={column.options} | |||
| selectedValues={[]} | |||
| onChange={(selectedValues) => handleInputChange(row.id as number, columnName, selectedValues)} | |||
| /> | |||
| ); | |||
| case 'read-only': | |||
| return ( | |||
| <span> | |||
| {row[columnName] as string} | |||
| </span> | |||
| ); | |||
| default: | |||
| return null; // Handle any default case if needed | |||
| } | |||
| })() | |||
| ) : ( | |||
| column.renderCell ? | |||
| column.renderCell(row) | |||
| : | |||
| <span onDoubleClick={() => isEdit && handleEditClick(row.id as number)}> | |||
| {row[columnName] as string} | |||
| </span> | |||
| )} | |||
| </TableCell> | |||
| ); | |||
| })} | |||
| </TableRow> | |||
| <TableRow> | |||
| <TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}> | |||
| <Collapse in={open} timeout="auto" unmountOnExit> | |||
| <Table> | |||
| <TableBody> | |||
| <TableRow> | |||
| <TableCell> | |||
| <TempInputGridForMockUp | |||
| stockInLine={row.lines as any[]} | |||
| /> | |||
| </TableCell> | |||
| </TableRow> | |||
| </TableBody> | |||
| </Table> | |||
| </Collapse> | |||
| </TableCell> | |||
| </TableRow> | |||
| </> | |||
| ) | |||
| } | |||
| const table = ( | |||
| <> | |||
| <TableContainer sx={{ maxHeight: 440 }}> | |||
| @@ -132,80 +256,7 @@ function EditableSearchResults<T extends ResultWithId>({ | |||
| </TableHead> | |||
| <TableBody> | |||
| {(isAutoPaging ? editedItems.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) : editedItems).map((item) => ( | |||
| <TableRow hover tabIndex={-1} key={item.id}> | |||
| { | |||
| !isHideButton && <TableCell> | |||
| {(editingRowId === item.id) ? ( | |||
| <> | |||
| <IconButton disabled={!isEdit} onClick={() => handleSaveClick(item)}> | |||
| <SaveIcon/> | |||
| </IconButton> | |||
| <IconButton disabled={!isEdit} onClick={() => setEditingRowId(null)}> | |||
| <CancelIcon/> | |||
| </IconButton> | |||
| </> | |||
| ) : ( | |||
| <> | |||
| <IconButton disabled={!isEdit} | |||
| onClick={() => handleEditClick(item.id as number)}> | |||
| <EditIcon/> | |||
| </IconButton> | |||
| <IconButton disabled={!isEdit} | |||
| onClick={() => handleDeleteClick(item.id as number)}> | |||
| <DeleteIcon/> | |||
| </IconButton> | |||
| </> | |||
| )} | |||
| </TableCell> | |||
| } | |||
| {columns.map((column, idx) => { | |||
| const columnName = column.field; | |||
| return ( | |||
| <TableCell key={`${columnName.toString()}-${idx}`}> | |||
| {editingRowId === item.id ? ( | |||
| (() => { | |||
| switch (column.type) { | |||
| case 'input': | |||
| return ( | |||
| <TextField | |||
| hiddenLabel={true} | |||
| fullWidth | |||
| defaultValue={item[columnName] as string} | |||
| onChange={(e) => handleInputChange(item.id as number, columnName, e.target.value)} | |||
| /> | |||
| ); | |||
| case 'multi-select': | |||
| return ( | |||
| <MultiSelect | |||
| //label={column.label} | |||
| options={column.options} | |||
| selectedValues={[]} | |||
| onChange={(selectedValues) => handleInputChange(item.id as number, columnName, selectedValues)} | |||
| /> | |||
| ); | |||
| case 'read-only': | |||
| return ( | |||
| <span> | |||
| {item[columnName] as string} | |||
| </span> | |||
| ); | |||
| default: | |||
| return null; // Handle any default case if needed | |||
| } | |||
| })() | |||
| ) : ( | |||
| column.renderCell ? | |||
| column.renderCell(item) | |||
| : | |||
| <span onDoubleClick={() => isEdit && handleEditClick(item.id as number)}> | |||
| {item[columnName] as string} | |||
| </span> | |||
| )} | |||
| </TableCell> | |||
| ); | |||
| })} | |||
| </TableRow> | |||
| <Row key={item.id} row={item} /> | |||
| ))} | |||
| </TableBody> | |||
| </Table> | |||
| @@ -36,7 +36,7 @@ interface Props<T extends ResultWithId> { | |||
| items: T[], | |||
| columns: Column<T>[], | |||
| 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<T extends ResultWithId>({ | |||
| <TablePagination | |||
| rowsPerPageOptions={[10, 25, 100]} | |||
| component="div" | |||
| count={pagingController.totalCount == 0 ? items.length : pagingController.totalCount} | |||
| count={!pagingController || pagingController.totalCount == 0 ? items.length : pagingController.totalCount} | |||
| rowsPerPage={rowsPerPage} | |||
| page={page} | |||
| onPageChange={handleChangePage} | |||
| @@ -0,0 +1,419 @@ | |||
| "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 { 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<SetStateAction<PurchaseOrderLine[]>>; | |||
| // 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<GridRowModesModel>({}); | |||
| const getRowId = useCallback<GridRowIdGetter<StockInLineRow>>( | |||
| (row) => row.id as number, | |||
| [] | |||
| ); | |||
| console.log(stockInLine); | |||
| const [entries, setEntries] = useState<StockInLineRow[]>(stockInLine || []); | |||
| const [modalInfo, setModalInfo] = useState<any>() | |||
| 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<GridColDef[]>( | |||
| () => [ | |||
| { | |||
| 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 [ | |||
| // <GridActionsCellItem | |||
| // icon={<PlayArrowIcon />} | |||
| // 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" | |||
| // />, | |||
| // <GridActionsCellItem | |||
| // icon={<FactCheckIcon />} | |||
| // 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" | |||
| // />, | |||
| // <GridActionsCellItem | |||
| // icon={<ShoppingCartIcon />} | |||
| // 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" | |||
| // />, | |||
| // <GridActionsCellItem | |||
| // icon={<DeleteIcon />} | |||
| // 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<StockInLineRow> | |||
| // 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<StockInLineRow>, originalRow: GridRowModel<StockInLineRow>) => { | |||
| 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 = ( | |||
| // <Box display="flex" gap={2} alignItems="center"> | |||
| // <Button | |||
| // disableRipple | |||
| // variant="outlined" | |||
| // startIcon={<Add />} | |||
| // disabled={defaultQty <= 0} | |||
| // onClick={addRow} | |||
| // size="small" | |||
| // > | |||
| // {t("Record pol")} | |||
| // </Button> | |||
| // </Box> | |||
| // ); | |||
| return ( | |||
| <> | |||
| <StyledDataGrid | |||
| getRowId={getRowId} | |||
| apiRef={apiRef} | |||
| autoHeight | |||
| sx={{ | |||
| "--DataGrid-overlayHeight": "100px", | |||
| ".MuiDataGrid-row .MuiDataGrid-cell.hasError": { | |||
| border: "1px solid", | |||
| borderColor: "error.main", | |||
| }, | |||
| ".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": { | |||
| border: "1px solid", | |||
| borderColor: "warning.main", | |||
| }, | |||
| }} | |||
| disableColumnMenu | |||
| editMode="row" | |||
| rows={entries} | |||
| rowModesModel={rowModesModel} | |||
| onRowModesModelChange={setRowModesModel} | |||
| processRowUpdate={processRowUpdate} | |||
| onProcessRowUpdateError={onProcessRowUpdateError} | |||
| columns={columns} | |||
| getCellClassName={(params: GridCellParams<StockInLineRow>) => { | |||
| 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 ( | |||
| <Box | |||
| display="flex" | |||
| justifyContent="center" | |||
| alignItems="center" | |||
| height="100%" | |||
| > | |||
| <Typography variant="caption">{t("Add some entries!")}</Typography> | |||
| </Box> | |||
| ); | |||
| }; | |||
| const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => { | |||
| return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>; | |||
| }; | |||
| export default TempInputGridForMockUp; | |||