@@ -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; |