| @@ -14,8 +14,8 @@ export const metadata: Metadata = { | |||
| title: "Purchase Order", | |||
| }; | |||
| const production: React.FC = async () => { | |||
| const { t } = await getServerI18n("claims"); | |||
| const PurchaseOrder: React.FC = async () => { | |||
| const { t } = await getServerI18n("purchaseOrder"); | |||
| // preloadClaims(); | |||
| return ( | |||
| @@ -45,4 +45,4 @@ const production: React.FC = async () => { | |||
| ); | |||
| }; | |||
| export default production; | |||
| export default PurchaseOrder; | |||
| @@ -15,7 +15,8 @@ export interface PostStockInLiineResponse<T> { | |||
| type?: string | |||
| message: string | null; | |||
| errorPosition: string | keyof T; | |||
| entity: StockInLine | StockInLine[] | |||
| entity: T | T[] | |||
| // entity: StockInLine | StockInLine[] | |||
| } | |||
| export interface StockInLineEntry { | |||
| @@ -83,6 +84,7 @@ export const createStockInLine = async (data: StockInLineEntry) => { | |||
| body: JSON.stringify(data), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }); | |||
| // revalidateTag("po"); | |||
| return stockInLine | |||
| } | |||
| @@ -92,16 +94,34 @@ export const updateStockInLine = async (data: StockInLineEntry & ModalFormInput) | |||
| body: JSON.stringify(data), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }); | |||
| // revalidateTag("po"); | |||
| return stockInLine | |||
| } | |||
| export const startPo = async (poId: number) => { | |||
| const po = await serverFetchJson<PostStockInLiineResponse<PoResult>>(`${BASE_API_URL}/po/start/${poId}`, { | |||
| method: "POST", | |||
| body: JSON.stringify({ poId }), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }); | |||
| revalidateTag("po"); | |||
| return po | |||
| } | |||
| export const checkPolAndCompletePo = async (poId: number) => { | |||
| const po = await serverFetchJson<PostStockInLiineResponse<PoResult>>(`${BASE_API_URL}/po//check/${poId}`, { | |||
| const po = await serverFetchJson<PostStockInLiineResponse<PoResult>>(`${BASE_API_URL}/po/check/${poId}`, { | |||
| method: "POST", | |||
| body: JSON.stringify({ poId }), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }); | |||
| revalidateTag("po"); | |||
| return po | |||
| } | |||
| export const fetchPoInClient = cache(async (id: number) => { | |||
| return serverFetchJson<PoResult>(`${BASE_API_URL}/po/detail/${id}`, { | |||
| next: { tags: ["po"] }, | |||
| }); | |||
| }); | |||
| @@ -41,8 +41,10 @@ export interface StockInLine { | |||
| acceptedQty: number | |||
| price: number | |||
| priceUnit: string | |||
| productionDate: string | |||
| expiryDate: string | |||
| shelfLife?: number, | |||
| receiptDate?: string | |||
| productionDate?: string | |||
| expiryDate?: string | |||
| status: string | |||
| supplier: string | |||
| lotNo: string | |||
| @@ -18,6 +18,12 @@ export const integerFormatter = new Intl.NumberFormat("en-HK", { | |||
| }) | |||
| export const INPUT_DATE_FORMAT = "YYYY-MM-DD"; | |||
| export const OUTPUT_DATE_FORMAT = "YYYY/MM/DD"; | |||
| export const OUTPUT_TIME_FORMAT = "HH:mm:ss"; | |||
| export const stockInLineStatusMap: { [status: string]: number } = { | |||
| "draft": 0, | |||
| "pending": 1, | |||
| @@ -36,8 +36,10 @@ import { | |||
| } from "@mui/x-data-grid"; | |||
| import { | |||
| checkPolAndCompletePo, | |||
| fetchPoInClient, | |||
| fetchStockInLineInfo, | |||
| PurchaseQcResult, | |||
| startPo, | |||
| testFetch, | |||
| } from "@/app/api/po/actions"; | |||
| import { | |||
| @@ -88,14 +90,46 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||
| console.log(cameras); | |||
| const { t } = useTranslation(); | |||
| const apiRef = useGridApiRef(); | |||
| const [rows, setRows] = useState<PurchaseOrderLine[]>(po.pol || []); | |||
| const [purchaseOrder, setPurchaseOrder] = useState({ ...po }); | |||
| const [rows, setRows] = useState<PurchaseOrderLine[]>( | |||
| purchaseOrder.pol || [] | |||
| ); | |||
| const params = useSearchParams(); | |||
| const [currPoStatus, setCurrPoStatus] = useState(purchaseOrder.status); | |||
| const handleComplete = useCallback(async () => { | |||
| const checkRes = await checkPolAndCompletePo(purchaseOrder.id); | |||
| console.log(checkRes); | |||
| const newPo = await fetchPoInClient(purchaseOrder.id); | |||
| setPurchaseOrder(newPo); | |||
| }, [checkPolAndCompletePo, fetchPoInClient]); | |||
| const handleStartPo = useCallback(async () => { | |||
| const startRes = await startPo(purchaseOrder.id); | |||
| console.log(startRes); | |||
| const newPo = await fetchPoInClient(purchaseOrder.id); | |||
| setPurchaseOrder(newPo); | |||
| }, [startPo, fetchPoInClient]); | |||
| useEffect(() => { | |||
| setRows(purchaseOrder.pol || []); | |||
| }, [purchaseOrder]); | |||
| function Row(props: { row: PurchaseOrderLine }) { | |||
| const { row } = props; | |||
| const [open, setOpen] = useState(false); | |||
| const [processedQty, setProcessedQty] = useState(row.processed); | |||
| const [currStatus, setCurrStatus] = useState(row.status); | |||
| const [stockInLine, setStockInLine] = useState(row.stockInLine); | |||
| const totalWeight = useMemo( | |||
| () => calculateWeight(row.qty, row.uom), | |||
| [calculateWeight] | |||
| ); | |||
| const weightUnit = useMemo( | |||
| () => returnWeightUnit(row.uom), | |||
| [returnWeightUnit] | |||
| ); | |||
| useEffect(() => { | |||
| if (processedQty === row.qty) { | |||
| setCurrStatus("completed".toUpperCase()); | |||
| @@ -106,20 +140,12 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||
| } | |||
| }, [processedQty]); | |||
| const totalWeight = useMemo( | |||
| () => calculateWeight(row.qty, row.uom), | |||
| [calculateWeight] | |||
| ); | |||
| const weightUnit = useMemo( | |||
| () => returnWeightUnit(row.uom), | |||
| [returnWeightUnit] | |||
| ); | |||
| return ( | |||
| <> | |||
| <TableRow sx={{ "& > *": { borderBottom: "unset" }, color: "black" }}> | |||
| <TableCell> | |||
| <IconButton | |||
| disabled={purchaseOrder.status.toLowerCase() === "pending"} | |||
| aria-label="expand row" | |||
| size="small" | |||
| onClick={() => setOpen(!open)} | |||
| @@ -152,9 +178,10 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||
| <PoInputGrid | |||
| qc={qc} | |||
| setRows={setRows} | |||
| stockInLine={stockInLine} | |||
| setStockInLine={setStockInLine} | |||
| setProcessedQty={setProcessedQty} | |||
| itemDetail={row} | |||
| stockInLine={row.stockInLine} | |||
| warehouse={warehouse} | |||
| /> | |||
| </Box> | |||
| @@ -199,20 +226,6 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||
| setPutAwayOpen(true); | |||
| }, []); | |||
| const handleComplete = useCallback(async () => { | |||
| const res = await checkPolAndCompletePo(po.id) | |||
| if (res.type === "completed") { | |||
| // toast.success(res.message) | |||
| console.log(res) | |||
| return | |||
| } | |||
| if (res.type === "receiving") { | |||
| // toast.error(res.message) | |||
| console.log(res) | |||
| return | |||
| } | |||
| }, [checkPolAndCompletePo]); | |||
| return ( | |||
| <> | |||
| <Stack | |||
| @@ -220,13 +233,26 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||
| // component="form" | |||
| // onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | |||
| > | |||
| <Grid container xs={12} justifyContent="space-between"> | |||
| <Grid container xs={12} justifyContent="start"> | |||
| <Grid item> | |||
| <Typography mb={2} variant="h4"> | |||
| {po.code} | |||
| {/* {purchaseOrder.code} - {currPoStatus} */} | |||
| {purchaseOrder.code} - {purchaseOrder.status} | |||
| </Typography> | |||
| </Grid> | |||
| </Grid> | |||
| <Grid container xs={12} justifyContent="start"> | |||
| {purchaseOrder.status.toLowerCase() === "pending" && ( | |||
| <Grid item> | |||
| <Button onClick={handleStartPo}>Start</Button> | |||
| </Grid> | |||
| )} | |||
| {purchaseOrder.status.toLowerCase() === "receiving" && ( | |||
| <Grid item> | |||
| <Button onClick={handleComplete}>Complete</Button> | |||
| </Grid> | |||
| )} | |||
| </Grid> | |||
| {/* <Grid container xs={12} justifyContent="space-between"> | |||
| <Button onClick={handleComplete}>Complete</Button> | |||
| </Grid> */} | |||
| @@ -242,9 +268,15 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||
| </Tabs> | |||
| </Grid> | |||
| {/* <Grid item xs={4}> */} | |||
| {/* scanner */} | |||
| {/* scanner */} | |||
| {/* </Grid> */} | |||
| <Grid item xs={4} display="flex" justifyContent="end" alignItems="end"> | |||
| <Grid | |||
| item | |||
| xs={4} | |||
| display="flex" | |||
| justifyContent="end" | |||
| alignItems="end" | |||
| > | |||
| <QrModal | |||
| open={isOpenScanner} | |||
| onClose={onCloseScanner} | |||
| @@ -63,6 +63,7 @@ interface ResultWithId { | |||
| interface Props { | |||
| qc: QcItemWithChecks[]; | |||
| setRows: Dispatch<SetStateAction<PurchaseOrderLine[]>>; | |||
| setStockInLine: Dispatch<SetStateAction<StockInLine[]>>; | |||
| setProcessedQty: Dispatch<SetStateAction<number>>; | |||
| itemDetail: PurchaseOrderLine; | |||
| stockInLine: StockInLine[]; | |||
| @@ -100,6 +101,7 @@ class ProcessRowUpdateError extends Error { | |||
| function PoInputGrid({ | |||
| qc, | |||
| setRows, | |||
| setStockInLine, | |||
| setProcessedQty, | |||
| itemDetail, | |||
| stockInLine, | |||
| @@ -122,6 +124,7 @@ function PoInputGrid({ | |||
| const [escalOpen, setEscalOpen] = useState(false); | |||
| const [stockInOpen, setStockInOpen] = useState(false); | |||
| const [putAwayOpen, setPutAwayOpen] = useState(false); | |||
| const [btnIsLoading, setBtnIsLoading] = useState(false); | |||
| const [currQty, setCurrQty] = useState(() => { | |||
| const total = entries.reduce( | |||
| (acc, curr) => acc + (curr.acceptedQty || 0), | |||
| @@ -147,17 +150,14 @@ function PoInputGrid({ | |||
| ); | |||
| const handleStart = useCallback( | |||
| (id: GridRowId, params: any) => () => { | |||
| setBtnIsLoading(true); | |||
| setRowModesModel((prev) => ({ | |||
| ...prev, | |||
| [id]: { mode: GridRowModes.View }, | |||
| })); | |||
| setTimeout(async () => { | |||
| // post stock in line | |||
| console.log("delayed"); | |||
| console.log(params); | |||
| const oldId = params.row.id; | |||
| console.log(oldId); | |||
| console.log(params.row); | |||
| const postData = { | |||
| itemId: params.row.itemId, | |||
| itemNo: params.row.itemNo, | |||
| @@ -171,6 +171,13 @@ function PoInputGrid({ | |||
| setEntries((prev) => | |||
| prev.map((p) => (p.id === oldId ? (res.entity as StockInLine) : p)) | |||
| ); | |||
| setStockInLine( | |||
| (prev) => | |||
| prev.map((p) => | |||
| p.id === oldId ? (res.entity as StockInLine) : p | |||
| ) as StockInLine[] | |||
| ); | |||
| setBtnIsLoading(false); | |||
| // do post directly to test | |||
| // openStartModal(); | |||
| }, 200); | |||
| @@ -183,6 +190,7 @@ function PoInputGrid({ | |||
| const handleQC = useCallback( | |||
| (id: GridRowId, params: any) => async () => { | |||
| setBtnIsLoading(true); | |||
| setRowModesModel((prev) => ({ | |||
| ...prev, | |||
| [id]: { mode: GridRowModes.View }, | |||
| @@ -199,12 +207,14 @@ function PoInputGrid({ | |||
| // open qc modal | |||
| console.log("delayed"); | |||
| openQcModal(); | |||
| setBtnIsLoading(false); | |||
| }, 200); | |||
| }, | |||
| [fetchQcDefaultValue] | |||
| ); | |||
| const handleEscalation = useCallback( | |||
| (id: GridRowId, params: any) => () => { | |||
| // setBtnIsLoading(true); | |||
| setRowModesModel((prev) => ({ | |||
| ...prev, | |||
| [id]: { mode: GridRowModes.View }, | |||
| @@ -214,12 +224,14 @@ function PoInputGrid({ | |||
| // open qc modal | |||
| console.log("delayed"); | |||
| openEscalationModal(); | |||
| // setBtnIsLoading(false); | |||
| }, 200); | |||
| }, | |||
| [] | |||
| ); | |||
| const handleStockIn = useCallback( | |||
| (id: GridRowId, params: any) => () => { | |||
| // setBtnIsLoading(true); | |||
| setRowModesModel((prev) => ({ | |||
| ...prev, | |||
| [id]: { mode: GridRowModes.View }, | |||
| @@ -231,6 +243,7 @@ function PoInputGrid({ | |||
| // return the record with its status as pending | |||
| // update layout | |||
| console.log("delayed"); | |||
| // setBtnIsLoading(false); | |||
| }, 200); | |||
| }, | |||
| [] | |||
| @@ -238,6 +251,7 @@ function PoInputGrid({ | |||
| const handlePutAway = useCallback( | |||
| (id: GridRowId, params: any) => () => { | |||
| // setBtnIsLoading(true); | |||
| setRowModesModel((prev) => ({ | |||
| ...prev, | |||
| [id]: { mode: GridRowModes.View }, | |||
| @@ -249,6 +263,7 @@ function PoInputGrid({ | |||
| // return the record with its status as pending | |||
| // update layout | |||
| console.log("delayed"); | |||
| // setBtnIsLoading(false); | |||
| }, 200); | |||
| }, | |||
| [] | |||
| @@ -256,6 +271,7 @@ function PoInputGrid({ | |||
| const printQrcode = useCallback( | |||
| async (row: any) => { | |||
| setBtnIsLoading(true); | |||
| console.log(row.id); | |||
| const postData = { stockInLineIds: [row.id] }; | |||
| // const postData = { stockInLineIds: [42,43,44] }; | |||
| @@ -264,6 +280,7 @@ function PoInputGrid({ | |||
| console.log(response); | |||
| downloadFile(new Uint8Array(response.blobValue), response.filename!!); | |||
| } | |||
| setBtnIsLoading(false); | |||
| }, | |||
| [fetchPoQrcode, downloadFile] | |||
| ); | |||
| @@ -319,11 +336,11 @@ function PoInputGrid({ | |||
| () => [ | |||
| { | |||
| field: "itemNo", | |||
| flex: 0.8, | |||
| flex: 0.4, | |||
| }, | |||
| { | |||
| field: "itemName", | |||
| flex: 1, | |||
| flex: 0.6, | |||
| }, | |||
| { | |||
| field: "acceptedQty", | |||
| @@ -363,7 +380,7 @@ function PoInputGrid({ | |||
| field: "actions", | |||
| type: "actions", | |||
| headerName: "start | qc | escalation | stock in | putaway | delete", | |||
| flex: 1, | |||
| flex: 1.5, | |||
| cellClassName: "actions", | |||
| getActions: (params) => { | |||
| console.log(params.row.status); | |||
| @@ -376,7 +393,7 @@ function PoInputGrid({ | |||
| color: "primary.main", | |||
| // marginRight: 1, | |||
| }} | |||
| disabled={!(stockInLineStatusMap[status] === 0)} | |||
| disabled={btnIsLoading || !(stockInLineStatusMap[status] === 0)} | |||
| // set _isNew to false after posting | |||
| // or check status | |||
| onClick={handleStart(params.row.id, params)} | |||
| @@ -390,7 +407,7 @@ function PoInputGrid({ | |||
| color: "primary.main", | |||
| // marginRight: 1, | |||
| }} | |||
| disabled={stockInLineStatusMap[status] < 1} | |||
| disabled={btnIsLoading || stockInLineStatusMap[status] < 1} | |||
| // set _isNew to false after posting | |||
| // or check status | |||
| onClick={handleQC(params.row.id, params)} | |||
| @@ -405,8 +422,9 @@ function PoInputGrid({ | |||
| // marginRight: 1, | |||
| }} | |||
| disabled={ | |||
| btnIsLoading || | |||
| stockInLineStatusMap[status] <= 0 || | |||
| stockInLineStatusMap[status] >= 4 | |||
| stockInLineStatusMap[status] >= 5 | |||
| } | |||
| // set _isNew to false after posting | |||
| // or check status | |||
| @@ -421,7 +439,11 @@ function PoInputGrid({ | |||
| color: "primary.main", | |||
| // marginRight: 1, | |||
| }} | |||
| disabled={stockInLineStatusMap[status] <= 2 || stockInLineStatusMap[status] >= 7} | |||
| disabled={ | |||
| btnIsLoading || | |||
| stockInLineStatusMap[status] <= 2 || | |||
| stockInLineStatusMap[status] >= 7 | |||
| } | |||
| // set _isNew to false after posting | |||
| // or check status | |||
| onClick={handleStockIn(params.row.id, params)} | |||
| @@ -435,7 +457,7 @@ function PoInputGrid({ | |||
| color: "primary.main", | |||
| // marginRight: 1, | |||
| }} | |||
| disabled={stockInLineStatusMap[status] < 7} | |||
| disabled={btnIsLoading || stockInLineStatusMap[status] < 7} | |||
| // set _isNew to false after posting | |||
| // or check status | |||
| onClick={handlePutAway(params.row.id, params)} | |||
| @@ -449,7 +471,7 @@ function PoInputGrid({ | |||
| color: "primary.main", | |||
| // marginRight: 1, | |||
| }} | |||
| disabled={stockInLineStatusMap[status] !== 8} | |||
| disabled={btnIsLoading || stockInLineStatusMap[status] !== 8} | |||
| // set _isNew to false after posting | |||
| // or check status | |||
| onClick={handleQrCode(params.row.id, params)} | |||
| @@ -462,7 +484,7 @@ function PoInputGrid({ | |||
| sx={{ | |||
| color: "error.main", | |||
| }} | |||
| disabled={stockInLineStatusMap[status] !== 0} | |||
| disabled={btnIsLoading || stockInLineStatusMap[status] !== 0} | |||
| // disabled={Boolean(params.row.status)} | |||
| onClick={handleDelete(params.row.id)} | |||
| color="inherit" | |||
| @@ -472,7 +494,7 @@ function PoInputGrid({ | |||
| }, | |||
| }, | |||
| ], | |||
| [] | |||
| [btnIsLoading] | |||
| ); | |||
| const addRow = useCallback(() => { | |||
| @@ -533,6 +555,8 @@ function PoInputGrid({ | |||
| const newEntries = entries.map((e) => | |||
| getRowId(e) === getRowId(originalRow) ? rowToSave : e | |||
| ); | |||
| setStockInLine(newEntries as StockInLine[]); | |||
| console.log("triggered"); | |||
| setEntries(newEntries); | |||
| //update remaining qty | |||
| const total = newEntries.reduce( | |||
| @@ -569,9 +593,7 @@ function PoInputGrid({ | |||
| </Button> | |||
| </Box> | |||
| ); | |||
| useEffect(() => { | |||
| console.log(modalInfo); | |||
| }, [modalInfo]); | |||
| return ( | |||
| <> | |||
| <StyledDataGrid | |||
| @@ -623,7 +645,9 @@ function PoInputGrid({ | |||
| <> | |||
| <PoQcStockInModal | |||
| type={"qc"} | |||
| // setRows={setRows} | |||
| setEntries={setEntries} | |||
| setStockInLine={setStockInLine} | |||
| setItemDetail={setModalInfo} | |||
| qc={qc} | |||
| open={qcOpen} | |||
| @@ -636,7 +660,9 @@ function PoInputGrid({ | |||
| <> | |||
| <PoQcStockInModal | |||
| type={"escalation"} | |||
| // setRows={setRows} | |||
| setEntries={setEntries} | |||
| setStockInLine={setStockInLine} | |||
| setItemDetail={setModalInfo} | |||
| // qc={qc} | |||
| open={escalOpen} | |||
| @@ -649,7 +675,9 @@ function PoInputGrid({ | |||
| <> | |||
| <PoQcStockInModal | |||
| type={"stockIn"} | |||
| // setRows={setRows} | |||
| setEntries={setEntries} | |||
| setStockInLine={setStockInLine} | |||
| // qc={qc} | |||
| setItemDetail={setModalInfo} | |||
| open={stockInOpen} | |||
| @@ -662,7 +690,9 @@ function PoInputGrid({ | |||
| <> | |||
| <PoQcStockInModal | |||
| type={"putaway"} | |||
| // setRows={setRows} | |||
| setEntries={setEntries} | |||
| setStockInLine={setStockInLine} | |||
| setItemDetail={setModalInfo} | |||
| open={putAwayOpen} | |||
| warehouse={warehouse} | |||
| @@ -22,23 +22,35 @@ import { useTranslation } from "react-i18next"; | |||
| import QcForm from "./QcForm"; | |||
| import { QcItemWithChecks } from "@/app/api/qc"; | |||
| import { Check, CurrencyYuanRounded, TtyTwoTone } from "@mui/icons-material"; | |||
| import { StockInLine } from "@/app/api/po"; | |||
| import { PurchaseOrderLine, StockInLine } from "@/app/api/po"; | |||
| import { useSearchParams } from "next/navigation"; | |||
| import { StockInLineRow } from "./PoInputGrid"; | |||
| import EscalationForm from "./EscalationForm"; | |||
| import StockInForm from "./StockInForm"; | |||
| import PutawayForm from "./PutawayForm"; | |||
| import { stockInLineStatusMap } from "@/app/utils/formatUtil"; | |||
| import { | |||
| INPUT_DATE_FORMAT, | |||
| stockInLineStatusMap, | |||
| } from "@/app/utils/formatUtil"; | |||
| import dayjs from "dayjs"; | |||
| import arraySupport from "dayjs/plugin/arraySupport"; | |||
| import { downloadFile } from "@/app/utils/commonUtil"; | |||
| import { fetchPoQrcode } from "@/app/api/pdf/actions"; | |||
| dayjs.extend(arraySupport) | |||
| dayjs.extend(arraySupport); | |||
| 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>> | |||
| setItemDetail: Dispatch< | |||
| SetStateAction< | |||
| | (StockInLine & { | |||
| warehouseId?: number; | |||
| }) | |||
| | undefined | |||
| > | |||
| >; | |||
| qc?: QcItemWithChecks[]; | |||
| warehouse?: any[]; | |||
| type: "qc" | "stockIn" | "escalation" | "putaway"; | |||
| @@ -75,7 +87,9 @@ const style = { | |||
| const PoQcStockInModal: React.FC<Props> = ({ | |||
| type, | |||
| // setRows, | |||
| setEntries, | |||
| setStockInLine, | |||
| open, | |||
| onClose, | |||
| itemDetail, | |||
| @@ -86,12 +100,16 @@ const PoQcStockInModal: React.FC<Props> = ({ | |||
| const [serverError, setServerError] = useState(""); | |||
| const { t } = useTranslation(); | |||
| const params = useSearchParams(); | |||
| const [btnIsLoading, setBtnIsLoading] = useState(false); | |||
| console.log(params.get("id")); | |||
| console.log(itemDetail); | |||
| console.log(itemDetail.qcResult); | |||
| const formProps = useForm<ModalFormInput>({ | |||
| defaultValues: { | |||
| ...itemDetail, | |||
| // receiptDate: itemDetail.receiptDate || dayjs().add(-1, "month").format(INPUT_DATE_FORMAT), | |||
| // warehouseId: itemDetail.defaultWarehouseId || 0 | |||
| }, | |||
| }); | |||
| // console.log(formProps); | |||
| @@ -111,40 +129,95 @@ const PoQcStockInModal: React.FC<Props> = ({ | |||
| } | |||
| }, [itemDetail]); | |||
| const fix0IndexedDate = useCallback((date: string | number[] | undefined) => { | |||
| if (Array.isArray(date)) { | |||
| console.log(date) | |||
| return dayjs([date[0], date[1] - 1, date[2]]).format("YYYY-MM-DD") | |||
| } | |||
| return date | |||
| }, []) | |||
| // const fix0IndexedDate = useCallback((date: string | number[] | undefined) => { | |||
| // if (Array.isArray(date)) { | |||
| // console.log(date); | |||
| // return dayjs([date[0], date[1] - 1, date[2]]).format("YYYY-MM-DD"); | |||
| // } | |||
| // return date; | |||
| // }, []); | |||
| const checkStockIn = useCallback( | |||
| (data: ModalFormInput): boolean => { | |||
| let hasErrors = false; | |||
| if (itemDetail.shelfLife && !data.productionDate && !data.expiryDate) { | |||
| formProps.setError("productionDate", { | |||
| message: "Please provide at least one", | |||
| type: "invalid", | |||
| }); | |||
| formProps.setError("expiryDate", { | |||
| message: "Please provide at least one", | |||
| type: "invalid", | |||
| }); | |||
| hasErrors = true; | |||
| } | |||
| if (!itemDetail.shelfLife && !data.expiryDate) { | |||
| formProps.setError("expiryDate", { | |||
| message: "Please provide expiry date", | |||
| type: "invalid", | |||
| }); | |||
| hasErrors = true; | |||
| } | |||
| if (data.expiryDate && data.expiryDate < data.receiptDate!!) { | |||
| formProps.setError("expiryDate", { | |||
| message: "Expired", | |||
| type: "invalid", | |||
| }); | |||
| hasErrors = true; | |||
| } | |||
| return hasErrors; | |||
| }, | |||
| [itemDetail, formProps] | |||
| ); | |||
| const checkPutaway = useCallback( | |||
| (data: ModalFormInput): boolean => { | |||
| let hasErrors = false; | |||
| console.log(data.warehouseId); | |||
| if (!data.warehouseId || data.warehouseId <= 0) { | |||
| formProps.setError("warehouseId", { | |||
| message: "Please provide warehouseId", | |||
| type: "invalid", | |||
| }); | |||
| hasErrors = true; | |||
| } | |||
| return hasErrors; | |||
| }, | |||
| [itemDetail, formProps] | |||
| ); | |||
| const onSubmit = useCallback<SubmitHandler<ModalFormInput & {}>>( | |||
| async (data, event) => { | |||
| formProps.clearErrors(); | |||
| let hasErrors = false; | |||
| setBtnIsLoading(true); | |||
| console.log(errors); | |||
| console.log(data); | |||
| console.log(itemDetail); | |||
| console.log(data.receiptDate) | |||
| console.log(fix0IndexedDate(data.receiptDate)) | |||
| // console.log(fix0IndexedDate(data.receiptDate)); | |||
| try { | |||
| // add checking | |||
| // const qty = data.sampleRate | |||
| if (type === "stockIn") { | |||
| hasErrors = checkStockIn(data) | |||
| console.log(hasErrors) | |||
| } | |||
| if (type === "putaway") { | |||
| hasErrors = checkPutaway(data); | |||
| console.log(hasErrors) | |||
| } | |||
| //////////////////////// modify this mess later ////////////////////// | |||
| var productionDate = null; | |||
| var expiryDate = null; | |||
| var receiptDate = null; | |||
| var acceptedQty = null; | |||
| if (data.productionDate && data.productionDate.length > 0) { | |||
| productionDate = fix0IndexedDate(data.productionDate); | |||
| if (data.productionDate) { | |||
| productionDate = dayjs(data.productionDate).format(INPUT_DATE_FORMAT); | |||
| } | |||
| if (data.expiryDate && data.expiryDate.length > 0) { | |||
| expiryDate = fix0IndexedDate(data.expiryDate); | |||
| if (data.expiryDate) { | |||
| expiryDate = dayjs(data.expiryDate).format(INPUT_DATE_FORMAT); | |||
| } | |||
| if (data.receiptDate && data.receiptDate.length > 0) { | |||
| receiptDate = fix0IndexedDate(data.receiptDate); | |||
| if (data.receiptDate) { | |||
| receiptDate = dayjs(data.receiptDate).format(INPUT_DATE_FORMAT); | |||
| } | |||
| // if () | |||
| if (data.qcResult) { | |||
| @@ -163,17 +236,20 @@ const PoQcStockInModal: React.FC<Props> = ({ | |||
| receiptDate: receiptDate, | |||
| } as StockInLineEntry & ModalFormInput; | |||
| ////////////////////////////////////////////////////////////////////// | |||
| console.log(args); | |||
| // return | |||
| if (hasErrors) { | |||
| console.log(args); | |||
| setServerError(t("An error has occurred. Please try again later.")); | |||
| return false; | |||
| setBtnIsLoading(false); | |||
| return; | |||
| } | |||
| console.log(args); | |||
| // setBtnIsLoading(false); | |||
| // return | |||
| const res = await updateStockInLine(args); | |||
| if (Boolean(res.id)) { | |||
| // update entries | |||
| const newEntries = res.entity as StockInLine[]; | |||
| console.log(newEntries) | |||
| console.log(newEntries); | |||
| if (setEntries) { | |||
| setEntries((prev) => { | |||
| const updatedEntries = [...prev]; // Create a new array | |||
| @@ -181,7 +257,24 @@ const PoQcStockInModal: React.FC<Props> = ({ | |||
| const index = updatedEntries.findIndex((p) => p.id === item.id); | |||
| if (index !== -1) { | |||
| // Update existing item | |||
| console.log(item) | |||
| console.log(item); | |||
| updatedEntries[index] = item; | |||
| } else { | |||
| // Add new item | |||
| updatedEntries.push(item); | |||
| } | |||
| }); | |||
| return updatedEntries; // Return the new array | |||
| }); | |||
| } | |||
| if (setStockInLine) { | |||
| setStockInLine((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 | |||
| console.log(item); | |||
| updatedEntries[index] = item; | |||
| } else { | |||
| // Add new item | |||
| @@ -192,31 +285,56 @@ const PoQcStockInModal: React.FC<Props> = ({ | |||
| }); | |||
| } | |||
| // add loading | |||
| setItemDetail(undefined) | |||
| setBtnIsLoading(false); | |||
| setItemDetail(undefined); | |||
| closeHandler({}, "backdropClick"); | |||
| } | |||
| console.log(res); | |||
| // if (res) | |||
| } catch (e) { | |||
| // server error | |||
| setBtnIsLoading(false); | |||
| setServerError(t("An error has occurred. Please try again later.")); | |||
| console.log(e); | |||
| } | |||
| }, | |||
| [t, itemDetail] | |||
| [t, itemDetail, checkStockIn, checkPutaway] | |||
| ); | |||
| const printQrcode = useCallback(async () => { | |||
| setBtnIsLoading(true); | |||
| const postData = { stockInLineIds: [itemDetail.id] }; | |||
| // const postData = { stockInLineIds: [42,43,44] }; | |||
| const response = await fetchPoQrcode(postData); | |||
| if (response) { | |||
| console.log(response); | |||
| downloadFile(new Uint8Array(response.blobValue), response.filename!!); | |||
| } | |||
| setBtnIsLoading(false); | |||
| }, [itemDetail, fetchPoQrcode, downloadFile]); | |||
| const renderSubmitButton = useMemo((): Boolean => { | |||
| if (itemDetail) { | |||
| const status = itemDetail.status; | |||
| console.log(status); | |||
| switch (type) { | |||
| case "qc": | |||
| return stockInLineStatusMap[status] >= 1 && stockInLineStatusMap[status] <= 2; | |||
| return ( | |||
| stockInLineStatusMap[status] >= 1 && | |||
| stockInLineStatusMap[status] <= 2 | |||
| ); | |||
| case "escalation": | |||
| return stockInLineStatusMap[status] === 1 || stockInLineStatusMap[status] >= 3 || stockInLineStatusMap[status] <= 5; | |||
| return ( | |||
| stockInLineStatusMap[status] === 1 || | |||
| stockInLineStatusMap[status] >= 3 || | |||
| stockInLineStatusMap[status] <= 5 | |||
| ); | |||
| case "stockIn": | |||
| return stockInLineStatusMap[status] >= 3 && stockInLineStatusMap[status] <= 6; | |||
| return ( | |||
| stockInLineStatusMap[status] >= 3 && | |||
| stockInLineStatusMap[status] <= 6 | |||
| ); | |||
| case "putaway": | |||
| return stockInLineStatusMap[status] === 7; | |||
| default: | |||
| @@ -248,19 +366,30 @@ const PoQcStockInModal: React.FC<Props> = ({ | |||
| {itemDetail !== undefined && type === "putaway" && ( | |||
| <PutawayForm itemDetail={itemDetail} warehouse={warehouse!!} /> | |||
| )} | |||
| {renderSubmitButton ? ( | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| {renderSubmitButton ? ( | |||
| <Button | |||
| name="submit" | |||
| variant="contained" | |||
| startIcon={<Check />} | |||
| type="submit" | |||
| // disabled={submitDisabled} | |||
| disabled={btnIsLoading} | |||
| > | |||
| {t("submit")} | |||
| </Button> | |||
| </Stack> | |||
| ) : undefined} | |||
| ) : undefined} | |||
| {itemDetail !== undefined && type === "putaway" && ( | |||
| <Button | |||
| name="print" | |||
| variant="contained" | |||
| // startIcon={<Check />} | |||
| onClick={printQrcode} | |||
| disabled={btnIsLoading} | |||
| > | |||
| {t("print")} | |||
| </Button> | |||
| )} | |||
| </Stack> | |||
| </Box> | |||
| </Modal> | |||
| </FormProvider> | |||
| @@ -16,7 +16,7 @@ import { | |||
| Tooltip, | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import { useFormContext } from "react-hook-form"; | |||
| import { Controller, useFormContext } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import StyledDataGrid from "../StyledDataGrid"; | |||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||
| @@ -40,7 +40,9 @@ import { WarehouseResult } from "@/app/api/warehouse"; | |||
| import { stockInLineStatusMap } from "@/app/utils/formatUtil"; | |||
| import { QRCodeSVG } from "qrcode.react"; | |||
| import { QrCode } from "../QrCode"; | |||
| import ReactQrCodeScanner, { ScannerConfig } from "../ReactQrCodeScanner/ReactQrCodeScanner"; | |||
| import ReactQrCodeScanner, { | |||
| ScannerConfig, | |||
| } from "../ReactQrCodeScanner/ReactQrCodeScanner"; | |||
| import { QrCodeInfo } from "@/app/api/qrcode"; | |||
| interface Props { | |||
| @@ -84,19 +86,24 @@ const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse }) => { | |||
| clearErrors, | |||
| } = useFormContext<PutawayInput>(); | |||
| console.log(itemDetail); | |||
| const [recordQty, setRecordQty] = useState(0); | |||
| const [warehouseId, setWarehouseId] = useState(0); | |||
| // const [recordQty, setRecordQty] = useState(0); | |||
| const [warehouseId, setWarehouseId] = useState(itemDetail.defaultWarehouseId); | |||
| const filteredWarehouse = useMemo(() => { | |||
| // do filtering here if any | |||
| return warehouse; | |||
| }, []); | |||
| const defaultOption = { | |||
| value: 0, // think think sin | |||
| label: t("Select warehouse"), | |||
| group: "default", | |||
| } | |||
| const options = useMemo(() => { | |||
| return [ | |||
| { | |||
| value: -1, // think think sin | |||
| label: t("Select warehouse"), | |||
| group: "default", | |||
| }, | |||
| // { | |||
| // value: 0, // think think sin | |||
| // label: t("Select warehouse"), | |||
| // group: "default", | |||
| // }, | |||
| ...filteredWarehouse.map((w) => ({ | |||
| value: w.id, | |||
| label: `${w.code} - ${w.name}`, | |||
| @@ -107,8 +114,8 @@ const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse }) => { | |||
| const currentValue = | |||
| warehouseId > 0 | |||
| ? options.find((o) => o.value === warehouseId) | |||
| : options.find((o) => o.value === getValues("warehouseId")) || options[0]; | |||
| : options.find((o) => o.value === getValues("warehouseId")) || defaultOption; | |||
| const onChange = useCallback( | |||
| ( | |||
| event: React.SyntheticEvent, | |||
| @@ -119,19 +126,47 @@ const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse }) => { | |||
| group: string; | |||
| }; | |||
| console.log(singleNewVal); | |||
| setValue("warehouseId", singleNewVal.value); | |||
| console.log("onChange"); | |||
| // setValue("warehouseId", singleNewVal.value); | |||
| setWarehouseId(singleNewVal.value); | |||
| }, | |||
| [] | |||
| ); | |||
| // const accQty = watch("acceptedQty"); | |||
| // const validateForm = useCallback(() => { | |||
| // console.log(accQty); | |||
| // if (accQty > itemDetail.acceptedQty) { | |||
| // setError("acceptedQty", { | |||
| // message: `acceptedQty must not greater than ${itemDetail.acceptedQty}`, | |||
| // type: "required", | |||
| // }); | |||
| // } | |||
| // if (accQty < 1) { | |||
| // setError("acceptedQty", { | |||
| // message: `minimal value is 1`, | |||
| // type: "required", | |||
| // }); | |||
| // } | |||
| // if (isNaN(accQty)) { | |||
| // setError("acceptedQty", { | |||
| // message: `value must be a number`, | |||
| // type: "required", | |||
| // }); | |||
| // } | |||
| // }, [accQty]); | |||
| // useEffect(() => { | |||
| // clearErrors(); | |||
| // validateForm(); | |||
| // }, [validateForm]); | |||
| const qrContent = useMemo( | |||
| () => ({ | |||
| stockInLineId: itemDetail.id, | |||
| itemId: itemDetail.itemId, | |||
| lotNo: itemDetail.lotNo, | |||
| // warehouseId: 1 // for testing | |||
| // warehouseId: 2 // for testing | |||
| // expiryDate: itemDetail.expiryDate, | |||
| // productionDate: itemDetail.productionDate, | |||
| // supplier: itemDetail.supplier, | |||
| @@ -140,7 +175,7 @@ const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse }) => { | |||
| [itemDetail] | |||
| ); | |||
| const [isOpenScanner, setOpenScanner] = useState(false); | |||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
| (...args) => { | |||
| setOpenScanner(false); | |||
| @@ -158,23 +193,33 @@ const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse }) => { | |||
| const scannerConfig = useMemo<ScannerConfig>( | |||
| () => ({ | |||
| onUpdate: (err, result) => { | |||
| console.log(result); | |||
| console.log(Boolean(result)); | |||
| if (result) { | |||
| const data: QrCodeInfo = JSON.parse(result.getText()); | |||
| console.log(data); | |||
| if (data.warehouseId) { | |||
| console.log(data.warehouseId); | |||
| setWarehouseId(data.warehouseId); | |||
| onCloseScanner() | |||
| onCloseScanner(); | |||
| } | |||
| } else return; | |||
| }, | |||
| }), | |||
| [] | |||
| [onCloseScanner] | |||
| ); | |||
| useEffect(() => { | |||
| setValue("status", "completed"); | |||
| }, []); | |||
| useEffect(() => { | |||
| if (warehouseId > 0) { | |||
| setValue("warehouseId", warehouseId); | |||
| clearErrors("warehouseId") | |||
| } | |||
| }, [warehouseId]); | |||
| return ( | |||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | |||
| <Grid item xs={12}> | |||
| @@ -260,11 +305,13 @@ const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse }) => { | |||
| disableClearable | |||
| disabled | |||
| fullWidth | |||
| defaultValue={options.find((o) => o.value === 1)} | |||
| defaultValue={options.find((o) => o.value === 1)} /// modify this later | |||
| // onChange={onChange} | |||
| getOptionLabel={(option) => option.label} | |||
| options={options} | |||
| renderInput={(params) => <TextField {...params} label="Default Warehouse"/>} | |||
| renderInput={(params) => ( | |||
| <TextField {...params} label="Default Warehouse" /> | |||
| )} | |||
| /> | |||
| </FormControl> | |||
| </Grid> | |||
| @@ -278,25 +325,63 @@ const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse }) => { | |||
| max: itemDetail.acceptedQty, | |||
| valueAsNumber: true, | |||
| })} | |||
| defaultValue={itemDetail.acceptedQty} | |||
| // defaultValue={itemDetail.acceptedQty} | |||
| error={Boolean(errors.acceptedQty)} | |||
| helperText={errors.acceptedQty?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={1}> | |||
| <Button onClick={onOpenScanner}>bind</Button> | |||
| <Button onClick={onOpenScanner}>bind</Button> | |||
| </Grid> | |||
| <Grid item xs={5.5}> | |||
| {/* <Controller | |||
| control={control} | |||
| name="warehouseId" | |||
| render={({ field }) => { | |||
| console.log(field); | |||
| return ( | |||
| <Autocomplete | |||
| noOptionsText={t("No Warehouse")} | |||
| disableClearable | |||
| fullWidth | |||
| value={options.find((o) => o.value == field.value)} | |||
| onChange={onChange} | |||
| getOptionLabel={(option) => option.label} | |||
| options={options} | |||
| renderInput={(params) => ( | |||
| <TextField | |||
| {...params} | |||
| label={"Select warehouse"} | |||
| error={Boolean(errors.warehouseId?.message)} | |||
| helperText={warehouseHelperText} | |||
| // helperText={errors.warehouseId?.message} | |||
| /> | |||
| )} | |||
| /> | |||
| ); | |||
| }} | |||
| /> */} | |||
| <FormControl fullWidth> | |||
| <Autocomplete | |||
| noOptionsText={t("No Warehouse")} | |||
| disableClearable | |||
| fullWidth | |||
| // value={warehouseId > 0 | |||
| // ? options.find((o) => o.value === warehouseId) | |||
| // : undefined} | |||
| value={currentValue} | |||
| onChange={onChange} | |||
| getOptionLabel={(option) => option.label} | |||
| options={options} | |||
| renderInput={(params) => <TextField {...params} />} | |||
| renderInput={(params) => ( | |||
| <TextField | |||
| {...params} | |||
| // label={"Select warehouse"} | |||
| error={Boolean(errors.warehouseId?.message)} | |||
| helperText={errors.warehouseId?.message} | |||
| // helperText={warehouseHelperText} | |||
| /> | |||
| )} | |||
| /> | |||
| </FormControl> | |||
| </Grid> | |||
| @@ -318,11 +403,11 @@ const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse }) => { | |||
| <Button onClick={onOpenScanner}>bind</Button> | |||
| </Grid> */} | |||
| <Modal open={isOpenScanner} onClose={closeHandler}> | |||
| <Box sx={style}> | |||
| <ReactQrCodeScanner scannerConfig={scannerConfig} /> | |||
| </Box> | |||
| </Modal> | |||
| <Modal open={isOpenScanner} onClose={closeHandler}> | |||
| <Box sx={style}> | |||
| <ReactQrCodeScanner scannerConfig={scannerConfig} /> | |||
| </Box> | |||
| </Modal> | |||
| </Grid> | |||
| ); | |||
| }; | |||
| @@ -38,7 +38,7 @@ import { NEXT_PUBLIC_API_URL } from "@/config/api"; | |||
| import axiosInstance from "@/app/(main)/axios/axiosInstance"; | |||
| interface Props { | |||
| itemDetail: StockInLine | |||
| itemDetail: StockInLine; | |||
| qc: QcItemWithChecks[]; | |||
| } | |||
| type EntryError = | |||
| @@ -65,9 +65,37 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail }) => { | |||
| clearErrors, | |||
| } = useFormContext<PurchaseQCInput>(); | |||
| console.log(itemDetail); | |||
| console.log(defaultValues); | |||
| const [recordQty, setRecordQty] = useState(0); | |||
| console.log(defaultValues); | |||
| //// validate form | |||
| const accQty = watch("acceptedQty"); | |||
| const validateForm = useCallback(() => { | |||
| console.log(accQty); | |||
| if (accQty > itemDetail.acceptedQty) { | |||
| setError("acceptedQty", { | |||
| message: `acceptedQty must not greater than ${itemDetail.acceptedQty}`, | |||
| type: "required", | |||
| }); | |||
| } | |||
| if (accQty < 1) { | |||
| setError("acceptedQty", { | |||
| message: `minimal value is 1`, | |||
| type: "required", | |||
| }); | |||
| } | |||
| if (isNaN(accQty)) { | |||
| setError("acceptedQty", { | |||
| message: `value must be a number`, | |||
| type: "required", | |||
| }); | |||
| } | |||
| }, [accQty]); | |||
| useEffect(() => { | |||
| clearErrors(); | |||
| validateForm(); | |||
| }, [validateForm]); | |||
| // const [recordQty, setRecordQty] = useState(0); | |||
| const columns = useMemo<GridColDef[]>( | |||
| () => [ | |||
| { | |||
| @@ -105,7 +133,7 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail }) => { | |||
| // id: params.id, | |||
| // field: "type", | |||
| // value: "determine1", | |||
| // }); | |||
| // }); | |||
| }} | |||
| /> | |||
| ); | |||
| @@ -144,6 +172,7 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail }) => { | |||
| ], | |||
| [qc] | |||
| ); | |||
| /// validate datagrid | |||
| const validation = useCallback( | |||
| (newRow: GridRowModel<PoQcRow>): EntryError => { | |||
| const error: EntryError = {}; | |||
| @@ -161,17 +190,17 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail }) => { | |||
| }, | |||
| [] | |||
| ); | |||
| useEffect(() => { | |||
| console.log(itemDetail) | |||
| var status = "receiving" | |||
| console.log(itemDetail); | |||
| var status = "receiving"; | |||
| // switch (itemDetail.status) { | |||
| // case 'pending': | |||
| // status = "receiving" | |||
| // break; | |||
| // } | |||
| setValue("status", status) | |||
| }, [itemDetail]) | |||
| setValue("status", status); | |||
| }, [itemDetail]); | |||
| return ( | |||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | |||
| @@ -187,21 +216,22 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail }) => { | |||
| spacing={2} | |||
| sx={{ mt: 0.5 }} | |||
| > | |||
| <Grid item xs={12} lg={6}> | |||
| <Grid item xs={12} lg={12}> | |||
| <TextField | |||
| label={t("accepted Qty")} | |||
| fullWidth | |||
| // value={itemDetail.acceptedQty} | |||
| {...register("acceptedQty", { | |||
| required: "acceptedQty required!", | |||
| valueAsNumber: true | |||
| valueAsNumber: true, | |||
| max: itemDetail.acceptedQty, | |||
| })} | |||
| // disabled | |||
| error={Boolean(errors.acceptedQty)} | |||
| helperText={errors.acceptedQty?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={12} lg={6}> | |||
| {/* <Grid item xs={12} lg={6}> | |||
| <TextField | |||
| label={t("Total record qty")} | |||
| fullWidth | |||
| @@ -213,14 +243,15 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail }) => { | |||
| // error={Boolean(errors.sampleRate)} | |||
| // helperText={errors.sampleRate?.message} | |||
| /> | |||
| </Grid> | |||
| </Grid> */} | |||
| <Grid item xs={12} lg={6}> | |||
| <TextField | |||
| label={t("sampleRate")} | |||
| fullWidth | |||
| defaultValue={1} | |||
| {...register("sampleRate", { | |||
| required: "sampleRate required!", | |||
| valueAsNumber: true | |||
| valueAsNumber: true, | |||
| })} | |||
| error={Boolean(errors.sampleRate)} | |||
| helperText={errors.sampleRate?.message} | |||
| @@ -230,8 +261,10 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail }) => { | |||
| <TextField | |||
| label={t("sampleWeight")} | |||
| fullWidth | |||
| defaultValue={1} | |||
| {...register("sampleWeight", { | |||
| required: "sampleWeight required!", | |||
| valueAsNumber: true, | |||
| })} | |||
| error={Boolean(errors.sampleWeight)} | |||
| helperText={errors.sampleWeight?.message} | |||
| @@ -241,8 +274,10 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail }) => { | |||
| <TextField | |||
| label={t("totalWeight")} | |||
| fullWidth | |||
| defaultValue={1} | |||
| {...register("totalWeight", { | |||
| required: "totalWeight required!", | |||
| valueAsNumber: true, | |||
| })} | |||
| error={Boolean(errors.totalWeight)} | |||
| helperText={errors.totalWeight?.message} | |||
| @@ -263,7 +298,9 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail }) => { | |||
| _formKey={"qcResult"} | |||
| columns={columns} | |||
| validateRow={validation} | |||
| needAdd={itemDetail.status === "qc" || itemDetail.status === "pending"} | |||
| needAdd={ | |||
| itemDetail.status === "qc" || itemDetail.status === "pending" | |||
| } | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| @@ -1,6 +1,10 @@ | |||
| "use client"; | |||
| import { PurchaseQcResult, PurchaseQCInput, StockInInput } from "@/app/api/po/actions"; | |||
| import { | |||
| PurchaseQcResult, | |||
| PurchaseQCInput, | |||
| StockInInput, | |||
| } from "@/app/api/po/actions"; | |||
| import { | |||
| Box, | |||
| Card, | |||
| @@ -11,7 +15,7 @@ import { | |||
| Tooltip, | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import { useFormContext } from "react-hook-form"; | |||
| import { Controller, useFormContext } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import StyledDataGrid from "../StyledDataGrid"; | |||
| import { useCallback, useEffect, useMemo } from "react"; | |||
| @@ -31,6 +35,10 @@ 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; | |||
| @@ -44,11 +52,14 @@ type EntryError = | |||
| // type PoQcRow = TableRow<Partial<PurchaseQcResult>, EntryError>; | |||
| const StockInForm: React.FC<Props> = ({ | |||
| const StockInForm: React.FC<Props> = ({ | |||
| // qc, | |||
| itemDetail, | |||
| }) => { | |||
| const { t } = useTranslation(); | |||
| }) => { | |||
| const { | |||
| t, | |||
| i18n: { language }, | |||
| } = useTranslation(); | |||
| const apiRef = useGridApiRef(); | |||
| const { | |||
| register, | |||
| @@ -62,18 +73,33 @@ const StockInForm: React.FC<Props> = ({ | |||
| setError, | |||
| clearErrors, | |||
| } = useFormContext<StockInInput>(); | |||
| console.log(itemDetail) | |||
| console.log(itemDetail); | |||
| useEffect(() => { | |||
| console.log("triggered"); | |||
| // receiptDate default tdy | |||
| setValue("receiptDate", dayjs().add(0, "month").format(INPUT_DATE_FORMAT)); | |||
| setValue("status", "received"); | |||
| }, []); | |||
| useEffect(() => { | |||
| console.log("triggered") | |||
| setValue("status", "received") | |||
| }, []) | |||
| 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]) | |||
| return ( | |||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | |||
| <Grid item xs={12}> | |||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||
| {t("Qc Detail")} | |||
| {t("Stock In Detail")} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid | |||
| @@ -95,14 +121,38 @@ const StockInForm: React.FC<Props> = ({ | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={4}> | |||
| <TextField | |||
| label={t("receiptDate")} | |||
| fullWidth | |||
| {...register("receiptDate", { | |||
| required: "receiptDate required!", | |||
| })} | |||
| error={Boolean(errors.receiptDate)} | |||
| helperText={errors.receiptDate?.message} | |||
| <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"))} | |||
| 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={4}> | |||
| @@ -128,25 +178,75 @@ const StockInForm: React.FC<Props> = ({ | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={4}> | |||
| <TextField | |||
| label={t("productionDate")} | |||
| fullWidth | |||
| {...register("productionDate", { | |||
| // required: "productionDate required!", | |||
| })} | |||
| // error={Boolean(errors.productionDate)} | |||
| // helperText={errors.productionDate?.message} | |||
| <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} | |||
| 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={4}> | |||
| <TextField | |||
| label={t("expiryDate")} | |||
| fullWidth | |||
| {...register("expiryDate", { | |||
| required: "expiryDate required!", | |||
| })} | |||
| error={Boolean(errors.expiryDate)} | |||
| helperText={errors.expiryDate?.message} | |||
| <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} | |||
| 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> | |||
| @@ -52,6 +52,14 @@ const PoSearch: React.FC<Props> = ({ po }) => { | |||
| name: "code", | |||
| label: t("Code"), | |||
| }, | |||
| { | |||
| name: "orderDate", | |||
| label: t("OrderDate"), | |||
| }, | |||
| { | |||
| name: "status", | |||
| label: t("Status"), | |||
| }, | |||
| // { | |||
| // name: "name", | |||
| // label: t("Name"), | |||
| @@ -7,6 +7,10 @@ import { notFound } from "next/navigation"; | |||
| import PoSearchLoading from "./PoSearchLoading"; | |||
| import PoSearch from "./PoSearch"; | |||
| import { fetchPoList, PoResult } from "@/app/api/po"; | |||
| import dayjs from "dayjs"; | |||
| import arraySupport from "dayjs/plugin/arraySupport"; | |||
| import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
| dayjs.extend(arraySupport); | |||
| interface SubComponents { | |||
| Loading: typeof PoSearchLoading; | |||
| @@ -26,8 +30,14 @@ const PoSearchWrapper: React.FC<Props> & SubComponents = async ( | |||
| ] = await Promise.all([ | |||
| fetchPoList() | |||
| ]); | |||
| return <PoSearch po={po} />; | |||
| console.log(po) | |||
| const fixPoDate = po.map((p) => { | |||
| return ({ | |||
| ...p, | |||
| orderDate: dayjs(p.orderDate).format(OUTPUT_DATE_FORMAT) | |||
| }) | |||
| }) | |||
| return <PoSearch po={fixPoDate} />; | |||
| }; | |||
| PoSearchWrapper.Loading = PoSearchLoading; | |||
| @@ -29,7 +29,7 @@ const style = { | |||
| export var defaultScannerConfig: ScannerConfig = { | |||
| onUpdate: (err, result) => { | |||
| if (result) { | |||
| const data = JSON.parse(result.getText()) | |||
| const data = JSON.parse(result.getText()); | |||
| console.log(data); | |||
| } else return; | |||
| }, | |||
| @@ -48,19 +48,27 @@ export interface ScannerConfig { | |||
| delay?: number; // Delay between scans in milliseconds. Default is 500ms. | |||
| videoConstraints?: MediaTrackConstraints; // Video constraints to pass to the webcam. If not provided, the default constraints will be used. | |||
| formats?: BarcodeFormat[] | BarcodeStringFormat[]; // Array of barcode formats to decode. If not provided, all formats will be used. A smaller list may improve the speed of the scan. | |||
| stopStream?: boolean | |||
| stopStream?: boolean; | |||
| } | |||
| const ReactQrCodeScanner: React.FC<Props> = ({ | |||
| scannerConfig, | |||
| }) => { | |||
| const [stopStream, setStopStream] = useState(scannerConfig.stopStream || defaultScannerConfig.stopStream || false); | |||
| const ReactQrCodeScanner: React.FC<Props> = ({ scannerConfig }) => { | |||
| const [stopStream, setStopStream] = useState( | |||
| scannerConfig.stopStream || defaultScannerConfig.stopStream || false | |||
| ); | |||
| const [torchEnabled, setTorchEnabled] = useState<boolean>(false); | |||
| const _scannerConfig = useMemo(() => ({ | |||
| ...defaultScannerConfig, | |||
| ...scannerConfig, | |||
| }),[]) | |||
| // const _scannerConfig = useMemo(() => ({ | |||
| // ...defaultScannerConfig, | |||
| // ...scannerConfig, | |||
| // }),[]) | |||
| const [_scannerConfig, setScannerConfig] = useState<ScannerConfig>({ | |||
| ...defaultScannerConfig | |||
| }); | |||
| useEffect(() => { | |||
| setScannerConfig({ | |||
| ...defaultScannerConfig, | |||
| ...scannerConfig, | |||
| }); | |||
| }, []); | |||
| const SwitchOnOffScanner = useCallback(() => { | |||
| // Stop the QR Reader stream (fixes issue where the browser freezes when closing the modal) and then dismiss the modal one tick later | |||
| setStopStream((prev) => !prev); | |||
| @@ -71,21 +79,19 @@ const ReactQrCodeScanner: React.FC<Props> = ({ | |||
| }, []); | |||
| return ( | |||
| <> | |||
| {!stopStream ? ( | |||
| <BarcodeScanner | |||
| stopStream={stopStream} | |||
| torch={torchEnabled} | |||
| {..._scannerConfig} | |||
| /> | |||
| ) : undefined} | |||
| <Button onClick={SwitchOnOffTorch}> | |||
| {torchEnabled ? "off" : "on"} | |||
| </Button> | |||
| <Button onClick={SwitchOnOffScanner}> | |||
| {stopStream ? "start" : "stop"} | |||
| </Button> | |||
| </> | |||
| <> | |||
| {!stopStream ? ( | |||
| <BarcodeScanner | |||
| stopStream={stopStream} | |||
| torch={torchEnabled} | |||
| {..._scannerConfig} | |||
| /> | |||
| ) : undefined} | |||
| <Button onClick={SwitchOnOffTorch}>{torchEnabled ? "off" : "on"}</Button> | |||
| <Button onClick={SwitchOnOffScanner}> | |||
| {stopStream ? "start" : "stop"} | |||
| </Button> | |||
| </> | |||
| ); | |||
| }; | |||
| export default ReactQrCodeScanner; | |||