| @@ -13,7 +13,7 @@ import { UploadProvider } from "@/components/UploadProvider/UploadProvider"; | |||
| import SessionProviderWrapper from "@/components/SessionProviderWrapper/SessionProviderWrapper"; | |||
| import QrCodeScannerProvider from "@/components/QrCodeScannerProvider/QrCodeScannerProvider"; | |||
| import { I18nProvider } from "@/i18n"; | |||
| import "src/app/global.css" | |||
| export default async function MainLayout({ | |||
| children, | |||
| }: { | |||
| @@ -39,6 +39,7 @@ export interface InventoryLotLineResult { | |||
| uom: string; | |||
| qtyPerSmallestUnit: number; | |||
| baseUom: string; | |||
| stockInLineId: number | |||
| } | |||
| export interface InventoryLotLineItem { | |||
| @@ -38,8 +38,10 @@ export interface PurchaseQcResult { | |||
| } | |||
| export interface StockInInput { | |||
| status: string; | |||
| poCode: string; | |||
| productLotNo?: string; | |||
| dnNo?: string; | |||
| itemName: string; | |||
| invoiceNo?: string; | |||
| receiptDate: string; | |||
| acceptedQty: number; | |||
| @@ -1,3 +1,7 @@ | |||
| @tailwind components; | |||
| @tailwind utilities; | |||
| @tailwind utilities; | |||
| html, body { | |||
| overscroll-behavior: none; | |||
| } | |||
| @@ -160,6 +160,7 @@ const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit, type, onReleaseClick | |||
| type: "input-number", | |||
| style: { | |||
| textAlign: "right", | |||
| // width: "100px", | |||
| }, | |||
| renderCell: (row) => { | |||
| if (typeof row.demandQty == "number") { | |||
| @@ -174,6 +175,7 @@ const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit, type, onReleaseClick | |||
| type: "read-only", | |||
| style: { | |||
| textAlign: "left", | |||
| // width: "100px", | |||
| }, | |||
| renderCell: (row) => { | |||
| return row.uomName; | |||
| @@ -185,6 +187,7 @@ const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit, type, onReleaseClick | |||
| type: "read-only", | |||
| style: { | |||
| textAlign: "right", | |||
| // width: "100px", | |||
| }, | |||
| renderCell: (row) => { | |||
| return <ProdTimeColumn prodTimeInMinute={row.prodTimeInMinute} /> | |||
| @@ -196,6 +199,7 @@ const ViewByFGDetails: React.FC<Props> = ({ apiRef, isEdit, type, onReleaseClick | |||
| type: "read-only", | |||
| style: { | |||
| textAlign: "right", | |||
| // width: "100px", | |||
| }, | |||
| // editable: true, | |||
| }, | |||
| @@ -84,9 +84,15 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||
| const [rows, setRows] = useState<PurchaseOrderLine[]>( | |||
| purchaseOrder.pol || [], | |||
| ); | |||
| const params = useSearchParams(); | |||
| const searchParams = useSearchParams(); | |||
| // const [currPoStatus, setCurrPoStatus] = useState(purchaseOrder.status); | |||
| const removeParam = (paramToRemove: string) => { | |||
| const newParams = new URLSearchParams(searchParams.toString()); | |||
| newParams.delete(paramToRemove); | |||
| window.history.replaceState({}, '', `${window.location.pathname}?${newParams}`); | |||
| }; | |||
| const handleCompletePo = useCallback(async () => { | |||
| const checkRes = await checkPolAndCompletePo(purchaseOrder.id); | |||
| console.log(checkRes); | |||
| @@ -107,6 +113,8 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||
| function Row(props: { row: PurchaseOrderLine }) { | |||
| const { row } = props; | |||
| const [firstReceiveQty, setFirstReceiveQty] = useState<number>() | |||
| const [secondReceiveQty, setSecondReceiveQty] = useState<number>() | |||
| const [open, setOpen] = useState(false); | |||
| const [processedQty, setProcessedQty] = useState(row.processed); | |||
| const [currStatus, setCurrStatus] = useState(row.status); | |||
| @@ -155,11 +163,30 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||
| <TableCell align="left">{row.price}</TableCell> | |||
| {/* <TableCell align="left">{row.expiryDate}</TableCell> */} | |||
| <TableCell align="left">{t(`${currStatus.toLowerCase()}`)}</TableCell> | |||
| {/* <TableCell align="left"> | |||
| 0 | |||
| </TableCell> | |||
| <TableCell align="left"> | |||
| <TextField | |||
| label="輸入數量" | |||
| type="text" // Use type="text" to allow validation in the change handler | |||
| variant="outlined" | |||
| value={secondReceiveQty} | |||
| // onChange={handleChange} | |||
| InputProps={{ | |||
| inputProps: { | |||
| min: 0, // Optional: set a minimum value | |||
| step: 1 // Optional: set the step for the number input | |||
| } | |||
| }} | |||
| /> | |||
| </TableCell> */} | |||
| </TableRow> | |||
| <TableRow> | |||
| {/* <TableCell /> */} | |||
| <TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={12}> | |||
| <Collapse in={open} timeout="auto" unmountOnExit> | |||
| <Collapse in={true} timeout="auto" unmountOnExit> | |||
| {/* <Collapse in={open} timeout="auto" unmountOnExit> */} | |||
| <Table> | |||
| <TableBody> | |||
| <TableRow> | |||
| @@ -260,7 +287,21 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||
| } | |||
| }, [purchaseOrder.status, t, handleStartPo, handleCompletePo]); | |||
| console.log(window.innerWidth) | |||
| const FIRST_IN_FIELD = "firstInQty" | |||
| const SECOND_IN_FIELD = "secondInQty" | |||
| const renderFieldCondition = useCallback((field: "firstInQty" | "secondInQty"): boolean => { | |||
| switch (field) { | |||
| case FIRST_IN_FIELD: | |||
| return true; | |||
| case SECOND_IN_FIELD: | |||
| return true; | |||
| default: | |||
| return false; // Default case | |||
| } | |||
| }, []); | |||
| return ( | |||
| <> | |||
| <Stack | |||
| @@ -277,18 +318,56 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||
| </Typography> | |||
| </Grid> | |||
| </Grid> | |||
| <Grid container xs={12} justifyContent="start"> | |||
| <Grid item> | |||
| <Button | |||
| {false ? (<Grid container xs={12} justifyContent="start"> | |||
| <Grid item xs={3}> | |||
| <TextField | |||
| label={t("dnNo")} | |||
| type="text" // Use type="text" to allow validation in the change handler | |||
| variant="outlined" | |||
| // value={secondReceiveQty} | |||
| // onChange={handleChange} | |||
| InputProps={{ | |||
| inputProps: { | |||
| min: 0, // Optional: set a minimum value | |||
| step: 1 // Optional: set the step for the number input | |||
| } | |||
| }} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={3}> | |||
| <TextField | |||
| label={t("dnDate")} | |||
| type="text" // Use type="text" to allow validation in the change handler | |||
| variant="outlined" | |||
| defaultValue={"07/08/2025"} | |||
| // value={secondReceiveQty} | |||
| // onChange={handleChange} | |||
| InputProps={{ | |||
| inputProps: { | |||
| min: 0, // Optional: set a minimum value | |||
| step: 1 // Optional: set the step for the number input | |||
| } | |||
| }} | |||
| /> | |||
| {/* <Button | |||
| onClick={buttonData.onClick} | |||
| disabled={buttonData.disabled} | |||
| color={buttonData.buttonColor as ButtonProps["color"]} | |||
| startIcon={buttonData.buttonIcon} | |||
| > | |||
| {buttonData.buttonText} | |||
| </Button> | |||
| </Button> */} | |||
| </Grid> | |||
| </Grid> | |||
| <Grid | |||
| item | |||
| xs={6} | |||
| display="flex" | |||
| justifyContent="end" | |||
| alignItems="end" | |||
| > | |||
| <Button onClick={onOpenScanner}>{t("Accept submit")}</Button> | |||
| </Grid> | |||
| </Grid>) : undefined} | |||
| <Grid container xs={12} justifyContent="space-between"> | |||
| <Grid item xs={8}> | |||
| <Tabs | |||
| @@ -296,7 +375,7 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||
| onChange={handleTabChange} | |||
| variant="scrollable" | |||
| > | |||
| <Tab label={t("General")} iconPosition="end" /> | |||
| {/* <Tab label={t("General")} iconPosition="end" /> */} | |||
| {/* <Tab label={t("Bind Storage")} iconPosition="end" /> */} | |||
| </Tabs> | |||
| </Grid> | |||
| @@ -315,7 +394,8 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||
| onClose={onCloseScanner} | |||
| warehouse={warehouse} | |||
| /> | |||
| <Button onClick={onOpenScanner}>{t("bind")}</Button> | |||
| {/* <Button onClick={onOpenScanner}>{t("Accept submit")}</Button> */} | |||
| {/* <Button onClick={onOpenScanner}>{t("bind")}</Button> */} | |||
| </Grid> | |||
| </Grid> | |||
| {/* tab 1 */} | |||
| @@ -336,6 +416,10 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||
| <TableCell align="left">{t("price")}</TableCell> | |||
| {/* <TableCell align="left">{t("expiryDate")}</TableCell> */} | |||
| <TableCell align="left">{t("status")}</TableCell> | |||
| {/* start == true && firstInQty == null ? no hide : hide*/} | |||
| {/* {renderFieldCondition(FIRST_IN_FIELD) ? <TableCell align="left">{t("receivedQty")}</TableCell> : undefined} */} | |||
| {/* start == true && firstInQty == null ? hide and disabled : no hide*/} | |||
| {/* {renderFieldCondition(SECOND_IN_FIELD) ? <TableCell align="left">{t("dnQty")}</TableCell> : undefined} */} | |||
| {/* <TableCell align="left">{"add icon button"}</TableCell> */} | |||
| </TableRow> | |||
| </TableHead> | |||
| @@ -57,6 +57,7 @@ import { fetchQcResult } from "@/app/api/qc/actions"; | |||
| import PoQcStockInModal from "./PoQcStockInModal"; | |||
| import DoDisturbIcon from "@mui/icons-material/DoDisturb"; | |||
| import { useSession } from "next-auth/react"; | |||
| import PoQcStockInModalVer2 from "./QcStockInModalVer2"; | |||
| interface ResultWithId { | |||
| id: number; | |||
| @@ -255,6 +256,40 @@ function PoInputGrid({ | |||
| }, | |||
| [fetchQcDefaultValue, openQcModal], | |||
| ); | |||
| const [newOpen, setNewOpen] = useState(false); | |||
| const closeNewModal = useCallback(() => { | |||
| setNewOpen(false); | |||
| }, []); | |||
| const openNewModal = useCallback(() => { | |||
| setNewOpen(true); | |||
| }, []); | |||
| const handleNewQC = useCallback( | |||
| (id: GridRowId, params: any) => async () => { | |||
| setBtnIsLoading(true); | |||
| setRowModesModel((prev) => ({ | |||
| ...prev, | |||
| [id]: { mode: GridRowModes.View }, | |||
| })); | |||
| const qcResult = await fetchQcDefaultValue(id); | |||
| console.log(params.row); | |||
| console.log(qcResult); | |||
| setModalInfo({ | |||
| ...params.row, | |||
| qcResult: qcResult, | |||
| }); | |||
| // set default values | |||
| setTimeout(() => { | |||
| // open qc modal | |||
| console.log("delayed"); | |||
| openNewModal(); | |||
| setBtnIsLoading(false); | |||
| }, 200); | |||
| }, | |||
| [fetchQcDefaultValue, openNewModal], | |||
| ); | |||
| const handleEscalation = useCallback( | |||
| (id: GridRowId, params: any) => () => { | |||
| // setBtnIsLoading(true); | |||
| @@ -373,20 +408,38 @@ function PoInputGrid({ | |||
| { | |||
| field: "itemNo", | |||
| headerName: t("itemNo"), | |||
| width: 120, | |||
| width: 100, | |||
| // flex: 0.4, | |||
| }, | |||
| { | |||
| field: "dnNo", | |||
| headerName: t("dnNo"), | |||
| width: 100, | |||
| renderCell: () => { | |||
| return <>DN0000001</> | |||
| } | |||
| // flex: 0.4, | |||
| }, | |||
| { | |||
| field: "dnDate", | |||
| headerName: t("dnDate"), | |||
| width: 100, | |||
| renderCell: () => { | |||
| return <>07/08/2025</> | |||
| } | |||
| // flex: 0.4, | |||
| }, | |||
| { | |||
| field: "itemName", | |||
| headerName: t("itemName"), | |||
| width: 120, | |||
| width: 100, | |||
| // flex: 0.6, | |||
| }, | |||
| { | |||
| field: "acceptedQty", | |||
| headerName: t("acceptedQty"), | |||
| // flex: 0.5, | |||
| width: 120, | |||
| width: 100, | |||
| type: "number", | |||
| // editable: true, | |||
| // replace with tooltip + content | |||
| @@ -417,7 +470,7 @@ function PoInputGrid({ | |||
| { | |||
| field: "status", | |||
| headerName: t("status"), | |||
| width: 120, | |||
| width: 70, | |||
| // flex: 0.5, | |||
| renderCell: (params) => { | |||
| return t(`${params.row.status}`); | |||
| @@ -426,12 +479,13 @@ function PoInputGrid({ | |||
| { | |||
| field: "actions", | |||
| type: "actions", | |||
| headerName: `${t("start")} | ${t("qc")} | ${t("escalation")} | ${t( | |||
| "stock in", | |||
| )} | ${t("putaway")} | ${t("delete")}`, | |||
| // headerName: `${t("start")} | ${t("qc")} | ${t("escalation")} | ${t( | |||
| // "stock in", | |||
| // )} | ${t("putaway")} | ${t("delete")}`, | |||
| headerName: "動作", | |||
| // headerName: "start | qc | escalation | stock in | putaway | delete", | |||
| width: 300, | |||
| // flex: 1.5, | |||
| // flex: 2, | |||
| cellClassName: "actions", | |||
| getActions: (params) => { | |||
| // console.log(params.row.status); | |||
| @@ -440,130 +494,158 @@ function PoInputGrid({ | |||
| // console.log(session?.user?.abilities?.includes("APPROVAL")); | |||
| return [ | |||
| <GridActionsCellItem | |||
| icon={<PlayArrowIcon />} | |||
| icon={<Button variant="contained">{t("qc processing")}</Button>} | |||
| label="start" | |||
| sx={{ | |||
| color: "primary.main", | |||
| // marginRight: 1, | |||
| }} | |||
| 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", | |||
| // marginRight: 1, | |||
| }} | |||
| disabled={ | |||
| // stockInLineStatusMap[status] === 9 || | |||
| stockInLineStatusMap[status] < 1 | |||
| } | |||
| // 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: 1, | |||
| }} | |||
| disabled={ | |||
| stockInLineStatusMap[status] === 9 || | |||
| stockInLineStatusMap[status] <= 0 || | |||
| stockInLineStatusMap[status] >= 5 | |||
| } | |||
| // disabled={!(stockInLineStatusMap[status] === 0)} | |||
| // set _isNew to false after posting | |||
| // or check status | |||
| onClick={handleEscalation(params.row.id, params)} | |||
| onClick={handleNewQC(params.row.id, params)} | |||
| color="inherit" | |||
| key="edit" | |||
| />, | |||
| <GridActionsCellItem | |||
| icon={<ShoppingCartIcon />} | |||
| label="stockin" | |||
| sx={{ | |||
| color: "primary.main", | |||
| // marginRight: 1, | |||
| }} | |||
| disabled={ | |||
| stockInLineStatusMap[status] === 9 || | |||
| stockInLineStatusMap[status] <= 2 || | |||
| stockInLineStatusMap[status] >= 7 || | |||
| (stockInLineStatusMap[status] >= 3 && | |||
| stockInLineStatusMap[status] <= 5 && | |||
| !session?.user?.abilities?.includes("APPROVAL")) | |||
| } | |||
| // set _isNew to false after posting | |||
| // or check status | |||
| onClick={handleStockIn(params.row.id, params)} | |||
| color="inherit" | |||
| key="edit" | |||
| />, | |||
| <GridActionsCellItem | |||
| icon={<ShoppingCartIcon />} | |||
| label="putaway" | |||
| icon={<Button variant="contained">{t("putawayBtn")}</Button>} | |||
| label="start" | |||
| sx={{ | |||
| color: "primary.main", | |||
| // marginRight: 1, | |||
| }} | |||
| disabled={ | |||
| stockInLineStatusMap[status] === 9 || | |||
| stockInLineStatusMap[status] < 7 | |||
| } | |||
| // disabled={!(stockInLineStatusMap[status] === 0)} | |||
| // set _isNew to false after posting | |||
| // or check status | |||
| onClick={handlePutAway(params.row.id, params)} | |||
| onClick={handleStart(params.row.id, params)} | |||
| color="inherit" | |||
| key="edit" | |||
| />, | |||
| // <GridActionsCellItem | |||
| // icon={<QrCodeIcon />} | |||
| // icon={<Button variant="contained">{t("qc processing")}</Button>} | |||
| // label="start" | |||
| // sx={{ | |||
| // color: "primary.main", | |||
| // // marginRight: 1, | |||
| // }} | |||
| // 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", | |||
| // // marginRight: 1, | |||
| // }} | |||
| // disabled={ | |||
| // // stockInLineStatusMap[status] === 9 || | |||
| // stockInLineStatusMap[status] < 1 | |||
| // } | |||
| // // 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: 1, | |||
| // }} | |||
| // disabled={ | |||
| // stockInLineStatusMap[status] === 9 || | |||
| // stockInLineStatusMap[status] <= 0 || | |||
| // stockInLineStatusMap[status] >= 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: 1, | |||
| // }} | |||
| // disabled={ | |||
| // stockInLineStatusMap[status] === 9 || | |||
| // stockInLineStatusMap[status] <= 2 || | |||
| // stockInLineStatusMap[status] >= 7 || | |||
| // (stockInLineStatusMap[status] >= 3 && | |||
| // stockInLineStatusMap[status] <= 5 && | |||
| // !session?.user?.abilities?.includes("APPROVAL")) | |||
| // } | |||
| // // 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: 1, | |||
| // }} | |||
| // disabled={stockInLineStatusMap[status] === 9 || stockInLineStatusMap[status] !== 8} | |||
| // disabled={ | |||
| // stockInLineStatusMap[status] === 9 || | |||
| // stockInLineStatusMap[status] < 7 | |||
| // } | |||
| // // set _isNew to false after posting | |||
| // // or check status | |||
| // onClick={handleQrCode(params.row.id, params)} | |||
| // onClick={handlePutAway(params.row.id, params)} | |||
| // color="inherit" | |||
| // key="edit" | |||
| // />, | |||
| // // <GridActionsCellItem | |||
| // // icon={<QrCodeIcon />} | |||
| // // label="putaway" | |||
| // // sx={{ | |||
| // // color: "primary.main", | |||
| // // // marginRight: 1, | |||
| // // }} | |||
| // // disabled={stockInLineStatusMap[status] === 9 || stockInLineStatusMap[status] !== 8} | |||
| // // // set _isNew to false after posting | |||
| // // // or check status | |||
| // // onClick={handleQrCode(params.row.id, params)} | |||
| // // color="inherit" | |||
| // // key="edit" | |||
| // // />, | |||
| // <GridActionsCellItem | |||
| // icon={ | |||
| // stockInLineStatusMap[status] >= 1 ? ( | |||
| // <DoDisturbIcon /> | |||
| // ) : ( | |||
| // <DeleteIcon /> | |||
| // ) | |||
| // } | |||
| // label="Delete" | |||
| // sx={{ | |||
| // color: "error.main", | |||
| // }} | |||
| // disabled={ | |||
| // stockInLineStatusMap[status] >= 7 && | |||
| // stockInLineStatusMap[status] <= 9 | |||
| // } | |||
| // onClick={ | |||
| // stockInLineStatusMap[status] === 0 | |||
| // ? handleDelete(params.row.id) | |||
| // : handleReject(params.row.id, params) | |||
| // } | |||
| // color="inherit" | |||
| // key="edit" | |||
| // />, | |||
| <GridActionsCellItem | |||
| icon={ | |||
| stockInLineStatusMap[status] >= 1 ? ( | |||
| <DoDisturbIcon /> | |||
| ) : ( | |||
| <DeleteIcon /> | |||
| ) | |||
| } | |||
| label="Delete" | |||
| sx={{ | |||
| color: "error.main", | |||
| }} | |||
| disabled={ | |||
| stockInLineStatusMap[status] >= 7 && | |||
| stockInLineStatusMap[status] <= 9 | |||
| } | |||
| onClick={ | |||
| stockInLineStatusMap[status] === 0 | |||
| ? handleDelete(params.row.id) | |||
| : handleReject(params.row.id, params) | |||
| } | |||
| color="inherit" | |||
| key="edit" | |||
| />, | |||
| ]; | |||
| }, | |||
| }, | |||
| @@ -594,6 +676,7 @@ function PoInputGrid({ | |||
| }, | |||
| })); | |||
| }, [currQty, getRowId, itemDetail]); | |||
| const validation = useCallback( | |||
| ( | |||
| newRow: GridRowModel<StockInLineRow>, | |||
| @@ -654,20 +737,22 @@ function PoInputGrid({ | |||
| ); | |||
| const footer = ( | |||
| <Box display="flex" gap={2} alignItems="center"> | |||
| <Button | |||
| disableRipple | |||
| variant="outlined" | |||
| startIcon={<Add />} | |||
| disabled={itemDetail.qty - currQty <= 0} | |||
| onClick={addRow} | |||
| size="small" | |||
| > | |||
| {t("Record pol")} | |||
| </Button> | |||
| </Box> | |||
| <> | |||
| {/* <Box display="flex" gap={2} alignItems="center"> | |||
| <Button | |||
| disableRipple | |||
| variant="outlined" | |||
| startIcon={<Add />} | |||
| disabled={itemDetail.qty - currQty <= 0} | |||
| onClick={addRow} | |||
| size="small" | |||
| > | |||
| {t("Record pol")} | |||
| </Button> | |||
| </Box> */} | |||
| </> | |||
| ); | |||
| return ( | |||
| <> | |||
| <StyledDataGrid | |||
| @@ -715,6 +800,21 @@ function PoInputGrid({ | |||
| footer: { child: footer }, | |||
| }} | |||
| /> | |||
| {modalInfo !== undefined && ( | |||
| <> | |||
| <PoQcStockInModalVer2 | |||
| // setRows={setRows} | |||
| setEntries={setEntries} | |||
| setStockInLine={setStockInLine} | |||
| setItemDetail={setModalInfo} | |||
| qc={qc} | |||
| open={newOpen} | |||
| onClose={closeNewModal} | |||
| itemDetail={modalInfo} | |||
| /> | |||
| </> | |||
| ) | |||
| } | |||
| {modalInfo !== undefined && ( | |||
| <> | |||
| <PoQcStockInModal | |||
| @@ -0,0 +1,400 @@ | |||
| "use client"; | |||
| import { | |||
| Dispatch, | |||
| MutableRefObject, | |||
| SetStateAction, | |||
| useCallback, | |||
| useEffect, | |||
| useMemo, | |||
| useState, | |||
| } from "react"; | |||
| import StyledDataGrid from "../StyledDataGrid"; | |||
| import { | |||
| FooterPropsOverrides, | |||
| GridActionsCellItem, | |||
| GridCellParams, | |||
| GridColDef, | |||
| GridEventListener, | |||
| GridRowEditStopReasons, | |||
| GridRowId, | |||
| GridRowIdGetter, | |||
| GridRowModel, | |||
| GridRowModes, | |||
| GridRowModesModel, | |||
| GridRowSelectionModel, | |||
| GridToolbarContainer, | |||
| GridValidRowModel, | |||
| useGridApiRef, | |||
| } from "@mui/x-data-grid"; | |||
| import { set, useFormContext } from "react-hook-form"; | |||
| import SaveIcon from "@mui/icons-material/Save"; | |||
| import DeleteIcon from "@mui/icons-material/Delete"; | |||
| import CancelIcon from "@mui/icons-material/Cancel"; | |||
| import { Add } from "@mui/icons-material"; | |||
| import { Box, Button, Typography } from "@mui/material"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { | |||
| GridApiCommunity, | |||
| GridSlotsComponentsProps, | |||
| } from "@mui/x-data-grid/internals"; | |||
| // T == CreatexxxInputs map of the form's fields | |||
| // V == target field input inside CreatexxxInputs, e.g. qcChecks: ItemQc[], V = ItemQc | |||
| // E == error | |||
| interface ResultWithId { | |||
| id: string | number; | |||
| } | |||
| // export type InputGridProps = { | |||
| // [key: string]: any | |||
| // } | |||
| interface DefaultResult<E> { | |||
| _isNew: boolean; | |||
| _error: E; | |||
| } | |||
| interface SelectionResult<E> { | |||
| active: boolean; | |||
| _isNew: boolean; | |||
| _error: E; | |||
| } | |||
| type Result<E> = DefaultResult<E> | SelectionResult<E>; | |||
| export type TableRow<V, E> = Partial< | |||
| V & { | |||
| isActive: boolean | undefined; | |||
| _isNew: boolean; | |||
| _error: E; | |||
| } & ResultWithId | |||
| >; | |||
| export interface InputDataGridProps<T, V, E> { | |||
| apiRef: MutableRefObject<GridApiCommunity>; | |||
| checkboxSelection: false | undefined; | |||
| _formKey: keyof T; | |||
| columns: GridColDef[]; | |||
| validateRow: (newRow: GridRowModel<TableRow<V, E>>) => E; | |||
| needAdd?: boolean; | |||
| } | |||
| export interface SelectionInputDataGridProps<T, V, E> { | |||
| // thinking how do | |||
| apiRef: MutableRefObject<GridApiCommunity>; | |||
| checkboxSelection: true; | |||
| _formKey: keyof T; | |||
| columns: GridColDef[]; | |||
| validateRow: (newRow: GridRowModel<TableRow<V, E>>) => E; | |||
| needAdd?: boolean; | |||
| } | |||
| export type Props<T, V, E> = | |||
| | InputDataGridProps<T, V, E> | |||
| | SelectionInputDataGridProps<T, V, E>; | |||
| export class ProcessRowUpdateError<T, E> extends Error { | |||
| public readonly row: T; | |||
| public readonly errors: E | undefined; | |||
| constructor(row: T, message?: string, errors?: E) { | |||
| super(message); | |||
| this.row = row; | |||
| this.errors = errors; | |||
| Object.setPrototypeOf(this, ProcessRowUpdateError.prototype); | |||
| } | |||
| } | |||
| // T == CreatexxxInputs map of the form's fields | |||
| // V == target field input inside CreatexxxInputs, e.g. qcChecks: ItemQc[], V = ItemQc | |||
| // E == error | |||
| function QcDatagrid<T, V, E>({ | |||
| apiRef, | |||
| checkboxSelection = false, | |||
| _formKey, | |||
| columns, | |||
| validateRow, | |||
| needAdd, | |||
| }: Props<T, V, E>) { | |||
| const { | |||
| t, | |||
| // i18n: { language }, | |||
| } = useTranslation("common"); | |||
| const formKey = _formKey.toString(); | |||
| const { setValue, getValues } = useFormContext(); | |||
| const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | |||
| // const apiRef = useGridApiRef(); | |||
| const getRowId = useCallback<GridRowIdGetter<TableRow<V, E>>>( | |||
| (row) => row.id! as number, | |||
| [], | |||
| ); | |||
| const list: TableRow<V, E>[] = getValues(formKey); | |||
| // console.log(list) | |||
| const [rows, setRows] = useState<TableRow<V, E>[]>(() => { | |||
| const list: TableRow<V, E>[] = getValues(formKey); | |||
| return list && list.length > 0 ? list : []; | |||
| }); | |||
| // const originalRows = list && list.length > 0 ? list : []; | |||
| const originalRows = useMemo(() => ( | |||
| list && list.length > 0 ? list : [] | |||
| ), [list]) | |||
| // const originalRowModel = originalRows.filter((li) => li.isActive).map(i => i.id) as GridRowSelectionModel | |||
| const [rowSelectionModel, setRowSelectionModel] = | |||
| useState<GridRowSelectionModel>(() => { | |||
| // const rowModel = list.filter((li) => li.isActive).map(i => i.id) as GridRowSelectionModel | |||
| const rowModel: GridRowSelectionModel = getValues( | |||
| `${formKey}_active`, | |||
| ) as GridRowSelectionModel; | |||
| console.log(rowModel); | |||
| return rowModel; | |||
| }); | |||
| const handleSave = useCallback( | |||
| (id: GridRowId) => () => { | |||
| setRowModesModel((prevRowModesModel) => ({ | |||
| ...prevRowModesModel, | |||
| [id]: { mode: GridRowModes.View }, | |||
| })); | |||
| }, | |||
| [], | |||
| ); | |||
| const onProcessRowUpdateError = useCallback( | |||
| (updateError: ProcessRowUpdateError<T, E>) => { | |||
| const errors = updateError.errors; | |||
| const row = updateError.row; | |||
| console.log(errors); | |||
| apiRef.current.updateRows([{ ...row, _error: errors }]); | |||
| }, | |||
| [apiRef], | |||
| ); | |||
| const processRowUpdate = useCallback( | |||
| ( | |||
| newRow: GridRowModel<TableRow<V, E>>, | |||
| originalRow: GridRowModel<TableRow<V, E>>, | |||
| ) => { | |||
| ///////////////// | |||
| // validation here | |||
| const errors = validateRow(newRow); | |||
| console.log(newRow); | |||
| if (errors) { | |||
| throw new ProcessRowUpdateError( | |||
| originalRow, | |||
| "validation error", | |||
| errors, | |||
| ); | |||
| } | |||
| ///////////////// | |||
| const { _isNew, _error, ...updatedRow } = newRow; | |||
| const rowToSave = { | |||
| ...updatedRow, | |||
| } as TableRow<V, E>; /// test | |||
| console.log(rowToSave); | |||
| setRows((rw) => | |||
| rw.map((r) => (getRowId(r) === getRowId(originalRow) ? rowToSave : r)), | |||
| ); | |||
| return rowToSave; | |||
| }, | |||
| [validateRow, getRowId], | |||
| ); | |||
| const addRow = useCallback(() => { | |||
| const newEntry = { id: Date.now(), _isNew: true } as TableRow<V, E>; | |||
| setRows((prev) => [...prev, newEntry]); | |||
| setRowModesModel((model) => ({ | |||
| ...model, | |||
| [getRowId(newEntry)]: { | |||
| mode: GridRowModes.Edit, | |||
| // fieldToFocus: "team", /// test | |||
| }, | |||
| })); | |||
| }, [getRowId]); | |||
| const reset = useCallback(() => { | |||
| setRowModesModel({}); | |||
| setRows(originalRows); | |||
| }, [originalRows]); | |||
| const handleCancel = useCallback( | |||
| (id: GridRowId) => () => { | |||
| setRowModesModel((model) => ({ | |||
| ...model, | |||
| [id]: { mode: GridRowModes.View, ignoreModifications: true }, | |||
| })); | |||
| const editedRow = rows.find((row) => getRowId(row) === id); | |||
| if (editedRow?._isNew) { | |||
| setRows((rw) => rw.filter((r) => getRowId(r) !== id)); | |||
| } else { | |||
| setRows((rw) => | |||
| rw.map((r) => (getRowId(r) === id ? { ...r, _error: undefined } : r)), | |||
| ); | |||
| } | |||
| }, | |||
| [rows, getRowId], | |||
| ); | |||
| const handleDelete = useCallback( | |||
| (id: GridRowId) => () => { | |||
| setRows((prevRows) => prevRows.filter((row) => getRowId(row) !== id)); | |||
| }, | |||
| [getRowId], | |||
| ); | |||
| const _columns = useMemo<GridColDef[]>( | |||
| () => [ | |||
| ...columns, | |||
| { | |||
| field: "actions", | |||
| type: "actions", | |||
| headerName: "", | |||
| flex: 0.5, | |||
| cellClassName: "actions", | |||
| getActions: ({ id }: { id: GridRowId }) => { | |||
| const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; | |||
| if (isInEditMode) { | |||
| return [ | |||
| <GridActionsCellItem | |||
| icon={<SaveIcon />} | |||
| label="Save" | |||
| key="edit" | |||
| sx={{ | |||
| color: "primary.main", | |||
| }} | |||
| onClick={handleSave(id)} | |||
| />, | |||
| <GridActionsCellItem | |||
| icon={<CancelIcon />} | |||
| label="Cancel" | |||
| key="edit" | |||
| onClick={handleCancel(id)} | |||
| />, | |||
| ]; | |||
| } | |||
| return [ | |||
| <GridActionsCellItem | |||
| icon={<DeleteIcon />} | |||
| label="Delete" | |||
| sx={{ | |||
| color: "error.main", | |||
| }} | |||
| onClick={handleDelete(id)} | |||
| color="inherit" | |||
| key="edit" | |||
| />, | |||
| ]; | |||
| }, | |||
| }, | |||
| ], | |||
| [columns, rowModesModel, handleSave, handleCancel, handleDelete], | |||
| ); | |||
| // sync useForm | |||
| useEffect(() => { | |||
| // console.log(formKey) | |||
| // console.log(rows) | |||
| setValue(formKey, rows); | |||
| }, [formKey, rows, setValue]); | |||
| const footer = ( | |||
| <Box display="flex" gap={2} alignItems="center"> | |||
| <Button | |||
| disableRipple | |||
| variant="outlined" | |||
| startIcon={<Add />} | |||
| onClick={addRow} | |||
| size="small" | |||
| > | |||
| {t("Add Record")} | |||
| </Button> | |||
| <Button | |||
| disableRipple | |||
| variant="outlined" | |||
| startIcon={<Add />} | |||
| onClick={reset} | |||
| size="small" | |||
| > | |||
| {t("Clean Record")} | |||
| </Button> | |||
| </Box> | |||
| ); | |||
| // const handleRowEditStop: GridEventListener<'rowEditStop'> = (params, event) => { | |||
| // if (params.reason === GridRowEditStopReasons.rowFocusOut) { | |||
| // event.defaultMuiPrevented = true; | |||
| // } | |||
| // }; | |||
| return ( | |||
| <StyledDataGrid | |||
| // {...props} | |||
| // getRowId={getRowId as GridRowIdGetter<GridValidRowModel>} | |||
| // checkbox selection | |||
| checkboxSelection={checkboxSelection} | |||
| disableRowSelectionOnClick={checkboxSelection} | |||
| onRowSelectionModelChange={(newRowSelectionModel) => { | |||
| if (checkboxSelection) { | |||
| setRowSelectionModel(newRowSelectionModel); | |||
| setValue("qcChecks_active", newRowSelectionModel); | |||
| } | |||
| }} | |||
| rowSelectionModel={rowSelectionModel} | |||
| apiRef={apiRef} | |||
| rows={rows} | |||
| columns={!checkboxSelection ? _columns : columns} | |||
| editMode="row" | |||
| 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 | |||
| processRowUpdate={processRowUpdate as any} | |||
| // onRowEditStop={handleRowEditStop} | |||
| rowModesModel={rowModesModel} | |||
| onRowModesModelChange={setRowModesModel} | |||
| onProcessRowUpdateError={onProcessRowUpdateError} | |||
| getCellClassName={(params: GridCellParams<TableRow<T, E>>) => { | |||
| let classname = ""; | |||
| if (params.row._error) { | |||
| classname = "hasError"; | |||
| } | |||
| return classname; | |||
| }} | |||
| slots={ | |||
| !checkboxSelection | |||
| ? { | |||
| footer: FooterToolbar, | |||
| noRowsOverlay: NoRowsOverlay, | |||
| } | |||
| : undefined | |||
| } | |||
| slotProps={ | |||
| !checkboxSelection && Boolean(needAdd) | |||
| ? { | |||
| footer: { child: footer }, | |||
| } | |||
| : undefined | |||
| // slotProps={renderFooter ? { | |||
| // footer: { child: footer }, | |||
| // }: undefined | |||
| } | |||
| /> | |||
| ); | |||
| } | |||
| const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => { | |||
| return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>; | |||
| }; | |||
| 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> | |||
| ); | |||
| }; | |||
| export default QcDatagrid; | |||
| @@ -0,0 +1,242 @@ | |||
| "use client"; | |||
| import { PurchaseQcResult, PurchaseQCInput } from "@/app/api/po/actions"; | |||
| import { | |||
| Box, | |||
| Card, | |||
| CardContent, | |||
| Grid, | |||
| Stack, | |||
| Tab, | |||
| Tabs, | |||
| TabsProps, | |||
| TextField, | |||
| Tooltip, | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import { useFormContext } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import StyledDataGrid from "../StyledDataGrid"; | |||
| import { useCallback, useEffect, 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 { GridEditInputCell } from "@mui/x-data-grid"; | |||
| import { StockInLine } from "@/app/api/po"; | |||
| import { stockInLineStatusMap } from "@/app/utils/formatUtil"; | |||
| import { fetchQcItemCheck, fetchQcResult } from "@/app/api/qc/actions"; | |||
| import { QcItemWithChecks } from "@/app/api/qc"; | |||
| import axios from "@/app/(main)/axios/axiosInstance"; | |||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | |||
| import axiosInstance from "@/app/(main)/axios/axiosInstance"; | |||
| interface Props { | |||
| itemDetail: StockInLine; | |||
| qc: QcItemWithChecks[]; | |||
| disabled: boolean; | |||
| } | |||
| type EntryError = | |||
| | { | |||
| [field in keyof PurchaseQcResult]?: string; | |||
| } | |||
| | undefined; | |||
| type PoQcRow = TableRow<Partial<PurchaseQcResult>, EntryError>; | |||
| // fetchQcItemCheck | |||
| const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled }) => { | |||
| const { t } = useTranslation("purchaseOrder"); | |||
| const apiRef = useGridApiRef(); | |||
| const { | |||
| register, | |||
| formState: { errors, defaultValues, touchedFields }, | |||
| watch, | |||
| control, | |||
| setValue, | |||
| getValues, | |||
| reset, | |||
| resetField, | |||
| setError, | |||
| clearErrors, | |||
| } = useFormContext<PurchaseQCInput>(); | |||
| console.log(itemDetail); | |||
| console.log(defaultValues); | |||
| const [tabIndex, setTabIndex] = useState(0); | |||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||
| (_e, newValue) => { | |||
| setTabIndex(newValue); | |||
| }, | |||
| [], | |||
| ); | |||
| //// validate form | |||
| const accQty = watch("acceptedQty"); | |||
| const validateForm = useCallback(() => { | |||
| console.log(accQty); | |||
| if (accQty > itemDetail.acceptedQty) { | |||
| setError("acceptedQty", { | |||
| message: `${t("acceptedQty must not greater than")} ${ | |||
| itemDetail.acceptedQty | |||
| }`, | |||
| type: "required", | |||
| }); | |||
| } | |||
| if (accQty < 1) { | |||
| setError("acceptedQty", { | |||
| message: t("minimal value is 1"), | |||
| type: "required", | |||
| }); | |||
| } | |||
| if (isNaN(accQty)) { | |||
| setError("acceptedQty", { | |||
| message: t("value must be a number"), | |||
| type: "required", | |||
| }); | |||
| } | |||
| }, [accQty]); | |||
| useEffect(() => { | |||
| clearErrors(); | |||
| validateForm(); | |||
| }, [clearErrors, validateForm]); | |||
| const columns = useMemo<GridColDef[]>( | |||
| () => [ | |||
| { | |||
| field: "qcItemId", | |||
| headerName: t("qc Check"), | |||
| flex: 1, | |||
| editable: !disabled, | |||
| 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.qcItemId); | |||
| 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 PurchaseQcResult]; | |||
| console.log(errorMessage); | |||
| const content = ( | |||
| <QcSelect | |||
| allQcs={qc} | |||
| value={params.row.qcItemId} | |||
| onQcSelect={async (qcItemId) => { | |||
| await params.api.setEditCellValue({ | |||
| id: params.id, | |||
| field: "qcItemId", | |||
| value: qcItemId, | |||
| }); | |||
| // await params.api.setEditCellValue({ | |||
| // id: params.id, | |||
| // field: "type", | |||
| // value: "determine1", | |||
| // }); | |||
| }} | |||
| /> | |||
| ); | |||
| return errorMessage ? ( | |||
| <Tooltip title={errorMessage}> | |||
| <Box width="100%">{content}</Box> | |||
| </Tooltip> | |||
| ) : ( | |||
| content | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| field: "failQty", | |||
| headerName: t("failQty"), | |||
| flex: 1, | |||
| editable: !disabled, | |||
| 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 PurchaseQcResult]; | |||
| const content = <GridEditInputCell {...params} />; | |||
| return errorMessage ? ( | |||
| <Tooltip title={t(errorMessage)}> | |||
| <Box width="100%">{content}</Box> | |||
| </Tooltip> | |||
| ) : ( | |||
| content | |||
| ); | |||
| }, | |||
| }, | |||
| ], | |||
| [qc], | |||
| ); | |||
| /// validate datagrid | |||
| const validation = useCallback( | |||
| (newRow: GridRowModel<PoQcRow>): EntryError => { | |||
| const error: EntryError = {}; | |||
| const { qcItemId, failQty } = newRow; | |||
| if (!qcItemId || qcItemId <= 0) { | |||
| error["qcItemId"] = t("select qc"); | |||
| } | |||
| if (!failQty || failQty <= 0) { | |||
| error["failQty"] = t("enter a failQty"); | |||
| } | |||
| if (failQty && failQty > itemDetail.acceptedQty) { | |||
| error["failQty"] = t("qty too big"); | |||
| } | |||
| return Object.keys(error).length > 0 ? error : undefined; | |||
| }, | |||
| [], | |||
| ); | |||
| useEffect(() => { | |||
| console.log(itemDetail); | |||
| const status = "receiving"; | |||
| // switch (itemDetail.status) { | |||
| // case 'pending': | |||
| // status = "receiving" | |||
| // break; | |||
| // } | |||
| setValue("status", status); | |||
| }, [itemDetail]); | |||
| return ( | |||
| <> | |||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | |||
| <Grid | |||
| container | |||
| justifyContent="flex-start" | |||
| alignItems="flex-start" | |||
| spacing={2} | |||
| sx={{ mt: 0.5 }} | |||
| > | |||
| <Tabs | |||
| value={tabIndex} | |||
| onChange={handleTabChange} | |||
| variant="scrollable" | |||
| > | |||
| <Tab label={t("QC Info")} iconPosition="end" /> | |||
| <Tab label={t("Escalation History")} iconPosition="end" /> | |||
| </Tabs> | |||
| </Grid> | |||
| </Grid> | |||
| </> | |||
| ); | |||
| }; | |||
| export default QcFormVer2; | |||
| @@ -0,0 +1,149 @@ | |||
| "use client"; | |||
| import { StockInLine } from "@/app/api/po"; | |||
| import { ModalFormInput, PurchaseQcResult } from "@/app/api/po/actions"; | |||
| import { QcItemWithChecks } from "@/app/api/qc"; | |||
| import { Box, Button, Grid, Modal, ModalProps, Stack, Typography } from "@mui/material"; | |||
| import { Dispatch, SetStateAction, useCallback, useState } from "react"; | |||
| import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | |||
| import { StockInLineRow } from "./PoInputGrid"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import StockInForm from "./StockInForm"; | |||
| import StockInFormVer2 from "./StockInFormVer2"; | |||
| import QcFormVer2 from "./QcFormVer2"; | |||
| const style = { | |||
| position: "absolute", | |||
| top: "50%", | |||
| left: "50%", | |||
| transform: "translate(-50%, -50%)", | |||
| overflowY: "scroll", | |||
| bgcolor: "background.paper", | |||
| pt: 5, | |||
| px: 5, | |||
| pb: 10, | |||
| display: "block", | |||
| width: { xs: "60%", sm: "60%", md: "60%" }, | |||
| }; | |||
| interface CommonProps extends Omit<ModalProps, "children"> { | |||
| // setRows: Dispatch<SetStateAction<PurchaseOrderLine[]>>; | |||
| setEntries?: Dispatch<SetStateAction<StockInLineRow[]>>; | |||
| setStockInLine?: Dispatch<SetStateAction<StockInLine[]>>; | |||
| itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] }; | |||
| setItemDetail: Dispatch< | |||
| SetStateAction< | |||
| | (StockInLine & { | |||
| warehouseId?: number; | |||
| }) | |||
| | undefined | |||
| > | |||
| >; | |||
| qc?: QcItemWithChecks[]; | |||
| warehouse?: any[]; | |||
| // type: "qc" | "stockIn" | "escalation" | "putaway" | "reject"; | |||
| } | |||
| interface Props extends CommonProps{ | |||
| itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] }; | |||
| } | |||
| const PoQcStockInModalVer2: React.FC<Props> = ({ | |||
| // type, | |||
| // setRows, | |||
| setEntries, | |||
| setStockInLine, | |||
| open, | |||
| onClose, | |||
| itemDetail, | |||
| setItemDetail, | |||
| qc, | |||
| warehouse, | |||
| }) => { | |||
| const { | |||
| t, | |||
| i18n: { language }, | |||
| } = useTranslation("purchaseOrder"); | |||
| const formProps = useForm<ModalFormInput>({ | |||
| defaultValues: { | |||
| ...itemDetail, | |||
| // receiptDate: itemDetail.receiptDate || dayjs().add(-1, "month").format(INPUT_DATE_FORMAT), | |||
| // warehouseId: itemDetail.defaultWarehouseId || 0 | |||
| }, | |||
| }); | |||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
| (...args) => { | |||
| onClose?.(...args); | |||
| // reset(); | |||
| }, | |||
| [onClose], | |||
| ); | |||
| const [submissionType, setSubmissionType] = useState<"stockIn" | "qc" | "escalate" | undefined>(undefined) | |||
| const onSubmit = useCallback<SubmitHandler<ModalFormInput>>( | |||
| async (data, event) => { | |||
| console.log(event!.nativeEvent) | |||
| // divide 3 section for this submition | |||
| // switch (submissionType) { | |||
| // submit stock in data | |||
| // submit qc data | |||
| // submit putaway | |||
| // } | |||
| }, [submissionType]) | |||
| return ( | |||
| <> | |||
| <FormProvider {...formProps}> | |||
| <Modal open={open} onClose={closeHandler} sx={{ overflowY: "scroll" }}> | |||
| <Box | |||
| sx={style} | |||
| component="form" | |||
| onSubmit={formProps.handleSubmit(onSubmit)} | |||
| > | |||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | |||
| <Grid item xs={12}> | |||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||
| {t("qc processing")} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <StockInFormVer2 | |||
| itemDetail={itemDetail} | |||
| disabled={false} | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| <Button | |||
| id="stockIn" | |||
| type="button" | |||
| variant="contained" | |||
| color="primary" | |||
| > | |||
| {t("submitStockIn")} | |||
| </Button> | |||
| </Stack> | |||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | |||
| <QcFormVer2 | |||
| qc={qc!} | |||
| itemDetail={itemDetail} | |||
| disabled={false} | |||
| /> | |||
| </Grid> | |||
| <Button | |||
| id="qc" | |||
| type="button" | |||
| variant="contained" | |||
| color="secondary" | |||
| > | |||
| Submit QC | |||
| </Button> | |||
| </Box> | |||
| </Modal> | |||
| </FormProvider> | |||
| </> | |||
| ) | |||
| } | |||
| export default PoQcStockInModalVer2 | |||
| @@ -0,0 +1,323 @@ | |||
| "use client"; | |||
| import { | |||
| PurchaseQcResult, | |||
| PurchaseQCInput, | |||
| StockInInput, | |||
| } from "@/app/api/po/actions"; | |||
| import { | |||
| Box, | |||
| Card, | |||
| CardContent, | |||
| Grid, | |||
| Stack, | |||
| TextField, | |||
| Tooltip, | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import { Controller, useFormContext } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import StyledDataGrid from "../StyledDataGrid"; | |||
| import { useCallback, useEffect, 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"; | |||
| import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; | |||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||
| import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
| import dayjs from "dayjs"; | |||
| // change PurchaseQcResult to stock in entry props | |||
| interface Props { | |||
| itemDetail: StockInLine; | |||
| // qc: QcItemWithChecks[]; | |||
| disabled: boolean; | |||
| } | |||
| type EntryError = | |||
| | { | |||
| [field in keyof StockInInput]?: string; | |||
| } | |||
| | undefined; | |||
| // type PoQcRow = TableRow<Partial<PurchaseQcResult>, EntryError>; | |||
| const StockInFormVer2: React.FC<Props> = ({ | |||
| // qc, | |||
| itemDetail, | |||
| disabled, | |||
| }) => { | |||
| const { | |||
| t, | |||
| i18n: { language }, | |||
| } = useTranslation("purchaseOrder"); | |||
| const apiRef = useGridApiRef(); | |||
| const { | |||
| register, | |||
| formState: { errors, defaultValues, touchedFields }, | |||
| watch, | |||
| control, | |||
| setValue, | |||
| getValues, | |||
| reset, | |||
| resetField, | |||
| setError, | |||
| clearErrors, | |||
| } = useFormContext<StockInInput>(); | |||
| console.log(itemDetail); | |||
| useEffect(() => { | |||
| console.log("triggered"); | |||
| // receiptDate default tdy | |||
| setValue("receiptDate", dayjs().add(0, "month").format(INPUT_DATE_FORMAT)); | |||
| setValue("status", "received"); | |||
| }, [setValue]); | |||
| useEffect(() => { | |||
| console.log(errors); | |||
| }, [errors]); | |||
| const productionDate = watch("productionDate"); | |||
| const expiryDate = watch("expiryDate"); | |||
| useEffect(() => { | |||
| console.log(productionDate); | |||
| console.log(expiryDate); | |||
| if (expiryDate) clearErrors(); | |||
| if (productionDate) clearErrors(); | |||
| }, [productionDate, expiryDate, clearErrors]); | |||
| console.log(itemDetail) | |||
| return ( | |||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | |||
| {/* <Grid item xs={12}> | |||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||
| {t("Stock In Detail")} | |||
| </Typography> | |||
| </Grid> */} | |||
| <Grid | |||
| container | |||
| justifyContent="flex-start" | |||
| alignItems="flex-start" | |||
| spacing={2} | |||
| sx={{ mt: 0.5 }} | |||
| > | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("dnNo")} | |||
| fullWidth | |||
| {...register("dnNo", { | |||
| // required: "productLotNo required!", | |||
| })} | |||
| disabled={true} | |||
| // error={Boolean(errors.productLotNo)} | |||
| // helperText={errors.productLotNo?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("itemName")} | |||
| fullWidth | |||
| {...register("itemName", { | |||
| // required: "productLotNo required!", | |||
| })} | |||
| disabled={true} | |||
| // error={Boolean(errors.productLotNo)} | |||
| // helperText={errors.productLotNo?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("PO No.")} | |||
| fullWidth | |||
| {...register("poCode", { | |||
| // required: "productLotNo required!", | |||
| })} | |||
| disabled={true} | |||
| // error={Boolean(errors.productLotNo)} | |||
| // helperText={errors.productLotNo?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <Controller | |||
| control={control} | |||
| name="receiptDate" | |||
| rules={{ required: true }} | |||
| render={({ field }) => { | |||
| return ( | |||
| <LocalizationProvider | |||
| dateAdapter={AdapterDayjs} | |||
| adapterLocale={`${language}-hk`} | |||
| > | |||
| <DatePicker | |||
| {...field} | |||
| sx={{ width: "100%" }} | |||
| label={t("receiptDate")} | |||
| value={dayjs(watch("receiptDate"))} | |||
| disabled={true} | |||
| onChange={(date) => { | |||
| if (!date) return; | |||
| // setValue("receiptDate", date.format(INPUT_DATE_FORMAT)); | |||
| field.onChange(date); | |||
| }} | |||
| inputRef={field.ref} | |||
| slotProps={{ | |||
| textField: { | |||
| // required: true, | |||
| error: Boolean(errors.receiptDate?.message), | |||
| helperText: errors.receiptDate?.message, | |||
| }, | |||
| }} | |||
| /> | |||
| </LocalizationProvider> | |||
| ); | |||
| }} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <Controller | |||
| control={control} | |||
| name="productionDate" | |||
| // rules={{ required: !Boolean(expiryDate) }} | |||
| render={({ field }) => { | |||
| return ( | |||
| <LocalizationProvider | |||
| dateAdapter={AdapterDayjs} | |||
| adapterLocale={`${language}-hk`} | |||
| > | |||
| <DatePicker | |||
| {...field} | |||
| sx={{ width: "100%" }} | |||
| label={t("productionDate")} | |||
| value={productionDate ? dayjs(productionDate) : undefined} | |||
| disabled={disabled} | |||
| onChange={(date) => { | |||
| if (!date) return; | |||
| setValue( | |||
| "productionDate", | |||
| date.format(INPUT_DATE_FORMAT), | |||
| ); | |||
| // field.onChange(date); | |||
| }} | |||
| inputRef={field.ref} | |||
| slotProps={{ | |||
| textField: { | |||
| // required: true, | |||
| error: Boolean(errors.productionDate?.message), | |||
| helperText: errors.productionDate?.message, | |||
| }, | |||
| }} | |||
| /> | |||
| </LocalizationProvider> | |||
| ); | |||
| }} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("productLotNo")} | |||
| fullWidth | |||
| {...register("productLotNo", { | |||
| // required: "productLotNo required!", | |||
| })} | |||
| disabled={disabled} | |||
| error={Boolean(errors.productLotNo)} | |||
| helperText={errors.productLotNo?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <Controller | |||
| control={control} | |||
| name="expiryDate" | |||
| // rules={{ required: !Boolean(productionDate) }} | |||
| render={({ field }) => { | |||
| return ( | |||
| <LocalizationProvider | |||
| dateAdapter={AdapterDayjs} | |||
| adapterLocale={`${language}-hk`} | |||
| > | |||
| <DatePicker | |||
| {...field} | |||
| sx={{ width: "100%" }} | |||
| label={t("expiryDate")} | |||
| value={expiryDate ? dayjs(expiryDate) : undefined} | |||
| disabled={disabled} | |||
| onChange={(date) => { | |||
| console.log(date); | |||
| if (!date) return; | |||
| console.log(date.format(INPUT_DATE_FORMAT)); | |||
| setValue("expiryDate", date.format(INPUT_DATE_FORMAT)); | |||
| // field.onChange(date); | |||
| }} | |||
| inputRef={field.ref} | |||
| slotProps={{ | |||
| textField: { | |||
| // required: true, | |||
| error: Boolean(errors.expiryDate?.message), | |||
| helperText: errors.expiryDate?.message, | |||
| }, | |||
| }} | |||
| /> | |||
| </LocalizationProvider> | |||
| ); | |||
| }} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("acceptedQty")} | |||
| fullWidth | |||
| {...register("acceptedQty", { | |||
| required: "acceptedQty required!", | |||
| })} | |||
| disabled={disabled} | |||
| error={Boolean(errors.acceptedQty)} | |||
| helperText={errors.acceptedQty?.message} | |||
| /> | |||
| </Grid> | |||
| {/* <Grid item xs={4}> | |||
| <TextField | |||
| label={t("acceptedWeight")} | |||
| fullWidth | |||
| // {...register("acceptedWeight", { | |||
| // required: "acceptedWeight required!", | |||
| // })} | |||
| disabled={disabled} | |||
| error={Boolean(errors.acceptedWeight)} | |||
| helperText={errors.acceptedWeight?.message} | |||
| /> | |||
| </Grid> */} | |||
| </Grid> | |||
| <Grid | |||
| container | |||
| justifyContent="flex-start" | |||
| alignItems="flex-start" | |||
| spacing={2} | |||
| sx={{ mt: 0.5 }} | |||
| > | |||
| {/* <Grid item xs={12}> | |||
| <InputDataGrid<PurchaseQCInput, PurchaseQcResult, EntryError> | |||
| apiRef={apiRef} | |||
| checkboxSelection={false} | |||
| _formKey={"qcCheck"} | |||
| columns={columns} | |||
| validateRow={validationTest} | |||
| /> | |||
| </Grid> */} | |||
| </Grid> | |||
| </Grid> | |||
| ); | |||
| }; | |||
| export default StockInFormVer2; | |||
| @@ -0,0 +1,23 @@ | |||
| const dummyQCData = [ | |||
| { | |||
| id: 1, | |||
| qcItem: "目測", | |||
| isPassed: undefined, | |||
| failedQty: undefined, | |||
| remarks: undefined, | |||
| }, | |||
| { | |||
| id: 2, | |||
| qcItem: "目測2", | |||
| isPassed: undefined, | |||
| failedQty: undefined, | |||
| remarks: undefined, | |||
| }, | |||
| { | |||
| id: 3, | |||
| qcItem: "目測3", | |||
| isPassed: undefined, | |||
| failedQty: undefined, | |||
| remarks: undefined, | |||
| }, | |||
| ] | |||
| @@ -253,7 +253,7 @@ const RSOverview: React.FC<Props> = ({ type, defaultInputs }) => { | |||
| // setFilterObj({}); | |||
| // setTempSelectedValue({}); | |||
| refetchData(defaultInputs, "reset"); | |||
| }, []); | |||
| }, [defaultInputs, refetchData]); | |||
| const testRoughScheduleClick = useCallback(async () => { | |||
| try { | |||
| @@ -17,6 +17,9 @@ const StyledDataGrid = styled(DataGrid)(({ theme }) => ({ | |||
| "& .MuiDataGrid-columnSeparator": { | |||
| color: theme.palette.primary.main, | |||
| }, | |||
| "& .MuiDataGrid-row:nth-of-type(even)": { | |||
| backgroundColor: theme.palette.grey[200], // Light grey for even rows | |||
| }, | |||
| })); | |||
| export default StyledDataGrid; | |||
| @@ -82,6 +82,19 @@ | |||
| "Po Code": "採購訂單編號", | |||
| "No Warehouse": "沒有倉庫", | |||
| "Please scan warehouse qr code.": "請掃描倉庫 QR 碼。", | |||
| "receivedQty": "已來貨數量", | |||
| "dnQty": "送貨單數量", | |||
| "Accept submit": "接受來貨", | |||
| "qc processing": "處理來貨及品檢", | |||
| "putawayBtn": "上架", | |||
| "dnNo": "送貨單編號", | |||
| "dnDate": "送貨單日期", | |||
| "submitStockIn": "更新來貨資料", | |||
| "QC Info": "品檢資料", | |||
| "Escalation History": "品檢資料", | |||
| "Reject": "拒絕", | |||
| "submit": "提交", | |||