diff --git a/src/app/api/po/actions.ts b/src/app/api/po/actions.ts index 3a30825..db872d1 100644 --- a/src/app/api/po/actions.ts +++ b/src/app/api/po/actions.ts @@ -16,7 +16,6 @@ export interface PostStockInLiineResponse { entity: StockInLine | StockInLine[] } - export interface StockInLineEntry { id?: number itemId: number @@ -24,19 +23,37 @@ export interface StockInLineEntry { purchaseOrderLineId: number acceptedQty: number status?: string + expiryDate?: string } export interface PurchaseQcCheck { qcCheckId: number; qty: number; } - +export interface StockInInput { + productLotNo?: string, + receiptDate: string + acceptedQty: number + acceptedWeight?: number + productionDate?: string + expiryDate: string +} export interface PurchaseQCInput { sampleRate: number; sampleWeight: number; totalWeight: number; qcCheck: PurchaseQcCheck[]; } +export interface EscalationInput { + handler: string + stockInLine: StockInLineEntry[] +} +export interface PutawayInput { + handler: string + stockInLine: StockInLineEntry[] +} + +export type ModalFormInput = Partial export const testFetch = cache(async (id: number) => { return serverFetchJson(`${BASE_API_URL}/po/detail/${id}`, { @@ -53,8 +70,8 @@ export const createStockInLine = async (data: StockInLineEntry) => { return stockInLine } -export const updateStockInLine = async (data: StockInLineEntry) => { - const stockInLine = await serverFetchJson>(`${BASE_API_URL}/stockInLine/update`, { +export const updateStockInLine = async (data: StockInLineEntry & ModalFormInput) => { + const stockInLine = await serverFetchJson>(`${BASE_API_URL}/stockInLine/update`, { method: "POST", body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, diff --git a/src/app/api/po/index.ts b/src/app/api/po/index.ts index 534bdb4..8c1e10b 100644 --- a/src/app/api/po/index.ts +++ b/src/app/api/po/index.ts @@ -20,6 +20,7 @@ export interface PurchaseOrderLine { itemNo: string itemName: string qty: number + uom?: string price: number status: string stockInLine: StockInLine[] diff --git a/src/app/utils/fetchUtil.ts b/src/app/utils/fetchUtil.ts index 227d858..f13f955 100644 --- a/src/app/utils/fetchUtil.ts +++ b/src/app/utils/fetchUtil.ts @@ -45,7 +45,8 @@ export const serverFetch: typeof fetch = async (input, init) => { ...(accessToken ? { Authorization: `Bearer ${accessToken}`, - + Accept: + "application/json, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, multipart/form-data", } : {}), }, diff --git a/src/app/utils/formatUtil.ts b/src/app/utils/formatUtil.ts index e73f663..733cc01 100644 --- a/src/app/utils/formatUtil.ts +++ b/src/app/utils/formatUtil.ts @@ -8,13 +8,14 @@ export const moneyFormatter = new Intl.NumberFormat("en-HK", { currency: "HKD", }); -export const stockInLineStatusMap: { [status: string]: number } = { - draft: 0, - pending: 1, - qc: 2, - determine1: 3, - determine2: 4, - determine3: 5, - receiving: 6, - completed: 7, +export const stockInLineStatusMap: { [status: string]: {key: string, value: number} } = { + draft: { key: "draft", value: 0 }, + pending: { key: "pending", value: 1 }, + qc: { key: "qc", value: 2 }, + determine1: { key: "determine1", value: 3 }, + determine2: { key: "determine2", value: 4 }, + determine3: { key: "determine3", value: 5 }, + receiving: { key: "receiving", value: 6 }, + received: { key: "received", value: 7 }, + completed: { key: "completed", value: 8 }, }; \ No newline at end of file diff --git a/src/components/PoDetail/EscalationForm.tsx b/src/components/PoDetail/EscalationForm.tsx new file mode 100644 index 0000000..1312013 --- /dev/null +++ b/src/components/PoDetail/EscalationForm.tsx @@ -0,0 +1,206 @@ +"use client"; + +import { StockInLineEntry, EscalationInput } 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 StockInLineEntry]?: string; + } + | undefined; + +type PoEscalationRow = TableRow, EntryError>; + +const EscalationForm: React.FC = ({ +// qc, + itemDetail, + }) => { + const { t } = useTranslation(); + const apiRef = useGridApiRef(); + const { + register, + formState: { errors, defaultValues, touchedFields }, + watch, + control, + setValue, + getValues, + reset, + resetField, + setError, + clearErrors, + } = useFormContext(); + console.log(itemDetail) + const columns = useMemo( + () => [ + // { + // field: "qcCheckId", + // headerName: "qc Check", + // flex: 1, + // editable: true, + // valueFormatter(params) { + // const row = params.id ? params.api.getRow(params.id) : null; + // if (!row) { + // return null; + // } + // const Qc = qc.find((q) => q.id === row.qcCheckId); + // return Qc ? `${Qc.code} - ${Qc.name}` : t("Please select QC"); + // }, + // renderCell(params: GridRenderCellParams) { + // console.log(params.value); + // return {params.formattedValue}; + // }, + // renderEditCell(params: GridRenderEditCellParams) { + // const errorMessage = + // params.row._error?.[params.field as keyof StockInLineEntry]; + // console.log(errorMessage); + // const content = ( + // { + // await params.api.setEditCellValue({ + // id: params.id, + // field: "qcCheckId", + // value: qcCheckId, + // }); + // }} + // /> + // ); + // return errorMessage ? ( + // + // {content} + // + // ) : ( + // content + // ); + // }, + // }, + { + field: "qty", + headerName: "qty", + flex: 1, + editable: true, + type: "number", + renderEditCell(params: GridRenderEditCellParams) { + const errorMessage = + params.row._error?.[params.field as keyof StockInLineEntry]; + const content = ; + return errorMessage ? ( + + {content} + + ) : ( + content + ); + }, + }, + ], + [] + ); + const validationTest = useCallback( + (newRow: GridRowModel): EntryError => { + const error: EntryError = {}; + // const { qcCheckId, qty } = newRow; + // if (!qcCheckId || qcCheckId <= 0) { + // error["qcCheckId"] = "select qc"; + // } + // if (!qty || qty <= 0) { + // error["qty"] = "enter a qty"; + // } + return Object.keys(error).length > 0 ? error : undefined; + }, + [] + ); + return ( + + + + {t("Qc Detail")} + + + + + + + + + + + + + + apiRef={apiRef} + checkboxSelection={false} + _formKey={"stockInLine"} + columns={columns} + validateRow={validationTest} + /> + + + + ); +}; +export default EscalationForm; diff --git a/src/components/PoDetail/PoDetail.tsx b/src/components/PoDetail/PoDetail.tsx index 00c78bd..42f1286 100644 --- a/src/components/PoDetail/PoDetail.tsx +++ b/src/components/PoDetail/PoDetail.tsx @@ -77,7 +77,7 @@ const PoDetail: React.FC = ({ {row.itemNo} {row.itemName} {row.qty} - {/* {row.uom} */} + {row.uom} {row.price} {/* {row.expiryDate} */} {row.status} @@ -133,6 +133,7 @@ const PoDetail: React.FC = ({ {t("itemNo")} {t("itemName")} {t("qty")} + {t("uom")} {t("price")} {/* {t("expiryDate")} */} {t("status")} diff --git a/src/components/PoDetail/PoInputGrid.tsx b/src/components/PoDetail/PoInputGrid.tsx index adf8492..dde5b0e 100644 --- a/src/components/PoDetail/PoInputGrid.tsx +++ b/src/components/PoDetail/PoInputGrid.tsx @@ -30,14 +30,14 @@ 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"; - +import PoQcStockInModal from "./PoQcStockInModal"; +import NotificationImportantIcon from '@mui/icons-material/NotificationImportant'; interface ResultWithId { id: number; } @@ -85,19 +85,23 @@ function PoInputGrid({ qc, setRows, itemDetail, stockInLine }: Props) { const [entries, setEntries] = useState(stockInLine || []); const [modalInfo, setModalInfo] = useState() const [qcOpen, setQcOpen] = useState(false); + const [escalOpen, setEscalOpen] = useState(false); + const [stockInOpen, setStockInOpen] = useState(false); + const [putAwayOpen, setPutAwayOpen] = useState(false); + const [type, setType] = useState<"qc" | "stockIn">("qc"); 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 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) => () => { @@ -127,7 +131,7 @@ function PoInputGrid({ qc, setRows, itemDetail, stockInLine }: Props) { } const res = await createStockInLine(postData) console.log(res) - // setEntries((prev) => prev.map((p) => p.id === oldId ? res.entity : p)) + setEntries((prev) => prev.map((p) => p.id === oldId ? res.entity as StockInLine : p)) // do post directly to test // openStartModal(); }, 200); @@ -149,14 +153,49 @@ function PoInputGrid({ qc, setRows, itemDetail, stockInLine }: Props) { }, [] ); + const handleEscalation = useCallback( + (id: GridRowId, params: any) => () => { + setRowModesModel((prev) => ({ + ...prev, + [id]: { mode: GridRowModes.View }, + })); + setModalInfo(params.row) + setTimeout(() => { + // open qc modal + console.log("delayed"); + openEscalationModal() + }, 200); + }, + [] + ); const handleStockIn = useCallback( - (id: GridRowId) => () => { + (id: GridRowId, params: any) => () => { setRowModesModel((prev) => ({ ...prev, [id]: { mode: GridRowModes.View }, })); + setModalInfo(params.row) setTimeout(() => { // open stock in modal + openStockInModal() + // return the record with its status as pending + // update layout + console.log("delayed"); + }, 200); + }, + [] + ); + + const handlePutAway = useCallback( + (id: GridRowId, params: any) => () => { + setRowModesModel((prev) => ({ + ...prev, + [id]: { mode: GridRowModes.View }, + })); + setModalInfo(params.row) + setTimeout(() => { + // open stock in modal + openPutAwayModal() // return the record with its status as pending // update layout console.log("delayed"); @@ -172,11 +211,32 @@ function PoInputGrid({ qc, setRows, itemDetail, stockInLine }: Props) { setQcOpen(true); }, []); + const closeStockInModal = useCallback(() => { + setStockInOpen(false); + }, []); + const openStockInModal = useCallback(() => { + setStockInOpen(true); + }, []); + + const closePutAwayModal = useCallback(() => { + setPutAwayOpen(false); + }, []); + const openPutAwayModal = useCallback(() => { + setPutAwayOpen(true); + }, []); + + const closeEscalationModal = useCallback(() => { + setEscalOpen(false); + }, []); + const openEscalationModal = useCallback(() => { + setEscalOpen(true); + }, []); + const columns = useMemo( () => [ { field: "itemNo", - flex: 1, + flex: .8, }, { field: "itemName", @@ -198,20 +258,10 @@ function PoInputGrid({ qc, setRows, itemDetail, stockInLine }: Props) { { field: "actions", type: "actions", - headerName: "start | qc | stock in | delete", - flex: 1, + headerName: "start | qc | escalate | stock in | putaway | delete", + flex: 1.5, 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 [ @@ -220,8 +270,9 @@ function PoInputGrid({ qc, setRows, itemDetail, stockInLine }: Props) { label="start" sx={{ color: "primary.main", + marginRight: 2 }} - disabled={!(stockInLineStatusMap[status] === 0)} + disabled={!(stockInLineStatusMap[status].value === 0)} // set _isNew to false after posting // or check status onClick={handleStart(params.row.id, params)} @@ -233,24 +284,54 @@ function PoInputGrid({ qc, setRows, itemDetail, stockInLine }: Props) { label="qc" sx={{ color: "primary.main", + marginRight: 2 }} - disabled={stockInLineStatusMap[status] <= 0 || stockInLineStatusMap[status] >= 6} + disabled={stockInLineStatusMap[status].value <= 0 || stockInLineStatusMap[status].value >= 5} // set _isNew to false after posting // or check status onClick={handleQC(params.row.id, params)} color="inherit" key="edit" />, + } + label="escalation" + sx={{ + color: "primary.main", + marginRight: 2 + }} + disabled={stockInLineStatusMap[status].value <= 0 || stockInLineStatusMap[status].value >= 5} + // set _isNew to false after posting + // or check status + onClick={handleEscalation(params.row.id, params)} + color="inherit" + key="edit" + />, } label="stockin" sx={{ color: "primary.main", + marginRight: 2 + }} + disabled={stockInLineStatusMap[status].value !== 6} + // set _isNew to false after posting + // or check status + onClick={handleStockIn(params.row.id, params)} + color="inherit" + key="edit" + />, + } + label="putaway" + sx={{ + color: "primary.main", + marginRight: 2 }} - disabled={stockInLineStatusMap[status] !== 6} + disabled={stockInLineStatusMap[status].value !== 7} // set _isNew to false after posting // or check status - onClick={handleStockIn(params.row.id)} + onClick={handlePutAway(params.row.id, params)} color="inherit" key="edit" />, @@ -260,7 +341,7 @@ function PoInputGrid({ qc, setRows, itemDetail, stockInLine }: Props) { sx={{ color: "error.main", }} - disabled={stockInLineStatusMap[status] !== 0} + disabled={stockInLineStatusMap[status].value !== 0} // disabled={Boolean(params.row.status)} onClick={handleDelete(params.row.id)} color="inherit" @@ -404,7 +485,8 @@ function PoInputGrid({ qc, setRows, itemDetail, stockInLine }: Props) { }} /> <> - + <> + + + <> + + + <> + + ); } diff --git a/src/components/PoDetail/PoQcModal.tsx b/src/components/PoDetail/PoQcStockInModal.tsx similarity index 50% rename from src/components/PoDetail/PoQcModal.tsx rename to src/components/PoDetail/PoQcStockInModal.tsx index 71fdd3d..d014d1c 100644 --- a/src/components/PoDetail/PoQcModal.tsx +++ b/src/components/PoDetail/PoQcStockInModal.tsx @@ -1,8 +1,8 @@ "use client"; -import { PurchaseQCInput, StockInLineEntry, updateStockInLine } from "@/app/api/po/actions"; +import { ModalFormInput, PurchaseQCInput, StockInInput, 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 { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import QcForm from "./QcForm"; @@ -12,24 +12,36 @@ import { StockInLine } from "@/app/api/po"; import { useSearchParams } from "next/navigation"; import { stockInLineStatusMap } from "@/app/utils/formatUtil"; import { StockInLineRow } from "./PoInputGrid"; -// type: - +import EscalationForm from "./EscalationForm"; +import StockInForm from "./StockInForm"; interface CommonProps extends Omit { setEntries: Dispatch> itemDetail: StockInLine; qc?: QcItemWithChecks[]; warehouse?: any[]; + type: "qc" | "stockIn" | "escalation" | "putaway" } interface QcProps extends CommonProps { qc: QcItemWithChecks[]; + type: "qc" } interface StockInProps extends CommonProps { // naming + type: "stockIn" +} +interface PutawayProps extends CommonProps { + // naming + // warehouse: any[]; warehouse: any[]; + type: "putaway" +} +interface EscalationProps extends CommonProps { + // naming + type: "escalation" } -type Props = QcProps | StockInProps; +type Props = QcProps | StockInProps | EscalationProps | PutawayProps; const style = { position: "absolute", @@ -42,7 +54,8 @@ const style = { pb: 10, width: { xs: "80%", sm: "80%", md: "80%" }, }; -const PoQcModal: React.FC = ({ +const PoQcStockInModal: React.FC = ({ + type, setEntries, open, onClose, @@ -56,7 +69,7 @@ const PoQcModal: React.FC = ({ const params = useSearchParams() console.log(params.get("id")) const [defaultValues, setDefaultValues] = useState({}); - const formProps = useForm({ + const formProps = useForm({ defaultValues: defaultValues ? defaultValues : {}, }); const errors = formProps.formState.errors; @@ -72,32 +85,84 @@ const PoQcModal: React.FC = ({ setDefaultValues({}); }, []); - const onSubmit = useCallback>( + // status to be posted + const getPostingStatus = useCallback( + (type: string) => { + switch (type) { + case "qc": + return stockInLineStatusMap.receiving.key; + case "stockIn": + return stockInLineStatusMap.received.key; + case "putaway": + return stockInLineStatusMap.completed.key; + default: + return stockInLineStatusMap.pending.key; + } + }, [] + ) + + const onSubmit = useCallback>( async (data, event) => { let hasErrors = false; console.log(errors); console.log(data); console.log(itemDetail); try { - if (hasErrors) { - setServerError(t("An error has occurred. Please try again later.")); - return false; + console.log(type) + var status = getPostingStatus(type) + // if escalation, take data.status as status + console.log(status) + // add checking + // const qty = data.sampleRate + + //////////////////////// modify this mess later ////////////////////// + var productionDate = null + var acceptedQty = null + if (data.productionDate && data.productionDate.length > 0) { + productionDate = data.productionDate + } + if (data.acceptedQty) { + acceptedQty = parseInt(data.acceptedQty.toString()) + } else { + acceptedQty = data.sampleRate } - // do post update stock in line - // const reqStatus = stockInLineStatusMap - const args: StockInLineEntry = { + const args = { id: itemDetail.id, purchaseOrderId: parseInt(params.get("id")!!), purchaseOrderLineId: itemDetail.purchaseOrderLineId, itemId: itemDetail.itemId, - acceptedQty: itemDetail.acceptedQty, - status: "receiving", - } + ...data, + acceptedQty: acceptedQty, + productionDate: productionDate, + status: status, + } as StockInLineEntry & ModalFormInput; + ////////////////////////////////////////////////////////////////////// console.log(args) + // return + if (hasErrors) { + setServerError(t("An error has occurred. Please try again later.")); + return false; + } const res = await updateStockInLine(args) - // this.res.entity = list of entity - for (const inLine in res.entity as StockInLine[]) { - + if (Boolean(res.id)) { + // set entries + const newEntries = res.entity as StockInLine[] + setEntries((prev) => { + const updatedEntries = [...prev]; // Create a new array + newEntries.forEach((item) => { + const index = updatedEntries.findIndex(p => p.id === item.id); + if (index !== -1) { + // Update existing item + updatedEntries[index] = item; + } else { + // Add new item + updatedEntries.push(item); + } + }); + return updatedEntries; // Return the new array + }) + // add loading + closeHandler({}, "backdropClick") } console.log(res) @@ -110,7 +175,7 @@ const PoQcModal: React.FC = ({ }, [t, itemDetail] ); - + return ( <> @@ -120,7 +185,9 @@ const PoQcModal: React.FC = ({ component="form" onSubmit={formProps.handleSubmit(onSubmit)} > - {qc && } + {type === "qc" && } + {type === "stockIn" && } + {type === "escalation" && }