@@ -16,7 +16,6 @@ export interface PostStockInLiineResponse<T> { | |||
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<PurchaseQCInput & StockInInput & EscalationInput & PutawayInput> | |||
export const testFetch = cache(async (id: number) => { | |||
return serverFetchJson<PoResult>(`${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<PostStockInLiineResponse<StockInLineEntry>>(`${BASE_API_URL}/stockInLine/update`, { | |||
export const updateStockInLine = async (data: StockInLineEntry & ModalFormInput) => { | |||
const stockInLine = await serverFetchJson<PostStockInLiineResponse<StockInLineEntry & ModalFormInput>>(`${BASE_API_URL}/stockInLine/update`, { | |||
method: "POST", | |||
body: JSON.stringify(data), | |||
headers: { "Content-Type": "application/json" }, | |||
@@ -20,6 +20,7 @@ export interface PurchaseOrderLine { | |||
itemNo: string | |||
itemName: string | |||
qty: number | |||
uom?: string | |||
price: number | |||
status: string | |||
stockInLine: StockInLine[] | |||
@@ -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", | |||
} | |||
: {}), | |||
}, | |||
@@ -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 }, | |||
}; |
@@ -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<Partial<StockInLineEntry>, EntryError>; | |||
const EscalationForm: 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<EscalationInput>(); | |||
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<PoEscalationRow>(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<PoEscalationRow, number>) { | |||
// console.log(params.value); | |||
// return <TwoLineCell>{params.formattedValue}</TwoLineCell>; | |||
// }, | |||
// renderEditCell(params: GridRenderEditCellParams<PoEscalationRow, number>) { | |||
// const errorMessage = | |||
// params.row._error?.[params.field as keyof StockInLineEntry]; | |||
// 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<PoEscalationRow>) { | |||
const errorMessage = | |||
params.row._error?.[params.field as keyof StockInLineEntry]; | |||
const content = <GridEditInputCell {...params} />; | |||
return errorMessage ? ( | |||
<Tooltip title={t(errorMessage)}> | |||
<Box width="100%">{content}</Box> | |||
</Tooltip> | |||
) : ( | |||
content | |||
); | |||
}, | |||
}, | |||
], | |||
[] | |||
); | |||
const validationTest = useCallback( | |||
(newRow: GridRowModel<PoEscalationRow>): 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("handler")} | |||
fullWidth | |||
{...register("handler", { | |||
required: "handler required!", | |||
})} | |||
error={Boolean(errors.handler)} | |||
helperText={errors.handler?.message} | |||
/> | |||
</Grid> | |||
<Grid item xs={4}> | |||
<TextField | |||
label={t("total")} | |||
fullWidth | |||
// {...register("handler", { | |||
// required: "handler required!", | |||
// })} | |||
value={itemDetail.acceptedQty} | |||
disabled | |||
// error={Boolean(errors.handler)} | |||
// helperText={errors.handler?.message} | |||
/> | |||
</Grid> | |||
</Grid> | |||
<Grid | |||
container | |||
justifyContent="flex-start" | |||
alignItems="flex-start" | |||
spacing={2} | |||
sx={{ mt: 0.5 }} | |||
> | |||
<Grid item xs={12}> | |||
<InputDataGrid<EscalationInput, StockInLineEntry, EntryError> | |||
apiRef={apiRef} | |||
checkboxSelection={false} | |||
_formKey={"stockInLine"} | |||
columns={columns} | |||
validateRow={validationTest} | |||
/> | |||
</Grid> | |||
</Grid> | |||
</Grid> | |||
); | |||
}; | |||
export default EscalationForm; |
@@ -77,7 +77,7 @@ const PoDetail: React.FC<Props> = ({ | |||
<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.uom}</TableCell> | |||
<TableCell align="left">{row.price}</TableCell> | |||
{/* <TableCell align="left">{row.expiryDate}</TableCell> */} | |||
<TableCell align="left">{row.status}</TableCell> | |||
@@ -133,6 +133,7 @@ const PoDetail: React.FC<Props> = ({ | |||
<TableCell>{t("itemNo")}</TableCell> | |||
<TableCell align="left">{t("itemName")}</TableCell> | |||
<TableCell align="left">{t("qty")}</TableCell> | |||
<TableCell align="left">{t("uom")}</TableCell> | |||
<TableCell align="left">{t("price")}</TableCell> | |||
{/* <TableCell align="left">{t("expiryDate")}</TableCell> */} | |||
<TableCell align="left">{t("status")}</TableCell> | |||
@@ -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<StockInLineRow[]>(stockInLine || []); | |||
const [modalInfo, setModalInfo] = useState<StockInLine>() | |||
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<GridColDef[]>( | |||
() => [ | |||
{ | |||
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" | |||
/>, | |||
<GridActionsCellItem | |||
icon={<NotificationImportantIcon />} | |||
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" | |||
/>, | |||
<GridActionsCellItem | |||
icon={<ShoppingCartIcon />} | |||
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" | |||
/>, | |||
<GridActionsCellItem | |||
icon={<ShoppingCartIcon />} | |||
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) { | |||
}} | |||
/> | |||
<> | |||
<PoQcModal | |||
<PoQcStockInModal | |||
type={"qc"} | |||
setEntries={setEntries} | |||
qc={qc} | |||
open={qcOpen} | |||
@@ -412,6 +494,36 @@ function PoInputGrid({ qc, setRows, itemDetail, stockInLine }: Props) { | |||
itemDetail={modalInfo!!} | |||
/> | |||
</> | |||
<> | |||
<PoQcStockInModal | |||
type={"escalation"} | |||
setEntries={setEntries} | |||
// qc={qc} | |||
open={escalOpen} | |||
onClose={closeEscalationModal} | |||
itemDetail={modalInfo!!} | |||
/> | |||
</> | |||
<> | |||
<PoQcStockInModal | |||
type={"stockIn"} | |||
setEntries={setEntries} | |||
// qc={qc} | |||
open={stockInOpen} | |||
onClose={closeStockInModal} | |||
itemDetail={modalInfo!!} | |||
/> | |||
</> | |||
<> | |||
<PoQcStockInModal | |||
type={"putaway"} | |||
setEntries={setEntries} | |||
open={putAwayOpen} | |||
warehouse={[]} | |||
onClose={closePutAwayModal} | |||
itemDetail={modalInfo!!} | |||
/> | |||
</> | |||
</> | |||
); | |||
} | |||
@@ -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<ModalProps, "children"> { | |||
setEntries: Dispatch<SetStateAction<StockInLineRow[]>> | |||
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<Props> = ({ | |||
const PoQcStockInModal: React.FC<Props> = ({ | |||
type, | |||
setEntries, | |||
open, | |||
onClose, | |||
@@ -56,7 +69,7 @@ const PoQcModal: React.FC<Props> = ({ | |||
const params = useSearchParams() | |||
console.log(params.get("id")) | |||
const [defaultValues, setDefaultValues] = useState({}); | |||
const formProps = useForm<PurchaseQCInput>({ | |||
const formProps = useForm<ModalFormInput>({ | |||
defaultValues: defaultValues ? defaultValues : {}, | |||
}); | |||
const errors = formProps.formState.errors; | |||
@@ -72,32 +85,84 @@ const PoQcModal: React.FC<Props> = ({ | |||
setDefaultValues({}); | |||
}, []); | |||
const onSubmit = useCallback<SubmitHandler<PurchaseQCInput & {}>>( | |||
// 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<SubmitHandler<ModalFormInput & {}>>( | |||
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<Props> = ({ | |||
}, | |||
[t, itemDetail] | |||
); | |||
return ( | |||
<> | |||
<Modal open={open} onClose={closeHandler}> | |||
@@ -120,7 +185,9 @@ const PoQcModal: React.FC<Props> = ({ | |||
component="form" | |||
onSubmit={formProps.handleSubmit(onSubmit)} | |||
> | |||
{qc && <QcForm qc={qc} itemDetail={itemDetail} />} | |||
{type === "qc" && <QcForm qc={qc} itemDetail={itemDetail} />} | |||
{type === "stockIn" && <StockInForm itemDetail={itemDetail} />} | |||
{type === "escalation" && <EscalationForm itemDetail={itemDetail} />} | |||
<Stack direction="row" justifyContent="flex-end" gap={1}> | |||
<Button | |||
name="submit" | |||
@@ -138,4 +205,4 @@ const PoQcModal: React.FC<Props> = ({ | |||
</> | |||
); | |||
}; | |||
export default PoQcModal; | |||
export default PoQcStockInModal; |
@@ -0,0 +1,249 @@ | |||
"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, useState } 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 PutawayForm: 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 [recordQty, setRecordQty] = useState(0) | |||
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 recordQty = params.row.qty | |||
// if (recordQty !== undefined) { | |||
// setUnrecordQty((prev) => prev - recordQty) | |||
// } | |||
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 validation = 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"; | |||
} | |||
if (qty && qty > itemDetail.acceptedQty) { | |||
error["qty"] = "qty too big"; | |||
} | |||
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={6}> | |||
<TextField | |||
label={t("Total qty")} | |||
fullWidth | |||
value={itemDetail.acceptedQty} | |||
disabled | |||
// {...register("sampleRate", { | |||
// required: "sampleRate required!", | |||
// })} | |||
// error={Boolean(errors.sampleRate)} | |||
// helperText={errors.sampleRate?.message} | |||
/> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<TextField | |||
label={t("Total record qty")} | |||
fullWidth | |||
value={recordQty} | |||
disabled | |||
// {...register("sampleRate", { | |||
// required: "sampleRate required!", | |||
// })} | |||
// error={Boolean(errors.sampleRate)} | |||
// helperText={errors.sampleRate?.message} | |||
/> | |||
</Grid> | |||
<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={validation} | |||
/> | |||
</Grid> | |||
</Grid> | |||
</Grid> | |||
); | |||
}; | |||
export default PutawayForm; |
@@ -14,7 +14,7 @@ import { | |||
import { useFormContext } from "react-hook-form"; | |||
import { useTranslation } from "react-i18next"; | |||
import StyledDataGrid from "../StyledDataGrid"; | |||
import { useCallback, useMemo } from "react"; | |||
import { useCallback, useMemo, useState } from "react"; | |||
import { | |||
GridColDef, | |||
GridRowIdGetter, | |||
@@ -63,6 +63,7 @@ const QcForm: React.FC<Props> = ({ | |||
clearErrors, | |||
} = useFormContext<PurchaseQCInput>(); | |||
console.log(itemDetail) | |||
const [recordQty, setRecordQty] = useState(0) | |||
const columns = useMemo<GridColDef[]>( | |||
() => [ | |||
{ | |||
@@ -115,6 +116,10 @@ const QcForm: React.FC<Props> = ({ | |||
editable: true, | |||
type: "number", | |||
renderEditCell(params: GridRenderEditCellParams<PoQcRow>) { | |||
// const recordQty = params.row.qty | |||
// if (recordQty !== undefined) { | |||
// setUnrecordQty((prev) => prev - recordQty) | |||
// } | |||
const errorMessage = | |||
params.row._error?.[params.field as keyof PurchaseQcCheck]; | |||
const content = <GridEditInputCell {...params} />; | |||
@@ -130,7 +135,7 @@ const QcForm: React.FC<Props> = ({ | |||
], | |||
[] | |||
); | |||
const validationTest = useCallback( | |||
const validation = useCallback( | |||
(newRow: GridRowModel<PoQcRow>): EntryError => { | |||
const error: EntryError = {}; | |||
const { qcCheckId, qty } = newRow; | |||
@@ -140,6 +145,9 @@ const QcForm: React.FC<Props> = ({ | |||
if (!qty || qty <= 0) { | |||
error["qty"] = "enter a qty"; | |||
} | |||
if (qty && qty > itemDetail.acceptedQty) { | |||
error["qty"] = "qty too big"; | |||
} | |||
return Object.keys(error).length > 0 ? error : undefined; | |||
}, | |||
[] | |||
@@ -158,6 +166,32 @@ const QcForm: React.FC<Props> = ({ | |||
spacing={2} | |||
sx={{ mt: 0.5 }} | |||
> | |||
<Grid item xs={6}> | |||
<TextField | |||
label={t("Total qty")} | |||
fullWidth | |||
value={itemDetail.acceptedQty} | |||
disabled | |||
// {...register("sampleRate", { | |||
// required: "sampleRate required!", | |||
// })} | |||
// error={Boolean(errors.sampleRate)} | |||
// helperText={errors.sampleRate?.message} | |||
/> | |||
</Grid> | |||
<Grid item xs={6}> | |||
<TextField | |||
label={t("Total record qty")} | |||
fullWidth | |||
value={recordQty} | |||
disabled | |||
// {...register("sampleRate", { | |||
// required: "sampleRate required!", | |||
// })} | |||
// error={Boolean(errors.sampleRate)} | |||
// helperText={errors.sampleRate?.message} | |||
/> | |||
</Grid> | |||
<Grid item xs={4}> | |||
<TextField | |||
label={t("sampleRate")} | |||
@@ -205,7 +239,7 @@ const QcForm: React.FC<Props> = ({ | |||
checkboxSelection={false} | |||
_formKey={"qcCheck"} | |||
columns={columns} | |||
validateRow={validationTest} | |||
validateRow={validation} | |||
/> | |||
</Grid> | |||
</Grid> | |||
@@ -0,0 +1,168 @@ | |||
"use client"; | |||
import { PurchaseQcCheck, PurchaseQCInput, StockInInput } 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"; | |||
// change PurchaseQcCheck to stock in entry props | |||
interface Props { | |||
itemDetail: StockInLine; | |||
// qc: QcItemWithChecks[]; | |||
} | |||
type EntryError = | |||
| { | |||
[field in keyof StockInInput]?: string; | |||
} | |||
| undefined; | |||
// type PoQcRow = TableRow<Partial<PurchaseQcCheck>, EntryError>; | |||
const StockInForm: 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<StockInInput>(); | |||
console.log(itemDetail) | |||
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("productLotNo")} | |||
fullWidth | |||
{...register("productLotNo", { | |||
// required: "productLotNo required!", | |||
})} | |||
// error={Boolean(errors.productLotNo)} | |||
// helperText={errors.productLotNo?.message} | |||
/> | |||
</Grid> | |||
<Grid item xs={4}> | |||
<TextField | |||
label={t("receiptDate")} | |||
fullWidth | |||
{...register("receiptDate", { | |||
required: "receiptDate required!", | |||
})} | |||
error={Boolean(errors.receiptDate)} | |||
helperText={errors.receiptDate?.message} | |||
/> | |||
</Grid> | |||
<Grid item xs={4}> | |||
<TextField | |||
label={t("acceptedQty")} | |||
fullWidth | |||
{...register("acceptedQty", { | |||
required: "acceptedQty required!", | |||
})} | |||
error={Boolean(errors.acceptedQty)} | |||
helperText={errors.acceptedQty?.message} | |||
/> | |||
</Grid> | |||
<Grid item xs={4}> | |||
<TextField | |||
label={t("acceptedWeight")} | |||
fullWidth | |||
// {...register("acceptedWeight", { | |||
// required: "acceptedWeight required!", | |||
// })} | |||
error={Boolean(errors.acceptedWeight)} | |||
helperText={errors.acceptedWeight?.message} | |||
/> | |||
</Grid> | |||
<Grid item xs={4}> | |||
<TextField | |||
label={t("productionDate")} | |||
fullWidth | |||
{...register("productionDate", { | |||
// required: "productionDate required!", | |||
})} | |||
// error={Boolean(errors.productionDate)} | |||
// helperText={errors.productionDate?.message} | |||
/> | |||
</Grid> | |||
<Grid item xs={4}> | |||
<TextField | |||
label={t("expiryDate")} | |||
fullWidth | |||
{...register("expiryDate", { | |||
required: "expiryDate required!", | |||
})} | |||
error={Boolean(errors.expiryDate)} | |||
helperText={errors.expiryDate?.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 StockInForm; |