Преглед на файлове

partial po flow + rough schedule collapse

create_edit_user
MSI\derek преди 3 месеца
родител
ревизия
6b3f7d024e
променени са 27 файла, в които са добавени 2228 реда и са изтрити 148 реда
  1. +32
    -0
      src/app/(main)/po/edit/page.tsx
  2. +48
    -0
      src/app/(main)/po/page.tsx
  3. +65
    -0
      src/app/api/po/actions.ts
  4. +55
    -0
      src/app/api/po/index.ts
  5. +12
    -0
      src/app/api/qc/index.ts
  6. +11
    -0
      src/app/utils/formatUtil.ts
  7. +0
    -1
      src/components/InputDataGrid/InputDataGrid.tsx
  8. +1
    -1
      src/components/NavigationContent/NavigationContent.tsx
  9. +154
    -0
      src/components/PoDetail/PoDetail.tsx
  10. +40
    -0
      src/components/PoDetail/PoDetailLoading.tsx
  11. +55
    -0
      src/components/PoDetail/PoDetailWrapper.tsx
  12. +32
    -0
      src/components/PoDetail/PoInfoCard.tsx
  13. +435
    -0
      src/components/PoDetail/PoInputGrid.tsx
  14. +141
    -0
      src/components/PoDetail/PoQcModal.tsx
  15. +215
    -0
      src/components/PoDetail/QcForm.tsx
  16. +78
    -0
      src/components/PoDetail/QcSelect.tsx
  17. +24
    -0
      src/components/PoDetail/TwoLineCell.tsx
  18. +1
    -0
      src/components/PoDetail/index.ts
  19. +93
    -0
      src/components/PoSearch/PoSearch.tsx
  20. +40
    -0
      src/components/PoSearch/PoSearchLoading.tsx
  21. +35
    -0
      src/components/PoSearch/PoSearchWrapper.tsx
  22. +1
    -0
      src/components/PoSearch/index.ts
  23. +59
    -54
      src/components/QcItemSave/QcItemDetails.tsx
  24. +54
    -15
      src/components/RoughScheduleDetail/ViewByFGDetails.tsx
  25. +126
    -75
      src/components/SearchResults/EditableSearchResults.tsx
  26. +2
    -2
      src/components/SearchResults/SearchResults.tsx
  27. +419
    -0
      src/components/SearchResults/TempInputGridForMockUp.tsx

+ 32
- 0
src/app/(main)/po/edit/page.tsx Целия файл

@@ -0,0 +1,32 @@
import { SearchParams } from "@/app/utils/fetchUtil";
import { TypeEnum } from "@/app/utils/typeEnum";
import CreateProductMaterial from "@/components/CreateItem";
import PoDetail from "@/components/PoDetail";
import { I18nProvider, getServerI18n } from "@/i18n";
import { Typography } from "@mui/material";
import isString from "lodash/isString";
import { notFound } from "next/navigation";

type Props = {} & SearchParams;

const PoEdit: React.FC<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;

+ 48
- 0
src/app/(main)/po/page.tsx Целия файл

@@ -0,0 +1,48 @@
import { preloadClaims } from "@/app/api/claims";
import ClaimSearch from "@/components/ClaimSearch";
import PoSearch from "@/components/PoSearch";
import { getServerI18n } from "@/i18n";
import Add from "@mui/icons-material/Add";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { Metadata } from "next";
import Link from "next/link";
import { Suspense } from "react";

export const metadata: Metadata = {
title: "Purchase Order",
};

const production: React.FC = async () => {
const { t } = await getServerI18n("claims");
// preloadClaims();

return (
<>
<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;

+ 65
- 0
src/app/api/po/actions.ts Целия файл

@@ -0,0 +1,65 @@
"use server";
import { BASE_API_URL } from "@/config/api";
// import { ServerFetchError, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil";
import { revalidateTag } from "next/cache";
import { cache } from "react";
import { PoResult, StockInLine } from ".";
import { serverFetchJson } from "@/app/utils/fetchUtil";
// import { BASE_API_URL } from "@/config/api";

export interface PostStockInLiineResponse<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
}



+ 55
- 0
src/app/api/po/index.ts Целия файл

@@ -0,0 +1,55 @@
import { cache } from "react";
import "server-only";
import { serverFetchJson } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";

export interface PoResult {
id: number
code: string
orderDate: string
estimatedArrivalDate: string
completedDate: string
status: string
pol?: PurchaseOrderLine[]
}

export interface PurchaseOrderLine {
id: number
purchaseOrderId: number
itemId: number
itemNo: string
itemName: string
qty: number
price: number
status: string
stockInLine: StockInLine[]
}

export interface StockInLine {
id: number
stockInId: number
purchaseOrderId?: number
purchaseOrderLineId: number
itemId: number
itemNo: string
itemName: string
demandQty: number
acceptedQty: number
price: number
priceUnit: string
productDate: string
shelfLifeDate: string
status: string
}

export const fetchPoList = cache(async () => {
return serverFetchJson<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"] },
});
});

+ 12
- 0
src/app/api/qc/index.ts Целия файл

@@ -0,0 +1,12 @@
import { cache } from "react";
import "server-only";

export interface QcItemWithChecks {
id: number;
code: string;
name: string;
itemId: number;
lowerLimit: number;
upperLimit: number;
description: string;
}

+ 11
- 0
src/app/utils/formatUtil.ts Целия файл

@@ -7,3 +7,14 @@ export const moneyFormatter = new Intl.NumberFormat("en-HK", {
style: "currency",
currency: "HKD",
});

export const stockInLineStatusMap: { [status: string]: number } = {
draft: 0,
pending: 1,
qc: 2,
determine1: 3,
determine2: 4,
determine3: 5,
receiving: 6,
completed: 7,
};

+ 0
- 1
src/components/InputDataGrid/InputDataGrid.tsx Целия файл

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


+ 1
- 1
src/components/NavigationContent/NavigationContent.tsx Целия файл

@@ -47,7 +47,7 @@ const NavigationContent: React.FC = () => {
{
icon: <RequestQuote />,
label: "Purchase Order",
path: "",
path: "/po",
},
{
icon: <RequestQuote />,


+ 154
- 0
src/components/PoDetail/PoDetail.tsx Целия файл

@@ -0,0 +1,154 @@
"use client";

import { fetchPoWithStockInLines, PoResult, PurchaseOrderLine, StockInLine } from "@/app/api/po";
import {
Box,
Button,
Collapse,
Grid,
IconButton,
Paper,
Stack,
Tab,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Tabs,
TabsProps,
TextField,
Typography,
} from "@mui/material";
import { useTranslation } from "react-i18next";
// import InputDataGrid, { TableRow } from "../InputDataGrid/InputDataGrid";
import { GridColDef, GridRowModel, useGridApiRef } from "@mui/x-data-grid";
import { testFetch } from "@/app/api/po/actions";
import { useCallback, useEffect, useMemo, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import InputDataGrid, {
TableRow as InputTableRow,
} from "../InputDataGrid/InputDataGrid";
import PoInputGrid from "./PoInputGrid";
import { QcItemWithChecks } from "@/app/api/qc";
import { useSearchParams } from "next/navigation";

type Props = {
po: PoResult;
qc: QcItemWithChecks[]
};

type EntryError =
| {
[field in keyof StockInLine]?: string;
}
| undefined;

// type PolRow = TableRow<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;

+ 40
- 0
src/components/PoDetail/PoDetailLoading.tsx Целия файл

@@ -0,0 +1,40 @@
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Skeleton from "@mui/material/Skeleton";
import Stack from "@mui/material/Stack";
import React from "react";

// Can make this nicer
export const PoDetailLoading: React.FC = () => {
return (
<>
<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;

+ 55
- 0
src/components/PoDetail/PoDetailWrapper.tsx Целия файл

@@ -0,0 +1,55 @@
import { fetchAllItems } from "@/app/api/settings/item";
// import ItemsSearch from "./ItemsSearch";
// import ItemsSearchLoading from "./ItemsSearchLoading";
import { SearchParams } from "@/app/utils/fetchUtil";
import { TypeEnum } from "@/app/utils/typeEnum";
import { notFound } from "next/navigation";
import { fetchPoWithStockInLines, PoResult } from "@/app/api/po";
import PoDetailLoading from "./PoDetailLoading";
import PoDetail from "./PoDetail";
import { QcItemWithChecks } from "@/app/api/qc";

interface SubComponents {
Loading: typeof PoDetailLoading;
}

type Props = {
id: number;
};

const PoDetailWrapper: React.FC<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;

+ 32
- 0
src/components/PoDetail/PoInfoCard.tsx Целия файл

@@ -0,0 +1,32 @@
import {
Box,
Button,
Card,
CardContent,
Grid,
Stack,
Tab,
Tabs,
TabsProps,
Typography,
} from "@mui/material";
type Props = {
// id?: number
};

const PoInfoCard: React.FC<Props> = async (
{
// id
}
) => {
return (
<>
<Card>
<CardContent>
</CardContent>
</Card>
</>
);
};
export default PoInfoCard;

+ 435
- 0
src/components/PoDetail/PoInputGrid.tsx Целия файл

@@ -0,0 +1,435 @@
"use client";
import {
FooterPropsOverrides,
GridActionsCellItem,
GridCellParams,
GridRowId,
GridRowIdGetter,
GridRowModel,
GridRowModes,
GridRowModesModel,
GridToolbarContainer,
useGridApiRef,
} from "@mui/x-data-grid";
import {
Dispatch,
MutableRefObject,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import StyledDataGrid from "../StyledDataGrid";
import { GridColDef } from "@mui/x-data-grid";
import { Box, Button, Grid, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
import { Add } from "@mui/icons-material";
import SaveIcon from "@mui/icons-material/Save";
import DeleteIcon from "@mui/icons-material/Delete";
import CancelIcon from "@mui/icons-material/Cancel";
import FactCheckIcon from "@mui/icons-material/FactCheck";
import ShoppingCartIcon from "@mui/icons-material/ShoppingCart";
import PoQcModal from "./PoQcModal";
import { QcItemWithChecks } from "src/app/api/qc";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import { PurchaseOrderLine, StockInLine } from "@/app/api/po";
import { createStockInLine, testFetch } from "@/app/api/po/actions";
import { useSearchParams } from "next/navigation";
import { stockInLineStatusMap } from "@/app/utils/formatUtil";

interface ResultWithId {
id: number;
}

interface Props {
qc: QcItemWithChecks[];
setRows: Dispatch<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;

+ 141
- 0
src/components/PoDetail/PoQcModal.tsx Целия файл

@@ -0,0 +1,141 @@
"use client";

import { PurchaseQCInput, StockInLineEntry, updateStockInLine } from "@/app/api/po/actions";
import { Box, Button, Modal, ModalProps, Stack } from "@mui/material";
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react";
import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import QcForm from "./QcForm";
import { QcItemWithChecks } from "@/app/api/qc";
import { Check } from "@mui/icons-material";
import { StockInLine } from "@/app/api/po";
import { useSearchParams } from "next/navigation";
import { stockInLineStatusMap } from "@/app/utils/formatUtil";
import { StockInLineRow } from "./PoInputGrid";
// type:


interface CommonProps extends Omit<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;

+ 215
- 0
src/components/PoDetail/QcForm.tsx Целия файл

@@ -0,0 +1,215 @@
"use client";

import { PurchaseQcCheck, PurchaseQCInput } from "@/app/api/po/actions";
import {
Box,
Card,
CardContent,
Grid,
Stack,
TextField,
Tooltip,
Typography,
} from "@mui/material";
import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import StyledDataGrid from "../StyledDataGrid";
import { useCallback, useMemo } from "react";
import {
GridColDef,
GridRowIdGetter,
GridRowModel,
useGridApiContext,
GridRenderCellParams,
GridRenderEditCellParams,
useGridApiRef,
} from "@mui/x-data-grid";
import InputDataGrid from "../InputDataGrid";
import { TableRow } from "../InputDataGrid/InputDataGrid";
import TwoLineCell from "./TwoLineCell";
import QcSelect from "./QcSelect";
import { QcItemWithChecks } from "@/app/api/qc";
import { GridEditInputCell } from "@mui/x-data-grid";
import { StockInLine } from "@/app/api/po";

interface Props {
itemDetail: StockInLine;
qc: QcItemWithChecks[];
}
type EntryError =
| {
[field in keyof PurchaseQcCheck]?: string;
}
| undefined;

type PoQcRow = TableRow<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;

+ 78
- 0
src/components/PoDetail/QcSelect.tsx Целия файл

@@ -0,0 +1,78 @@
import React, { useCallback, useMemo } from "react";
import {
Autocomplete,
Box,
Checkbox,
Chip,
ListSubheader,
MenuItem,
TextField,
Tooltip,
} from "@mui/material";
import { QcItemWithChecks } from "@/app/api/qc";
import { useTranslation } from "react-i18next";

interface CommonProps {
allQcs: QcItemWithChecks[];
error?: boolean;
}

interface SingleAutocompleteProps extends CommonProps {
value: number | string | undefined;
onQcSelect: (qcCheckId: number) => void | Promise<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;

+ 24
- 0
src/components/PoDetail/TwoLineCell.tsx Целия файл

@@ -0,0 +1,24 @@
import { Box, Tooltip } from "@mui/material";
import React from "react";

const TwoLineCell: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<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;

+ 1
- 0
src/components/PoDetail/index.ts Целия файл

@@ -0,0 +1 @@
export { default } from "./PoDetailWrapper";

+ 93
- 0
src/components/PoSearch/PoSearch.tsx Целия файл

@@ -0,0 +1,93 @@
"use client";

import { PoResult } from "@/app/api/po";
import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useRouter, useSearchParams } from "next/navigation";
import SearchBox, { Criterion } from "../SearchBox";
import SearchResults, { Column } from "../SearchResults";
import { EditNote } from "@mui/icons-material";

type Props = {
po: PoResult[];
};
type SearchQuery = Partial<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;

+ 40
- 0
src/components/PoSearch/PoSearchLoading.tsx Целия файл

@@ -0,0 +1,40 @@
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Skeleton from "@mui/material/Skeleton";
import Stack from "@mui/material/Stack";
import React from "react";

// Can make this nicer
export const PoSearchLoading: React.FC = () => {
return (
<>
<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;

+ 35
- 0
src/components/PoSearch/PoSearchWrapper.tsx Целия файл

@@ -0,0 +1,35 @@
import { fetchAllItems } from "@/app/api/settings/item";
// import ItemsSearch from "./ItemsSearch";
// import ItemsSearchLoading from "./ItemsSearchLoading";
import { SearchParams } from "@/app/utils/fetchUtil";
import { TypeEnum } from "@/app/utils/typeEnum";
import { notFound } from "next/navigation";
import PoSearchLoading from "./PoSearchLoading";
import PoSearch from "./PoSearch";
import { fetchPoList, PoResult } from "@/app/api/po";

interface SubComponents {
Loading: typeof PoSearchLoading;
}

type Props = {
// type: TypeEnum;
};

const PoSearchWrapper: React.FC<Props> & SubComponents = async (
{
// type,
}
) => {
const [
po
] = await Promise.all([
fetchPoList()
]);

return <PoSearch po={po} />;
};

PoSearchWrapper.Loading = PoSearchLoading;

export default PoSearchWrapper;

+ 1
- 0
src/components/PoSearch/index.ts Целия файл

@@ -0,0 +1 @@
export { default } from "./PoSearchWrapper";

+ 59
- 54
src/components/QcItemSave/QcItemDetails.tsx Целия файл

@@ -1,58 +1,63 @@
import { SaveQcItemInputs } from "@/app/api/settings/qcItem/actions"
import { Box, Card, CardContent, Grid, Stack, TextField, Typography } from "@mui/material"
import { useFormContext } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { SaveQcItemInputs } from "@/app/api/settings/qcItem/actions";
import {
Box,
Card,
CardContent,
Grid,
Stack,
TextField,
Typography,
} from "@mui/material";
import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";

const QcItemDetails = () => {
const { t } = useTranslation();
const { register } = useFormContext<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;

+ 54
- 15
src/components/RoughScheduleDetail/ViewByFGDetails.tsx Целия файл

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


+ 126
- 75
src/components/SearchResults/EditableSearchResults.tsx Целия файл

@@ -16,6 +16,10 @@ import CancelIcon from "@mui/icons-material/Close";
import DeleteIcon from "@mui/icons-material/Delete";
import TextField from "@mui/material/TextField";
import MultiSelect from "@/components/SearchBox/MultiSelect";
import { Collapse } from "@mui/material";
import TempInputGridForMockUp from "./TempInputGridForMockUp";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";

export interface ResultWithId {
id: string | number;
@@ -41,6 +45,7 @@ export type Column<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>


+ 2
- 2
src/components/SearchResults/SearchResults.tsx Целия файл

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


+ 419
- 0
src/components/SearchResults/TempInputGridForMockUp.tsx Целия файл

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

Зареждане…
Отказ
Запис