@@ -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", | style: "currency", | ||||
currency: "HKD", | 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> { | export interface InputDataGridProps<T, V, E> { | ||||
// needAdd: boolean | undefined; | |||||
apiRef: MutableRefObject<GridApiCommunity> | apiRef: MutableRefObject<GridApiCommunity> | ||||
checkboxSelection: false | undefined; | checkboxSelection: false | undefined; | ||||
_formKey: keyof T; | _formKey: keyof T; | ||||
@@ -47,7 +47,7 @@ const NavigationContent: React.FC = () => { | |||||
{ | { | ||||
icon: <RequestQuote />, | icon: <RequestQuote />, | ||||
label: "Purchase Order", | label: "Purchase Order", | ||||
path: "", | |||||
path: "/po", | |||||
}, | }, | ||||
{ | { | ||||
icon: <RequestQuote />, | 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 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 = { | type Props = { | ||||
apiRef: MutableRefObject<GridApiCommunity> | apiRef: MutableRefObject<GridApiCommunity> | ||||
isEdit: Boolean | |||||
}; | }; | ||||
type EntryError = | type EntryError = | ||||
| { | | { | ||||
@@ -34,9 +35,11 @@ export type FGRecord = { | |||||
code: string; | code: string; | ||||
name: string; | name: string; | ||||
inStockQty: number; | inStockQty: number; | ||||
productionQty: number; | |||||
productionQty?: number; | |||||
purchaseQty?: number | |||||
} | } | ||||
const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit }) => { | const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit }) => { | ||||
const { | const { | ||||
t, | t, | ||||
@@ -58,35 +61,70 @@ const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit }) => { | |||||
'2025-05-17', | '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[][]>( | 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> | </Typography> | ||||
<EditableSearchResults<FGRecord> | <EditableSearchResults<FGRecord> | ||||
items={fakeRecords[index]} // Use the corresponding records for the day | items={fakeRecords[index]} // Use the corresponding records for the day | ||||
isMockUp={true} | |||||
columns={columns} | columns={columns} | ||||
setPagingController={setPagingController} | setPagingController={setPagingController} | ||||
pagingController={pagingController} | pagingController={pagingController} | ||||
@@ -16,6 +16,10 @@ import CancelIcon from "@mui/icons-material/Close"; | |||||
import DeleteIcon from "@mui/icons-material/Delete"; | import DeleteIcon from "@mui/icons-material/Delete"; | ||||
import TextField from "@mui/material/TextField"; | import TextField from "@mui/material/TextField"; | ||||
import MultiSelect from "@/components/SearchBox/MultiSelect"; | 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 { | export interface ResultWithId { | ||||
id: string | number; | id: string | number; | ||||
@@ -41,6 +45,7 @@ export type Column<T extends ResultWithId> = | |||||
interface Props<T extends ResultWithId> { | interface Props<T extends ResultWithId> { | ||||
items: T[], | items: T[], | ||||
isMockUp?: Boolean, | |||||
columns: Column<T>[], | columns: Column<T>[], | ||||
noWrapper?: boolean, | noWrapper?: boolean, | ||||
setPagingController: (value: { pageNum: number; pageSize: number; totalCount: number }) => void, | setPagingController: (value: { pageNum: number; pageSize: number; totalCount: number }) => void, | ||||
@@ -50,6 +55,7 @@ interface Props<T extends ResultWithId> { | |||||
function EditableSearchResults<T extends ResultWithId>({ | function EditableSearchResults<T extends ResultWithId>({ | ||||
items, | items, | ||||
isMockUp, | |||||
columns, | columns, | ||||
noWrapper, | noWrapper, | ||||
pagingController, | pagingController, | ||||
@@ -62,7 +68,7 @@ function EditableSearchResults<T extends ResultWithId>({ | |||||
const [rowsPerPage, setRowsPerPage] = useState(10); | const [rowsPerPage, setRowsPerPage] = useState(10); | ||||
const [editingRowId, setEditingRowId] = useState<number | null>(null); | const [editingRowId, setEditingRowId] = useState<number | null>(null); | ||||
const [editedItems, setEditedItems] = useState<T[]>(items); | const [editedItems, setEditedItems] = useState<T[]>(items); | ||||
console.log(items) | |||||
useEffect(()=>{ | useEffect(()=>{ | ||||
setEditedItems(items) | setEditedItems(items) | ||||
},[items]) | },[items]) | ||||
@@ -116,6 +122,124 @@ function EditableSearchResults<T extends ResultWithId>({ | |||||
} | } | ||||
},[isEdit]) | },[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 = ( | const table = ( | ||||
<> | <> | ||||
<TableContainer sx={{ maxHeight: 440 }}> | <TableContainer sx={{ maxHeight: 440 }}> | ||||
@@ -132,80 +256,7 @@ function EditableSearchResults<T extends ResultWithId>({ | |||||
</TableHead> | </TableHead> | ||||
<TableBody> | <TableBody> | ||||
{(isAutoPaging ? editedItems.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) : editedItems).map((item) => ( | {(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> | </TableBody> | ||||
</Table> | </Table> | ||||
@@ -36,7 +36,7 @@ interface Props<T extends ResultWithId> { | |||||
items: T[], | items: T[], | ||||
columns: Column<T>[], | columns: Column<T>[], | ||||
noWrapper?: boolean, | noWrapper?: boolean, | ||||
setPagingController: (value: (((prevState: { pageNum: number; pageSize: number; totalCount: number }) => { | |||||
setPagingController?: (value: (((prevState: { pageNum: number; pageSize: number; totalCount: number }) => { | |||||
pageNum: number; | pageNum: number; | ||||
pageSize: number; | pageSize: number; | ||||
totalCount: number | totalCount: number | ||||
@@ -163,7 +163,7 @@ function SearchResults<T extends ResultWithId>({ | |||||
<TablePagination | <TablePagination | ||||
rowsPerPageOptions={[10, 25, 100]} | rowsPerPageOptions={[10, 25, 100]} | ||||
component="div" | component="div" | ||||
count={pagingController.totalCount == 0 ? items.length : pagingController.totalCount} | |||||
count={!pagingController || pagingController.totalCount == 0 ? items.length : pagingController.totalCount} | |||||
rowsPerPage={rowsPerPage} | rowsPerPage={rowsPerPage} | ||||
page={page} | page={page} | ||||
onPageChange={handleChangePage} | 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; |