| @@ -12,6 +12,7 @@ export interface PostStockInLiineResponse<T> { | |||||
| id: number | null; | id: number | null; | ||||
| name: string; | name: string; | ||||
| code: string; | code: string; | ||||
| type?: string | |||||
| message: string | null; | message: string | null; | ||||
| errorPosition: string | keyof T; | errorPosition: string | keyof T; | ||||
| entity: StockInLine | StockInLine[] | entity: StockInLine | StockInLine[] | ||||
| @@ -70,22 +71,12 @@ export const testFetch = cache(async (id: number) => { | |||||
| }); | }); | ||||
| }); | }); | ||||
| export const testFetch2 = cache(async (id: number) => { | |||||
| return serverFetchJson<any[]>(`${BASE_API_URL}/qcResult/${id}`, { | |||||
| next: { tags: ["test"] }, | |||||
| export const fetchStockInLineInfo = cache(async (stockInLineId: number) => { | |||||
| return serverFetchJson<StockInLine>(`${BASE_API_URL}/stockInLine/${stockInLineId}`, { | |||||
| next: { tags: ["stockInLine"] }, | |||||
| }); | }); | ||||
| }); | }); | ||||
| export const test3 = cache(async (id: number) => { | |||||
| var endpoint = `${BASE_API_URL}/qcResult/${id}` | |||||
| // if (startDate.length > 0) endpoint += `&startDate=${startDate}` | |||||
| // if (endDate.length > 0) endpoint += `&endDate=${endDate}` | |||||
| // if (teamId > 0 ) endpoint += `&teamId=${teamId}` | |||||
| return serverFetchJson<any[]>(endpoint, { | |||||
| next: { tags: ["test"] }, | |||||
| }); | |||||
| }) | |||||
| export const createStockInLine = async (data: StockInLineEntry) => { | export const createStockInLine = async (data: StockInLineEntry) => { | ||||
| const stockInLine = await serverFetchJson<PostStockInLiineResponse<StockInLineEntry>>(`${BASE_API_URL}/stockInLine/create`, { | const stockInLine = await serverFetchJson<PostStockInLiineResponse<StockInLineEntry>>(`${BASE_API_URL}/stockInLine/create`, { | ||||
| method: "POST", | method: "POST", | ||||
| @@ -104,4 +95,13 @@ export const updateStockInLine = async (data: StockInLineEntry & ModalFormInput) | |||||
| return stockInLine | return stockInLine | ||||
| } | } | ||||
| export const checkPolAndCompletePo = async (poId: number) => { | |||||
| const po = await serverFetchJson<PostStockInLiineResponse<PoResult>>(`${BASE_API_URL}/po//check/${poId}`, { | |||||
| method: "POST", | |||||
| body: JSON.stringify({ poId }), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }); | |||||
| return po | |||||
| } | |||||
| @@ -48,6 +48,7 @@ export interface StockInLine { | |||||
| lotNo: string | lotNo: string | ||||
| poCode: string | poCode: string | ||||
| uom: Uom | uom: Uom | ||||
| defaultWarehouseId: number // id for now | |||||
| } | } | ||||
| export const fetchPoList = cache(async () => { | export const fetchPoList = cache(async () => { | ||||
| @@ -0,0 +1,11 @@ | |||||
| import { cache } from "react"; | |||||
| import "server-only"; | |||||
| import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
| import { BASE_API_URL } from "@/config/api"; | |||||
| export interface QrCodeInfo { | |||||
| stockInLineId?: number; | |||||
| itemId: number | |||||
| warehouseId?: number | |||||
| lotNo?: string | |||||
| } | |||||
| @@ -26,14 +26,14 @@ import { | |||||
| GridValidRowModel, | GridValidRowModel, | ||||
| useGridApiRef, | useGridApiRef, | ||||
| } from "@mui/x-data-grid"; | } from "@mui/x-data-grid"; | ||||
| import { useFormContext } from "react-hook-form"; | |||||
| import { set, useFormContext } from "react-hook-form"; | |||||
| import SaveIcon from "@mui/icons-material/Save"; | import SaveIcon from "@mui/icons-material/Save"; | ||||
| import DeleteIcon from "@mui/icons-material/Delete"; | import DeleteIcon from "@mui/icons-material/Delete"; | ||||
| import CancelIcon from "@mui/icons-material/Cancel"; | import CancelIcon from "@mui/icons-material/Cancel"; | ||||
| import { Add } from "@mui/icons-material"; | import { Add } from "@mui/icons-material"; | ||||
| import { Box, Button, Typography } from "@mui/material"; | import { Box, Button, Typography } from "@mui/material"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { GridApiCommunity } from "@mui/x-data-grid/internals"; | |||||
| import { GridApiCommunity, GridSlotsComponentsProps } from "@mui/x-data-grid/internals"; | |||||
| interface ResultWithId { | interface ResultWithId { | ||||
| id: string | number; | id: string | number; | ||||
| @@ -67,6 +67,7 @@ export interface InputDataGridProps<T, V, E> { | |||||
| _formKey: keyof T; | _formKey: keyof T; | ||||
| columns: GridColDef[]; | columns: GridColDef[]; | ||||
| validateRow: (newRow: GridRowModel<TableRow<V, E>>) => E; | validateRow: (newRow: GridRowModel<TableRow<V, E>>) => E; | ||||
| needAdd?: Boolean | |||||
| }; | }; | ||||
| export interface SelectionInputDataGridProps<T, V, E> { // thinking how do | export interface SelectionInputDataGridProps<T, V, E> { // thinking how do | ||||
| @@ -75,6 +76,7 @@ export interface SelectionInputDataGridProps<T, V, E> { // thinking how do | |||||
| _formKey: keyof T; | _formKey: keyof T; | ||||
| columns: GridColDef[]; | columns: GridColDef[]; | ||||
| validateRow: (newRow: GridRowModel<TableRow<V, E>>) => E; | validateRow: (newRow: GridRowModel<TableRow<V, E>>) => E; | ||||
| needAdd?: Boolean | |||||
| } | } | ||||
| export type Props<T, V, E> = InputDataGridProps<T, V, E> | SelectionInputDataGridProps<T, V, E> | export type Props<T, V, E> = InputDataGridProps<T, V, E> | SelectionInputDataGridProps<T, V, E> | ||||
| @@ -94,10 +96,11 @@ export class ProcessRowUpdateError<T, E> extends Error { | |||||
| // E == error | // E == error | ||||
| function InputDataGrid<T, V, E>({ | function InputDataGrid<T, V, E>({ | ||||
| apiRef, | apiRef, | ||||
| checkboxSelection, | |||||
| checkboxSelection = false, | |||||
| _formKey, | _formKey, | ||||
| columns, | columns, | ||||
| validateRow, | validateRow, | ||||
| needAdd, | |||||
| }: Props<T, V, E>) { | }: Props<T, V, E>) { | ||||
| const { | const { | ||||
| t, | t, | ||||
| @@ -113,6 +116,7 @@ function InputDataGrid<T, V, E>({ | |||||
| [] | [] | ||||
| ); | ); | ||||
| const list: TableRow<V, E>[] = getValues(formKey); | const list: TableRow<V, E>[] = getValues(formKey); | ||||
| // console.log(list) | |||||
| const [rows, setRows] = useState<TableRow<V, E>[]>(() => { | const [rows, setRows] = useState<TableRow<V, E>[]>(() => { | ||||
| const list: TableRow<V, E>[] = getValues(formKey); | const list: TableRow<V, E>[] = getValues(formKey); | ||||
| return list && list.length > 0 ? list : []; | return list && list.length > 0 ? list : []; | ||||
| @@ -348,9 +352,13 @@ function InputDataGrid<T, V, E>({ | |||||
| footer: FooterToolbar, | footer: FooterToolbar, | ||||
| noRowsOverlay: NoRowsOverlay, | noRowsOverlay: NoRowsOverlay, | ||||
| } : undefined} | } : undefined} | ||||
| slotProps={!checkboxSelection ? { | |||||
| slotProps={!checkboxSelection && Boolean(needAdd) ? { | |||||
| footer: { child: footer }, | footer: { child: footer }, | ||||
| } : undefined} | |||||
| }: undefined | |||||
| // slotProps={renderFooter ? { | |||||
| // footer: { child: footer }, | |||||
| // }: undefined | |||||
| } | |||||
| /> | /> | ||||
| ) | ) | ||||
| } | } | ||||
| @@ -28,9 +28,26 @@ import { | |||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| // import InputDataGrid, { TableRow } from "../InputDataGrid/InputDataGrid"; | // import InputDataGrid, { TableRow } from "../InputDataGrid/InputDataGrid"; | ||||
| import { GridColDef, GridRowModel, useGridApiRef } from "@mui/x-data-grid"; | |||||
| import { testFetch } from "@/app/api/po/actions"; | |||||
| import { useCallback, useContext, useEffect, useMemo, useState } from "react"; | |||||
| import { | |||||
| GridColDef, | |||||
| GridRowId, | |||||
| GridRowModel, | |||||
| useGridApiRef, | |||||
| } from "@mui/x-data-grid"; | |||||
| import { | |||||
| checkPolAndCompletePo, | |||||
| fetchStockInLineInfo, | |||||
| PurchaseQcResult, | |||||
| testFetch, | |||||
| } from "@/app/api/po/actions"; | |||||
| import { | |||||
| use, | |||||
| useCallback, | |||||
| useContext, | |||||
| useEffect, | |||||
| useMemo, | |||||
| useState, | |||||
| } from "react"; | |||||
| import { FormProvider, useForm } from "react-hook-form"; | import { FormProvider, useForm } from "react-hook-form"; | ||||
| import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; | import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; | ||||
| import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; | import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; | ||||
| @@ -46,6 +63,13 @@ import QrCodeScanner from "../QrCodeScanner"; | |||||
| import { CameraDevice, Html5Qrcode } from "html5-qrcode"; | import { CameraDevice, Html5Qrcode } from "html5-qrcode"; | ||||
| import { CameraContext } from "../Cameras/CameraProvider"; | import { CameraContext } from "../Cameras/CameraProvider"; | ||||
| import StyledDataGrid from "../StyledDataGrid"; | import StyledDataGrid from "../StyledDataGrid"; | ||||
| import { QrCodeInfo } from "@/app/api/qrcode"; | |||||
| import { fetchQcResult } from "@/app/api/qc/actions"; | |||||
| import PoQcStockInModal from "./PoQcStockInModal"; | |||||
| import ReactQrCodeScannerModal, { | |||||
| ScannerConfig, | |||||
| } from "../ReactQrCodeScanner/ReactQrCodeScanner"; | |||||
| import QrModal from "./QrModal"; | |||||
| type Props = { | type Props = { | ||||
| po: PoResult; | po: PoResult; | ||||
| @@ -72,7 +96,6 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||||
| const [open, setOpen] = useState(false); | const [open, setOpen] = useState(false); | ||||
| const [processedQty, setProcessedQty] = useState(row.processed); | const [processedQty, setProcessedQty] = useState(row.processed); | ||||
| const [currStatus, setCurrStatus] = useState(row.status); | const [currStatus, setCurrStatus] = useState(row.status); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| if (processedQty === row.qty) { | if (processedQty === row.qty) { | ||||
| setCurrStatus("completed".toUpperCase()); | setCurrStatus("completed".toUpperCase()); | ||||
| @@ -145,6 +168,13 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||||
| </> | </> | ||||
| ); | ); | ||||
| } | } | ||||
| const [tabIndex, setTabIndex] = useState(0); | |||||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||||
| (_e, newValue) => { | |||||
| setTabIndex(newValue); | |||||
| }, | |||||
| [] | |||||
| ); | |||||
| const [isOpenScanner, setOpenScanner] = useState(false); | const [isOpenScanner, setOpenScanner] = useState(false); | ||||
| const onOpenScanner = useCallback(() => { | const onOpenScanner = useCallback(() => { | ||||
| @@ -155,17 +185,33 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||||
| setOpenScanner(false); | setOpenScanner(false); | ||||
| }, []); | }, []); | ||||
| const handleScanSuccess = useCallback((result: string) => { | |||||
| console.log(result); | |||||
| const [itemInfo, setItemInfo] = useState< | |||||
| StockInLine & { warehouseId?: number } | |||||
| >(); | |||||
| const [putAwayOpen, setPutAwayOpen] = useState(false); | |||||
| // const [scannedInfo, setScannedInfo] = useState<QrCodeInfo>({} as QrCodeInfo); | |||||
| const closePutAwayModal = useCallback(() => { | |||||
| setPutAwayOpen(false); | |||||
| setItemInfo(undefined); | |||||
| }, []); | |||||
| const openPutAwayModal = useCallback(() => { | |||||
| setPutAwayOpen(true); | |||||
| }, []); | }, []); | ||||
| const [tabIndex, setTabIndex] = useState(0); | |||||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||||
| (_e, newValue) => { | |||||
| setTabIndex(newValue); | |||||
| }, | |||||
| [] | |||||
| ); | |||||
| 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 ( | return ( | ||||
| <> | <> | ||||
| @@ -180,30 +226,34 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||||
| {po.code} | {po.code} | ||||
| </Typography> | </Typography> | ||||
| </Grid> | </Grid> | ||||
| <Grid item> | |||||
| {/* go to scanner */} | |||||
| {/* scan item */} | |||||
| {/* putaway model form with defaultValues */} | |||||
| <QrCodeScanner | |||||
| cameras={cameras} | |||||
| isOpen={isOpenScanner} | |||||
| onClose={onCloseScanner} | |||||
| onScanSuccess={handleScanSuccess} | |||||
| /> | |||||
| <Button onClick={onOpenScanner}>bind</Button> | |||||
| </Grid> | |||||
| </Grid> | </Grid> | ||||
| <Grid container xs={12}> | |||||
| {/* <Grid container xs={12} justifyContent="space-between"> | |||||
| <Button onClick={handleComplete}>Complete</Button> | |||||
| </Grid> */} | |||||
| <Grid container xs={12} justifyContent="space-between"> | |||||
| <Grid item xs={8}> | |||||
| <Tabs | <Tabs | ||||
| value={tabIndex} | value={tabIndex} | ||||
| onChange={handleTabChange} | onChange={handleTabChange} | ||||
| variant="scrollable" | variant="scrollable" | ||||
| > | > | ||||
| <Tab label={t("General")} iconPosition="end" /> | <Tab label={t("General")} iconPosition="end" /> | ||||
| <Tab label={t("Bind Storage")} iconPosition="end" /> | |||||
| {/* <Tab label={t("Bind Storage")} iconPosition="end" /> */} | |||||
| </Tabs> | </Tabs> | ||||
| </Grid> | </Grid> | ||||
| {/* tab 1 */} | |||||
| {/* <Grid item xs={4}> */} | |||||
| {/* scanner */} | |||||
| {/* </Grid> */} | |||||
| <Grid item xs={4} display="flex" justifyContent="end" alignItems="end"> | |||||
| <QrModal | |||||
| open={isOpenScanner} | |||||
| onClose={onCloseScanner} | |||||
| warehouse={warehouse} | |||||
| /> | |||||
| <Button onClick={onOpenScanner}>bind</Button> | |||||
| </Grid> | |||||
| </Grid> | |||||
| {/* tab 1 */} | |||||
| <Grid sx={{ display: tabIndex === 0 ? "block" : "none" }}> | <Grid sx={{ display: tabIndex === 0 ? "block" : "none" }}> | ||||
| <TableContainer component={Paper}> | <TableContainer component={Paper}> | ||||
| <Table aria-label="collapsible table"> | <Table aria-label="collapsible table"> | ||||
| @@ -238,6 +288,18 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||||
| /> */} | /> */} | ||||
| </Grid> | </Grid> | ||||
| </Stack> | </Stack> | ||||
| {itemInfo !== undefined && ( | |||||
| <> | |||||
| <PoQcStockInModal | |||||
| type={"putaway"} | |||||
| open={putAwayOpen} | |||||
| warehouse={warehouse} | |||||
| setItemDetail={setItemInfo} | |||||
| onClose={closePutAwayModal} | |||||
| itemDetail={itemInfo} | |||||
| /> | |||||
| </> | |||||
| )} | |||||
| </> | </> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -20,23 +20,14 @@ type Props = { | |||||
| }; | }; | ||||
| const PoDetailWrapper: React.FC<Props> & SubComponents = async ({ id }) => { | const PoDetailWrapper: React.FC<Props> & SubComponents = async ({ id }) => { | ||||
| const [ | |||||
| poWithStockInLine, | |||||
| warehouse, | |||||
| qc, | |||||
| ] = await Promise.all([ | |||||
| const [poWithStockInLine, warehouse, qc] = await Promise.all([ | |||||
| fetchPoWithStockInLines(id), | fetchPoWithStockInLines(id), | ||||
| fetchWarehouseList(), | fetchWarehouseList(), | ||||
| fetchQcItemCheck() | |||||
| ]) | |||||
| fetchQcItemCheck(), | |||||
| ]); | |||||
| // const poWithStockInLine = await fetchPoWithStockInLines(id) | // const poWithStockInLine = await fetchPoWithStockInLines(id) | ||||
| console.log(poWithStockInLine) | |||||
| console.log(warehouse) | |||||
| console.log(qc) | |||||
| return <PoDetail po={poWithStockInLine} qc={qc} warehouse={warehouse}/>; | |||||
| return <PoDetail po={poWithStockInLine} qc={qc} warehouse={warehouse} />; | |||||
| }; | }; | ||||
| PoDetailWrapper.Loading = PoDetailLoading; | PoDetailWrapper.Loading = PoDetailLoading; | ||||
| @@ -33,17 +33,14 @@ import ShoppingCartIcon from "@mui/icons-material/ShoppingCart"; | |||||
| import { QcItemWithChecks } from "src/app/api/qc"; | import { QcItemWithChecks } from "src/app/api/qc"; | ||||
| import PlayArrowIcon from "@mui/icons-material/PlayArrow"; | import PlayArrowIcon from "@mui/icons-material/PlayArrow"; | ||||
| import { PurchaseOrderLine, StockInLine } from "@/app/api/po"; | import { PurchaseOrderLine, StockInLine } from "@/app/api/po"; | ||||
| import { | |||||
| createStockInLine, | |||||
| PurchaseQcResult, | |||||
| } from "@/app/api/po/actions"; | |||||
| import { createStockInLine, PurchaseQcResult } from "@/app/api/po/actions"; | |||||
| import { useSearchParams } from "next/navigation"; | import { useSearchParams } from "next/navigation"; | ||||
| import { | import { | ||||
| returnWeightUnit, | returnWeightUnit, | ||||
| calculateWeight, | calculateWeight, | ||||
| stockInLineStatusMap, | stockInLineStatusMap, | ||||
| } from "@/app/utils/formatUtil"; | } from "@/app/utils/formatUtil"; | ||||
| import PoQcStockInModal from "./PoQcStockInModal"; | |||||
| // import PoQcStockInModal from "./PoQcStockInModal"; | |||||
| import NotificationImportantIcon from "@mui/icons-material/NotificationImportant"; | import NotificationImportantIcon from "@mui/icons-material/NotificationImportant"; | ||||
| import { WarehouseResult } from "@/app/api/warehouse"; | import { WarehouseResult } from "@/app/api/warehouse"; | ||||
| import LooksOneIcon from "@mui/icons-material/LooksOne"; | import LooksOneIcon from "@mui/icons-material/LooksOne"; | ||||
| @@ -57,6 +54,8 @@ import QrCodeIcon from "@mui/icons-material/QrCode"; | |||||
| import { downloadFile } from "@/app/utils/commonUtil"; | import { downloadFile } from "@/app/utils/commonUtil"; | ||||
| import { fetchPoQrcode } from "@/app/api/pdf/actions"; | import { fetchPoQrcode } from "@/app/api/pdf/actions"; | ||||
| import { fetchQcResult } from "@/app/api/qc/actions"; | import { fetchQcResult } from "@/app/api/qc/actions"; | ||||
| import PoQcStockInModal from "./PoQcStockInModal"; | |||||
| interface ResultWithId { | interface ResultWithId { | ||||
| id: number; | id: number; | ||||
| } | } | ||||
| @@ -116,7 +115,9 @@ function PoInputGrid({ | |||||
| ); | ); | ||||
| console.log(stockInLine); | console.log(stockInLine); | ||||
| const [entries, setEntries] = useState<StockInLineRow[]>(stockInLine || []); | const [entries, setEntries] = useState<StockInLineRow[]>(stockInLine || []); | ||||
| const [modalInfo, setModalInfo] = useState<StockInLine & {qcResult?: PurchaseQcResult[]}>(); | |||||
| const [modalInfo, setModalInfo] = useState< | |||||
| StockInLine & { qcResult?: PurchaseQcResult[] } | |||||
| >(); | |||||
| const [qcOpen, setQcOpen] = useState(false); | const [qcOpen, setQcOpen] = useState(false); | ||||
| const [escalOpen, setEscalOpen] = useState(false); | const [escalOpen, setEscalOpen] = useState(false); | ||||
| const [stockInOpen, setStockInOpen] = useState(false); | const [stockInOpen, setStockInOpen] = useState(false); | ||||
| @@ -176,19 +177,9 @@ function PoInputGrid({ | |||||
| }, | }, | ||||
| [createStockInLine] | [createStockInLine] | ||||
| ); | ); | ||||
| const fetchQcDefaultValue = useCallback(async () => { | |||||
| // const authHeader = axiosInstance.defaults.headers['Authorization']; | |||||
| // if (!authHeader) { | |||||
| // return; // Exit the function if the token is not set | |||||
| // } | |||||
| // console.log(authHeader) | |||||
| // console.log(NEXT_PUBLIC_API_URL) | |||||
| // const res = await axiosInstance.get<QcItemWithChecks[]>(`${NEXT_PUBLIC_API_URL}/qcResult/${itemDetail.id}`) | |||||
| // const res = await testFetch2(itemDetail.id) | |||||
| const res = await fetchQcResult(itemDetail.id); | |||||
| console.log(res); | |||||
| return res | |||||
| }, [axiosInstance]); | |||||
| const fetchQcDefaultValue = useCallback(async (stockInLineId: GridRowId) => { | |||||
| return await fetchQcResult(stockInLineId as number); | |||||
| }, []); | |||||
| const handleQC = useCallback( | const handleQC = useCallback( | ||||
| (id: GridRowId, params: any) => async () => { | (id: GridRowId, params: any) => async () => { | ||||
| @@ -196,11 +187,12 @@ function PoInputGrid({ | |||||
| ...prev, | ...prev, | ||||
| [id]: { mode: GridRowModes.View }, | [id]: { mode: GridRowModes.View }, | ||||
| })); | })); | ||||
| const qcResult = await fetchQcDefaultValue(); | |||||
| console.log(qcResult) | |||||
| const qcResult = await fetchQcDefaultValue(id); | |||||
| console.log(params.row); | |||||
| console.log(qcResult); | |||||
| setModalInfo({ | setModalInfo({ | ||||
| ...params.row, | ...params.row, | ||||
| qcResult: qcResult | |||||
| qcResult: qcResult, | |||||
| }); | }); | ||||
| // set default values | // set default values | ||||
| setTimeout(() => { | setTimeout(() => { | ||||
| @@ -414,7 +406,7 @@ function PoInputGrid({ | |||||
| }} | }} | ||||
| disabled={ | disabled={ | ||||
| stockInLineStatusMap[status] <= 0 || | stockInLineStatusMap[status] <= 0 || | ||||
| stockInLineStatusMap[status] >= 5 | |||||
| stockInLineStatusMap[status] >= 4 | |||||
| } | } | ||||
| // set _isNew to false after posting | // set _isNew to false after posting | ||||
| // or check status | // or check status | ||||
| @@ -429,7 +421,7 @@ function PoInputGrid({ | |||||
| color: "primary.main", | color: "primary.main", | ||||
| // marginRight: 1, | // marginRight: 1, | ||||
| }} | }} | ||||
| disabled={stockInLineStatusMap[status] !== 6} | |||||
| disabled={stockInLineStatusMap[status] <= 2 || stockInLineStatusMap[status] >= 7} | |||||
| // set _isNew to false after posting | // set _isNew to false after posting | ||||
| // or check status | // or check status | ||||
| onClick={handleStockIn(params.row.id, params)} | onClick={handleStockIn(params.row.id, params)} | ||||
| @@ -563,13 +555,6 @@ function PoInputGrid({ | |||||
| [apiRef] | [apiRef] | ||||
| ); | ); | ||||
| // useEffect(() => { | |||||
| // const total = entries.reduce( | |||||
| // (acc, curr) => acc + (curr.acceptedQty || 0), | |||||
| // 0 | |||||
| // ); | |||||
| // setDefaultQty(itemDetail.qty - total); | |||||
| // }, [entries]); | |||||
| const footer = ( | const footer = ( | ||||
| <Box display="flex" gap={2} alignItems="center"> | <Box display="flex" gap={2} alignItems="center"> | ||||
| <Button | <Button | ||||
| @@ -584,6 +569,9 @@ function PoInputGrid({ | |||||
| </Button> | </Button> | ||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| useEffect(() => { | |||||
| console.log(modalInfo); | |||||
| }, [modalInfo]); | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <StyledDataGrid | <StyledDataGrid | ||||
| @@ -631,46 +619,58 @@ function PoInputGrid({ | |||||
| footer: { child: footer }, | footer: { child: footer }, | ||||
| }} | }} | ||||
| /> | /> | ||||
| <> | |||||
| <PoQcStockInModal | |||||
| type={"qc"} | |||||
| setEntries={setEntries} | |||||
| qc={qc} | |||||
| open={qcOpen} | |||||
| onClose={closeQcModal} | |||||
| itemDetail={modalInfo!!} | |||||
| /> | |||||
| </> | |||||
| <> | |||||
| <PoQcStockInModal | |||||
| type={"escalation"} | |||||
| setEntries={setEntries} | |||||
| // qc={qc} | |||||
| open={escalOpen} | |||||
| onClose={closeEscalationModal} | |||||
| itemDetail={modalInfo!!} | |||||
| /> | |||||
| </> | |||||
| <> | |||||
| <PoQcStockInModal | |||||
| type={"stockIn"} | |||||
| setEntries={setEntries} | |||||
| // qc={qc} | |||||
| open={stockInOpen} | |||||
| onClose={closeStockInModal} | |||||
| itemDetail={modalInfo!!} | |||||
| /> | |||||
| </> | |||||
| <> | |||||
| <PoQcStockInModal | |||||
| type={"putaway"} | |||||
| setEntries={setEntries} | |||||
| open={putAwayOpen} | |||||
| warehouse={warehouse} | |||||
| onClose={closePutAwayModal} | |||||
| itemDetail={modalInfo!!} | |||||
| /> | |||||
| </> | |||||
| {modalInfo !== undefined && ( | |||||
| <> | |||||
| <PoQcStockInModal | |||||
| type={"qc"} | |||||
| setEntries={setEntries} | |||||
| setItemDetail={setModalInfo} | |||||
| qc={qc} | |||||
| open={qcOpen} | |||||
| onClose={closeQcModal} | |||||
| itemDetail={modalInfo} | |||||
| /> | |||||
| </> | |||||
| )} | |||||
| {modalInfo !== undefined && ( | |||||
| <> | |||||
| <PoQcStockInModal | |||||
| type={"escalation"} | |||||
| setEntries={setEntries} | |||||
| setItemDetail={setModalInfo} | |||||
| // qc={qc} | |||||
| open={escalOpen} | |||||
| onClose={closeEscalationModal} | |||||
| itemDetail={modalInfo} | |||||
| /> | |||||
| </> | |||||
| )} | |||||
| {modalInfo !== undefined && ( | |||||
| <> | |||||
| <PoQcStockInModal | |||||
| type={"stockIn"} | |||||
| setEntries={setEntries} | |||||
| // qc={qc} | |||||
| setItemDetail={setModalInfo} | |||||
| open={stockInOpen} | |||||
| onClose={closeStockInModal} | |||||
| itemDetail={modalInfo} | |||||
| /> | |||||
| </> | |||||
| )} | |||||
| {modalInfo !== undefined && ( | |||||
| <> | |||||
| <PoQcStockInModal | |||||
| type={"putaway"} | |||||
| setEntries={setEntries} | |||||
| setItemDetail={setModalInfo} | |||||
| open={putAwayOpen} | |||||
| warehouse={warehouse} | |||||
| onClose={closePutAwayModal} | |||||
| itemDetail={modalInfo} | |||||
| /> | |||||
| </> | |||||
| )} | |||||
| </> | </> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -1,13 +1,27 @@ | |||||
| "use client"; | "use client"; | ||||
| import { ModalFormInput, PurchaseQCInput, PurchaseQcResult, StockInInput, StockInLineEntry, updateStockInLine } from "@/app/api/po/actions"; | |||||
| import { | |||||
| ModalFormInput, | |||||
| PurchaseQCInput, | |||||
| PurchaseQcResult, | |||||
| StockInInput, | |||||
| StockInLineEntry, | |||||
| updateStockInLine, | |||||
| } from "@/app/api/po/actions"; | |||||
| import { Box, Button, Modal, ModalProps, Stack } from "@mui/material"; | import { Box, Button, Modal, ModalProps, Stack } from "@mui/material"; | ||||
| import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; | |||||
| import { | |||||
| Dispatch, | |||||
| SetStateAction, | |||||
| useCallback, | |||||
| useEffect, | |||||
| useMemo, | |||||
| useState, | |||||
| } from "react"; | |||||
| import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import QcForm from "./QcForm"; | import QcForm from "./QcForm"; | ||||
| import { QcItemWithChecks } from "@/app/api/qc"; | import { QcItemWithChecks } from "@/app/api/qc"; | ||||
| import { Check, CurrencyYuanRounded } from "@mui/icons-material"; | |||||
| import { Check, CurrencyYuanRounded, TtyTwoTone } from "@mui/icons-material"; | |||||
| import { StockInLine } from "@/app/api/po"; | import { StockInLine } from "@/app/api/po"; | ||||
| import { useSearchParams } from "next/navigation"; | import { useSearchParams } from "next/navigation"; | ||||
| import { StockInLineRow } from "./PoInputGrid"; | import { StockInLineRow } from "./PoInputGrid"; | ||||
| @@ -15,35 +29,38 @@ import EscalationForm from "./EscalationForm"; | |||||
| import StockInForm from "./StockInForm"; | import StockInForm from "./StockInForm"; | ||||
| import PutawayForm from "./PutawayForm"; | import PutawayForm from "./PutawayForm"; | ||||
| import { stockInLineStatusMap } from "@/app/utils/formatUtil"; | import { stockInLineStatusMap } from "@/app/utils/formatUtil"; | ||||
| import dayjs from "dayjs"; | |||||
| import arraySupport from "dayjs/plugin/arraySupport"; | |||||
| dayjs.extend(arraySupport) | |||||
| interface CommonProps extends Omit<ModalProps, "children"> { | interface CommonProps extends Omit<ModalProps, "children"> { | ||||
| setEntries: Dispatch<SetStateAction<StockInLineRow[]>> | |||||
| itemDetail: StockInLine & {qcResult?: PurchaseQcResult[]}; | |||||
| setEntries?: Dispatch<SetStateAction<StockInLineRow[]>>; | |||||
| itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] }; | |||||
| setItemDetail: Dispatch<SetStateAction<(StockInLine & { | |||||
| warehouseId?: number; | |||||
| }) | undefined>> | |||||
| qc?: QcItemWithChecks[]; | qc?: QcItemWithChecks[]; | ||||
| warehouse?: any[]; | warehouse?: any[]; | ||||
| type: "qc" | "stockIn" | "escalation" | "putaway" | |||||
| type: "qc" | "stockIn" | "escalation" | "putaway"; | |||||
| } | } | ||||
| interface QcProps extends CommonProps { | interface QcProps extends CommonProps { | ||||
| qc: QcItemWithChecks[]; | qc: QcItemWithChecks[]; | ||||
| type: "qc" | |||||
| } | |||||
| type: "qc"; | |||||
| } | |||||
| interface StockInProps extends CommonProps { | interface StockInProps extends CommonProps { | ||||
| // naming | // naming | ||||
| type: "stockIn" | |||||
| type: "stockIn"; | |||||
| } | } | ||||
| interface PutawayProps extends CommonProps { | interface PutawayProps extends CommonProps { | ||||
| // naming | |||||
| // warehouse: any[]; | |||||
| warehouse: any[]; | warehouse: any[]; | ||||
| type: "putaway" | |||||
| type: "putaway"; | |||||
| } | } | ||||
| interface EscalationProps extends CommonProps { | interface EscalationProps extends CommonProps { | ||||
| // naming | // naming | ||||
| type: "escalation" | |||||
| type: "escalation"; | |||||
| } | } | ||||
| type Props = QcProps | StockInProps | EscalationProps | PutawayProps; | |||||
| type Props = QcProps | StockInProps | PutawayProps | EscalationProps; | |||||
| const style = { | const style = { | ||||
| position: "absolute", | position: "absolute", | ||||
| top: "50%", | top: "50%", | ||||
| @@ -55,35 +72,29 @@ const style = { | |||||
| pb: 10, | pb: 10, | ||||
| width: { xs: "80%", sm: "80%", md: "80%" }, | width: { xs: "80%", sm: "80%", md: "80%" }, | ||||
| }; | }; | ||||
| const PoQcStockInModal: React.FC<Props> = ({ | const PoQcStockInModal: React.FC<Props> = ({ | ||||
| type, | type, | ||||
| setEntries, | setEntries, | ||||
| open, | open, | ||||
| onClose, | |||||
| itemDetail, | |||||
| onClose, | |||||
| itemDetail, | |||||
| setItemDetail, | |||||
| qc, | qc, | ||||
| warehouse, | warehouse, | ||||
| }) => { | |||||
| console.log(itemDetail) | |||||
| }) => { | |||||
| const [serverError, setServerError] = useState(""); | const [serverError, setServerError] = useState(""); | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const params = useSearchParams() | |||||
| console.log(params.get("id")) | |||||
| const [defaultValues, setDefaultValues] = useState({}); | |||||
| const defaultValue = useMemo(() => { | |||||
| // switch (type) { | |||||
| // case "qc": | |||||
| // return {qcResult: itemDetail.qcResult} | |||||
| // } | |||||
| // return {} | |||||
| return {...itemDetail} | |||||
| }, []) | |||||
| // const formProps = useForm<ModalFormInput>({ | |||||
| // defaultValues: defaultValues ? defaultValues : {}, | |||||
| // }); | |||||
| const params = useSearchParams(); | |||||
| console.log(params.get("id")); | |||||
| console.log(itemDetail); | |||||
| console.log(itemDetail.qcResult); | |||||
| const formProps = useForm<ModalFormInput>({ | const formProps = useForm<ModalFormInput>({ | ||||
| defaultValues: defaultValue | |||||
| defaultValues: { | |||||
| ...itemDetail, | |||||
| }, | |||||
| }); | }); | ||||
| // console.log(formProps); | |||||
| const errors = formProps.formState.errors; | const errors = formProps.formState.errors; | ||||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | ||||
| (...args) => { | (...args) => { | ||||
| @@ -94,27 +105,52 @@ const PoQcStockInModal: React.FC<Props> = ({ | |||||
| ); | ); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| setDefaultValues({}); | |||||
| }, []); | |||||
| // setDefaultValues({...itemDetail}); | |||||
| if (!itemDetail) { | |||||
| console.log(itemDetail); | |||||
| } | |||||
| }, [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 onSubmit = useCallback<SubmitHandler<ModalFormInput & {}>>( | const onSubmit = useCallback<SubmitHandler<ModalFormInput & {}>>( | ||||
| async (data, event) => { | async (data, event) => { | ||||
| let hasErrors = false; | let hasErrors = false; | ||||
| console.log(errors); | console.log(errors); | ||||
| console.log(data); | console.log(data); | ||||
| console.log(itemDetail); | console.log(itemDetail); | ||||
| console.log(data.receiptDate) | |||||
| console.log(fix0IndexedDate(data.receiptDate)) | |||||
| try { | try { | ||||
| // add checking | // add checking | ||||
| // const qty = data.sampleRate | // const qty = data.sampleRate | ||||
| //////////////////////// modify this mess later ////////////////////// | //////////////////////// modify this mess later ////////////////////// | ||||
| var productionDate = null | |||||
| var acceptedQty = null | |||||
| var productionDate = null; | |||||
| var expiryDate = null; | |||||
| var receiptDate = null; | |||||
| var acceptedQty = null; | |||||
| if (data.productionDate && data.productionDate.length > 0) { | if (data.productionDate && data.productionDate.length > 0) { | ||||
| productionDate = data.productionDate | |||||
| productionDate = fix0IndexedDate(data.productionDate); | |||||
| } | |||||
| if (data.expiryDate && data.expiryDate.length > 0) { | |||||
| expiryDate = fix0IndexedDate(data.expiryDate); | |||||
| } | |||||
| if (data.receiptDate && data.receiptDate.length > 0) { | |||||
| receiptDate = fix0IndexedDate(data.receiptDate); | |||||
| } | } | ||||
| // if () | |||||
| if (data.qcResult) { | if (data.qcResult) { | ||||
| acceptedQty = itemDetail.acceptedQty - data.qcResult.reduce((acc, curr) => acc + curr.failQty, 0) | |||||
| acceptedQty = | |||||
| itemDetail.acceptedQty - | |||||
| data.qcResult.reduce((acc, curr) => acc + curr.failQty, 0); | |||||
| } | } | ||||
| const args = { | const args = { | ||||
| id: itemDetail.id, | id: itemDetail.id, | ||||
| @@ -123,36 +159,43 @@ const PoQcStockInModal: React.FC<Props> = ({ | |||||
| itemId: itemDetail.itemId, | itemId: itemDetail.itemId, | ||||
| ...data, | ...data, | ||||
| productionDate: productionDate, | productionDate: productionDate, | ||||
| expiryDate: expiryDate, | |||||
| receiptDate: receiptDate, | |||||
| } as StockInLineEntry & ModalFormInput; | } as StockInLineEntry & ModalFormInput; | ||||
| ////////////////////////////////////////////////////////////////////// | ////////////////////////////////////////////////////////////////////// | ||||
| console.log(args) | |||||
| console.log(args); | |||||
| // return | // return | ||||
| if (hasErrors) { | if (hasErrors) { | ||||
| setServerError(t("An error has occurred. Please try again later.")); | setServerError(t("An error has occurred. Please try again later.")); | ||||
| return false; | return false; | ||||
| } | } | ||||
| const res = await updateStockInLine(args) | |||||
| const res = await updateStockInLine(args); | |||||
| if (Boolean(res.id)) { | if (Boolean(res.id)) { | ||||
| // update entries | // update entries | ||||
| const newEntries = res.entity as StockInLine[] | |||||
| setEntries((prev) => { | |||||
| const updatedEntries = [...prev]; // Create a new array | |||||
| newEntries.forEach((item) => { | |||||
| const index = updatedEntries.findIndex(p => p.id === item.id); | |||||
| if (index !== -1) { | |||||
| // Update existing item | |||||
| updatedEntries[index] = item; | |||||
| } else { | |||||
| // Add new item | |||||
| updatedEntries.push(item); | |||||
| } | |||||
| const newEntries = res.entity as StockInLine[]; | |||||
| console.log(newEntries) | |||||
| if (setEntries) { | |||||
| setEntries((prev) => { | |||||
| const updatedEntries = [...prev]; // Create a new array | |||||
| newEntries.forEach((item) => { | |||||
| const index = updatedEntries.findIndex((p) => p.id === item.id); | |||||
| if (index !== -1) { | |||||
| // Update existing item | |||||
| console.log(item) | |||||
| updatedEntries[index] = item; | |||||
| } else { | |||||
| // Add new item | |||||
| updatedEntries.push(item); | |||||
| } | |||||
| }); | |||||
| return updatedEntries; // Return the new array | |||||
| }); | }); | ||||
| return updatedEntries; // Return the new array | |||||
| }) | |||||
| } | |||||
| // add loading | // add loading | ||||
| closeHandler({}, "backdropClick") | |||||
| setItemDetail(undefined) | |||||
| closeHandler({}, "backdropClick"); | |||||
| } | } | ||||
| console.log(res) | |||||
| console.log(res); | |||||
| // if (res) | // if (res) | ||||
| } catch (e) { | } catch (e) { | ||||
| // server error | // server error | ||||
| @@ -162,39 +205,49 @@ const PoQcStockInModal: React.FC<Props> = ({ | |||||
| }, | }, | ||||
| [t, itemDetail] | [t, itemDetail] | ||||
| ); | ); | ||||
| const renderSubmitButton = useMemo((): Boolean => { | const renderSubmitButton = useMemo((): Boolean => { | ||||
| if (itemDetail) { | if (itemDetail) { | ||||
| const status = itemDetail.status | |||||
| console.log(status) | |||||
| const status = itemDetail.status; | |||||
| console.log(status); | |||||
| switch (type) { | switch (type) { | ||||
| case "qc": | case "qc": | ||||
| return stockInLineStatusMap[status] === 1 | |||||
| case "putaway": | |||||
| return stockInLineStatusMap[status] === 7 | |||||
| return stockInLineStatusMap[status] >= 1 && stockInLineStatusMap[status] <= 2; | |||||
| case "escalation": | |||||
| return stockInLineStatusMap[status] === 1 || stockInLineStatusMap[status] >= 3 || stockInLineStatusMap[status] <= 5; | |||||
| case "stockIn": | case "stockIn": | ||||
| return stockInLineStatusMap[status] === 6 | |||||
| return stockInLineStatusMap[status] >= 3 && stockInLineStatusMap[status] <= 6; | |||||
| case "putaway": | |||||
| return stockInLineStatusMap[status] === 7; | |||||
| default: | default: | ||||
| return false; // Handle unexpected type | return false; // Handle unexpected type | ||||
| } | } | ||||
| } else return false | |||||
| }, [type, itemDetail]) | |||||
| useEffect(() => { | |||||
| console.log(renderSubmitButton) | |||||
| }, [renderSubmitButton]) | |||||
| } else return false; | |||||
| }, [type, itemDetail]); | |||||
| // useEffect(() => { | |||||
| // console.log(renderSubmitButton) | |||||
| // }, [renderSubmitButton]) | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <Modal open={open} onClose={closeHandler}> | |||||
| <FormProvider {...formProps}> | |||||
| <FormProvider {...formProps}> | |||||
| <Modal open={open} onClose={closeHandler}> | |||||
| <Box | <Box | ||||
| sx={style} | sx={style} | ||||
| component="form" | component="form" | ||||
| onSubmit={formProps.handleSubmit(onSubmit)} | onSubmit={formProps.handleSubmit(onSubmit)} | ||||
| > | > | ||||
| {type === "qc" && <QcForm qc={qc} itemDetail={itemDetail} />} | |||||
| {type === "stockIn" && <StockInForm itemDetail={itemDetail} />} | |||||
| {type === "escalation" && <EscalationForm itemDetail={itemDetail} />} | |||||
| {type === "putaway" && <PutawayForm itemDetail={itemDetail} warehouse={warehouse} />} | |||||
| {itemDetail !== undefined && type === "qc" && ( | |||||
| <QcForm qc={qc!!} itemDetail={itemDetail} /> | |||||
| )} | |||||
| {itemDetail !== undefined && type === "stockIn" && ( | |||||
| <StockInForm itemDetail={itemDetail} /> | |||||
| )} | |||||
| {itemDetail !== undefined && type === "escalation" && ( | |||||
| <EscalationForm itemDetail={itemDetail} /> | |||||
| )} | |||||
| {itemDetail !== undefined && type === "putaway" && ( | |||||
| <PutawayForm itemDetail={itemDetail} warehouse={warehouse!!} /> | |||||
| )} | |||||
| {renderSubmitButton ? ( | {renderSubmitButton ? ( | ||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | <Stack direction="row" justifyContent="flex-end" gap={1}> | ||||
| <Button | <Button | ||||
| @@ -207,11 +260,10 @@ const PoQcStockInModal: React.FC<Props> = ({ | |||||
| {t("submit")} | {t("submit")} | ||||
| </Button> | </Button> | ||||
| </Stack> | </Stack> | ||||
| ) : undefined | |||||
| } | |||||
| ) : undefined} | |||||
| </Box> | </Box> | ||||
| </FormProvider> | |||||
| </Modal> | |||||
| </Modal> | |||||
| </FormProvider> | |||||
| </> | </> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -4,10 +4,13 @@ import { PurchaseQcResult, PutawayInput } from "@/app/api/po/actions"; | |||||
| import { | import { | ||||
| Autocomplete, | Autocomplete, | ||||
| Box, | Box, | ||||
| Button, | |||||
| Card, | Card, | ||||
| CardContent, | CardContent, | ||||
| FormControl, | FormControl, | ||||
| Grid, | Grid, | ||||
| Modal, | |||||
| ModalProps, | |||||
| Stack, | Stack, | ||||
| TextField, | TextField, | ||||
| Tooltip, | Tooltip, | ||||
| @@ -35,8 +38,10 @@ import { GridEditInputCell } from "@mui/x-data-grid"; | |||||
| import { StockInLine } from "@/app/api/po"; | import { StockInLine } from "@/app/api/po"; | ||||
| import { WarehouseResult } from "@/app/api/warehouse"; | import { WarehouseResult } from "@/app/api/warehouse"; | ||||
| import { stockInLineStatusMap } from "@/app/utils/formatUtil"; | import { stockInLineStatusMap } from "@/app/utils/formatUtil"; | ||||
| import { QRCodeSVG } from 'qrcode.react'; | |||||
| import { QRCodeSVG } from "qrcode.react"; | |||||
| import { QrCode } from "../QrCode"; | import { QrCode } from "../QrCode"; | ||||
| import ReactQrCodeScanner, { ScannerConfig } from "../ReactQrCodeScanner/ReactQrCodeScanner"; | |||||
| import { QrCodeInfo } from "@/app/api/qrcode"; | |||||
| interface Props { | interface Props { | ||||
| itemDetail: StockInLine; | itemDetail: StockInLine; | ||||
| @@ -51,6 +56,18 @@ type EntryError = | |||||
| // type PoQcRow = TableRow<Partial<PurchaseQcResult>, EntryError>; | // type PoQcRow = TableRow<Partial<PurchaseQcResult>, EntryError>; | ||||
| const style = { | |||||
| position: "absolute", | |||||
| top: "50%", | |||||
| left: "50%", | |||||
| transform: "translate(-50%, -50%)", | |||||
| bgcolor: "background.paper", | |||||
| pt: 5, | |||||
| px: 5, | |||||
| pb: 10, | |||||
| width: "auto", | |||||
| }; | |||||
| const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse }) => { | const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse }) => { | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const apiRef = useGridApiRef(); | const apiRef = useGridApiRef(); | ||||
| @@ -108,18 +125,56 @@ const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse }) => { | |||||
| [] | [] | ||||
| ); | ); | ||||
| const qrContent = useMemo(() => ({ | |||||
| itemId: itemDetail.itemId, | |||||
| lotNo: itemDetail.lotNo, | |||||
| // expiryDate: itemDetail.expiryDate, | |||||
| // productionDate: itemDetail.productionDate, | |||||
| // supplier: itemDetail.supplier, | |||||
| // poCode: itemDetail.poCode, | |||||
| }),[itemDetail]) | |||||
| const qrContent = useMemo( | |||||
| () => ({ | |||||
| stockInLineId: itemDetail.id, | |||||
| itemId: itemDetail.itemId, | |||||
| lotNo: itemDetail.lotNo, | |||||
| // warehouseId: 1 // for testing | |||||
| // expiryDate: itemDetail.expiryDate, | |||||
| // productionDate: itemDetail.productionDate, | |||||
| // supplier: itemDetail.supplier, | |||||
| // poCode: itemDetail.poCode, | |||||
| }), | |||||
| [itemDetail] | |||||
| ); | |||||
| const [isOpenScanner, setOpenScanner] = useState(false); | |||||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | |||||
| (...args) => { | |||||
| setOpenScanner(false); | |||||
| }, | |||||
| [] | |||||
| ); | |||||
| const onOpenScanner = useCallback(() => { | |||||
| setOpenScanner(true); | |||||
| }, []); | |||||
| const onCloseScanner = useCallback(() => { | |||||
| setOpenScanner(false); | |||||
| }, []); | |||||
| const scannerConfig = useMemo<ScannerConfig>( | |||||
| () => ({ | |||||
| onUpdate: (err, result) => { | |||||
| if (result) { | |||||
| const data: QrCodeInfo = JSON.parse(result.getText()); | |||||
| console.log(data); | |||||
| if (data.warehouseId) { | |||||
| setWarehouseId(data.warehouseId); | |||||
| onCloseScanner() | |||||
| } | |||||
| } else return; | |||||
| }, | |||||
| }), | |||||
| [] | |||||
| ); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| setValue("status", "completed") | |||||
| }, []) | |||||
| setValue("status", "completed"); | |||||
| }, []); | |||||
| return ( | return ( | ||||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | <Grid container justifyContent="flex-start" alignItems="flex-start"> | ||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| @@ -190,7 +245,7 @@ const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse }) => { | |||||
| disabled | disabled | ||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={12}> | |||||
| <Grid item xs={6}> | |||||
| <TextField | <TextField | ||||
| label={t("expiryDate")} | label={t("expiryDate")} | ||||
| fullWidth | fullWidth | ||||
| @@ -199,6 +254,21 @@ const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse }) => { | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| <FormControl fullWidth> | |||||
| <Autocomplete | |||||
| noOptionsText={t("No Warehouse")} | |||||
| disableClearable | |||||
| disabled | |||||
| fullWidth | |||||
| defaultValue={options.find((o) => o.value === 1)} | |||||
| // onChange={onChange} | |||||
| getOptionLabel={(option) => option.label} | |||||
| options={options} | |||||
| renderInput={(params) => <TextField {...params} label="Default Warehouse"/>} | |||||
| /> | |||||
| </FormControl> | |||||
| </Grid> | |||||
| <Grid item xs={5.5}> | |||||
| <TextField | <TextField | ||||
| label={t("acceptedQty")} | label={t("acceptedQty")} | ||||
| fullWidth | fullWidth | ||||
| @@ -213,7 +283,10 @@ const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse }) => { | |||||
| helperText={errors.acceptedQty?.message} | helperText={errors.acceptedQty?.message} | ||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | |||||
| <Grid item xs={1}> | |||||
| <Button onClick={onOpenScanner}>bind</Button> | |||||
| </Grid> | |||||
| <Grid item xs={5.5}> | |||||
| <FormControl fullWidth> | <FormControl fullWidth> | ||||
| <Autocomplete | <Autocomplete | ||||
| noOptionsText={t("No Warehouse")} | noOptionsText={t("No Warehouse")} | ||||
| @@ -227,27 +300,29 @@ const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse }) => { | |||||
| /> | /> | ||||
| </FormControl> | </FormControl> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={12} style={{ display: 'flex', justifyContent: 'center' }}> | |||||
| <QrCode content={qrContent} sx={{width: 200, height: 200}}/> | |||||
| <Grid | |||||
| item | |||||
| xs={12} | |||||
| style={{ display: "flex", justifyContent: "center" }} | |||||
| > | |||||
| <QrCode content={qrContent} sx={{ width: 200, height: 200 }} /> | |||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| <Grid | |||||
| {/* <Grid | |||||
| container | container | ||||
| justifyContent="flex-start" | justifyContent="flex-start" | ||||
| alignItems="flex-start" | alignItems="flex-start" | ||||
| spacing={2} | spacing={2} | ||||
| sx={{ mt: 0.5 }} | sx={{ mt: 0.5 }} | ||||
| > | > | ||||
| {/* <Grid item xs={12}> | |||||
| <InputDataGrid<PutawayInput, PurchaseQcResult, EntryError> | |||||
| apiRef={apiRef} | |||||
| checkboxSelection={false} | |||||
| _formKey={"qcCheck"} | |||||
| columns={columns} | |||||
| validateRow={validation} | |||||
| /> | |||||
| </Grid> */} | |||||
| </Grid> | |||||
| <Button onClick={onOpenScanner}>bind</Button> | |||||
| </Grid> */} | |||||
| <Modal open={isOpenScanner} onClose={closeHandler}> | |||||
| <Box sx={style}> | |||||
| <ReactQrCodeScanner scannerConfig={scannerConfig} /> | |||||
| </Box> | |||||
| </Modal> | |||||
| </Grid> | </Grid> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -31,14 +31,14 @@ import QcSelect from "./QcSelect"; | |||||
| import { GridEditInputCell } from "@mui/x-data-grid"; | import { GridEditInputCell } from "@mui/x-data-grid"; | ||||
| import { StockInLine } from "@/app/api/po"; | import { StockInLine } from "@/app/api/po"; | ||||
| import { stockInLineStatusMap } from "@/app/utils/formatUtil"; | import { stockInLineStatusMap } from "@/app/utils/formatUtil"; | ||||
| import { fetchQcItemCheck } from "@/app/api/qc/actions"; | |||||
| import { fetchQcItemCheck, fetchQcResult } from "@/app/api/qc/actions"; | |||||
| import { QcItemWithChecks } from "@/app/api/qc"; | import { QcItemWithChecks } from "@/app/api/qc"; | ||||
| import axios from "@/app/(main)/axios/axiosInstance"; | import axios from "@/app/(main)/axios/axiosInstance"; | ||||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | import { NEXT_PUBLIC_API_URL } from "@/config/api"; | ||||
| import axiosInstance from "@/app/(main)/axios/axiosInstance"; | import axiosInstance from "@/app/(main)/axios/axiosInstance"; | ||||
| interface Props { | interface Props { | ||||
| itemDetail: StockInLine; | |||||
| itemDetail: StockInLine | |||||
| qc: QcItemWithChecks[]; | qc: QcItemWithChecks[]; | ||||
| } | } | ||||
| type EntryError = | type EntryError = | ||||
| @@ -65,24 +65,8 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail }) => { | |||||
| clearErrors, | clearErrors, | ||||
| } = useFormContext<PurchaseQCInput>(); | } = useFormContext<PurchaseQCInput>(); | ||||
| console.log(itemDetail); | console.log(itemDetail); | ||||
| console.log(defaultValues); | |||||
| // const [qc, setQc] = useState<QcItemWithChecks[]>([]) | |||||
| const fetchQcCheck = useCallback(async () => { | |||||
| const authHeader = axiosInstance.defaults.headers['Authorization']; | |||||
| if (!authHeader) { | |||||
| return; // Exit the function if the token is not set | |||||
| } | |||||
| const params = { | |||||
| itemId: itemDetail.itemId | |||||
| } | |||||
| console.log(params) | |||||
| const res = await axios.get<QcItemWithChecks[]>(`${NEXT_PUBLIC_API_URL}/qcCheck`, { params }) | |||||
| console.log(res) | |||||
| }, [axios]) | |||||
| useEffect(() => { | |||||
| fetchQcCheck() | |||||
| }, [fetchQcCheck]) | |||||
| const [recordQty, setRecordQty] = useState(0); | const [recordQty, setRecordQty] = useState(0); | ||||
| const columns = useMemo<GridColDef[]>( | const columns = useMemo<GridColDef[]>( | ||||
| () => [ | () => [ | ||||
| @@ -203,7 +187,7 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail }) => { | |||||
| spacing={2} | spacing={2} | ||||
| sx={{ mt: 0.5 }} | sx={{ mt: 0.5 }} | ||||
| > | > | ||||
| <Grid item xs={6}> | |||||
| <Grid item xs={12} lg={6}> | |||||
| <TextField | <TextField | ||||
| label={t("accepted Qty")} | label={t("accepted Qty")} | ||||
| fullWidth | fullWidth | ||||
| @@ -217,7 +201,7 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail }) => { | |||||
| helperText={errors.acceptedQty?.message} | helperText={errors.acceptedQty?.message} | ||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | |||||
| <Grid item xs={12} lg={6}> | |||||
| <TextField | <TextField | ||||
| label={t("Total record qty")} | label={t("Total record qty")} | ||||
| fullWidth | fullWidth | ||||
| @@ -230,7 +214,7 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail }) => { | |||||
| // helperText={errors.sampleRate?.message} | // helperText={errors.sampleRate?.message} | ||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={4}> | |||||
| <Grid item xs={12} lg={6}> | |||||
| <TextField | <TextField | ||||
| label={t("sampleRate")} | label={t("sampleRate")} | ||||
| fullWidth | fullWidth | ||||
| @@ -242,7 +226,7 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail }) => { | |||||
| helperText={errors.sampleRate?.message} | helperText={errors.sampleRate?.message} | ||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={4}> | |||||
| <Grid item xs={12} lg={6}> | |||||
| <TextField | <TextField | ||||
| label={t("sampleWeight")} | label={t("sampleWeight")} | ||||
| fullWidth | fullWidth | ||||
| @@ -253,7 +237,7 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail }) => { | |||||
| helperText={errors.sampleWeight?.message} | helperText={errors.sampleWeight?.message} | ||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={4}> | |||||
| <Grid item xs={12} lg={6}> | |||||
| <TextField | <TextField | ||||
| label={t("totalWeight")} | label={t("totalWeight")} | ||||
| fullWidth | fullWidth | ||||
| @@ -279,6 +263,7 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail }) => { | |||||
| _formKey={"qcResult"} | _formKey={"qcResult"} | ||||
| columns={columns} | columns={columns} | ||||
| validateRow={validation} | validateRow={validation} | ||||
| needAdd={itemDetail.status === "qc" || itemDetail.status === "pending"} | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| @@ -0,0 +1,42 @@ | |||||
| "use client" | |||||
| import { Box, Modal, ModalProps } from "@mui/material" | |||||
| import { useCallback } from "react"; | |||||
| interface Props extends Omit<ModalProps, "children"> { | |||||
| }; | |||||
| const style = { | |||||
| position: "absolute", | |||||
| top: "50%", | |||||
| left: "50%", | |||||
| transform: "translate(-50%, -50%)", | |||||
| bgcolor: "background.paper", | |||||
| pt: 5, | |||||
| px: 5, | |||||
| pb: 10, | |||||
| width: { xs: "80%", sm: "80%", md: "80%" }, | |||||
| }; | |||||
| const QrCodeScanner: React.FC<Props> = ({ | |||||
| open, | |||||
| }) => { | |||||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | |||||
| (...args) => { | |||||
| // onClose?.(...args); | |||||
| // reset(); | |||||
| }, | |||||
| [] | |||||
| ); | |||||
| return ( | |||||
| <Modal open={open} onClose={closeHandler}> | |||||
| <Box sx={style}> | |||||
| <input type="file" accept="image/*" capture="environment"> | |||||
| </input> | |||||
| </Box> | |||||
| </Modal>) | |||||
| } | |||||
| export default QrCodeScanner | |||||
| @@ -0,0 +1,167 @@ | |||||
| "use client"; | |||||
| import { Box, Button, Grid, Modal, ModalProps, Stack } from "@mui/material"; | |||||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||||
| import ReactQrCodeScanner, { | |||||
| ScannerConfig, | |||||
| } from "../ReactQrCodeScanner/ReactQrCodeScanner"; | |||||
| import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | |||||
| import { | |||||
| fetchStockInLineInfo, | |||||
| ModalFormInput, | |||||
| StockInLineEntry, | |||||
| updateStockInLine, | |||||
| } from "@/app/api/po/actions"; | |||||
| import PutawayForm from "./PutawayForm"; | |||||
| import { StockInLine } from "@/app/api/po"; | |||||
| import { WarehouseResult } from "@/app/api/warehouse"; | |||||
| import { QrCodeInfo } from "@/app/api/qrcode"; | |||||
| import { Check } from "@mui/icons-material"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| interface Props extends Omit<ModalProps, "children"> { | |||||
| warehouse: WarehouseResult[]; | |||||
| } | |||||
| const style = { | |||||
| position: "absolute", | |||||
| top: "50%", | |||||
| left: "50%", | |||||
| transform: "translate(-50%, -50%)", | |||||
| bgcolor: "background.paper", | |||||
| pt: 5, | |||||
| px: 5, | |||||
| pb: 10, | |||||
| width: "auto", | |||||
| }; | |||||
| const QrModal: React.FC<Props> = ({ open, onClose, warehouse }) => { | |||||
| const { t } = useTranslation(); | |||||
| const [serverError, setServerError] = useState(""); | |||||
| const formProps = useForm<ModalFormInput>({ | |||||
| defaultValues: { | |||||
| // ...itemDetail, | |||||
| }, | |||||
| }); | |||||
| const errors = formProps.formState.errors; | |||||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | |||||
| (...args) => { | |||||
| onClose?.(...args); | |||||
| setItemDetail(undefined); | |||||
| setStockInLineId(undefined); | |||||
| // reset(); | |||||
| }, | |||||
| [onClose] | |||||
| ); | |||||
| const [stockInLineId, setStockInLineId] = useState<number>(); | |||||
| const scannerConfig = useMemo<ScannerConfig>( | |||||
| () => ({ | |||||
| onUpdate: (err, result) => { | |||||
| if (result) { | |||||
| const data: QrCodeInfo = JSON.parse(result.getText()); | |||||
| console.log(data); | |||||
| if (data.stockInLineId) { | |||||
| console.log("still got in"); | |||||
| console.log(data.stockInLineId); | |||||
| setStockInLineId(data.stockInLineId); | |||||
| } | |||||
| } else return; | |||||
| }, | |||||
| }), | |||||
| [] | |||||
| ); | |||||
| const [itemDetail, setItemDetail] = useState<StockInLine>(); | |||||
| const fetchStockInLine = useCallback( | |||||
| async (stockInLineId: number) => { | |||||
| const res = await fetchStockInLineInfo(stockInLineId); | |||||
| setItemDetail(res); | |||||
| }, | |||||
| [fetchStockInLineInfo] | |||||
| ); | |||||
| useEffect(() => { | |||||
| if (stockInLineId) fetchStockInLine(stockInLineId); | |||||
| }, [stockInLineId]); | |||||
| const onSubmit = useCallback<SubmitHandler<ModalFormInput & {}>>( | |||||
| async (data, event) => { | |||||
| let hasErrors = false; | |||||
| console.log(errors); | |||||
| console.log(data); | |||||
| console.log(itemDetail); | |||||
| try { | |||||
| // add checking | |||||
| // const qty = data.sampleRate | |||||
| //////////////////////// modify this mess later ////////////////////// | |||||
| const args = { | |||||
| // id: itemDetail.id, | |||||
| // purchaseOrderId: parseInt(params.get("id")!!), | |||||
| // purchaseOrderLineId: itemDetail.purchaseOrderLineId, | |||||
| // itemId: itemDetail.itemId, | |||||
| // ...data, | |||||
| // productionDate: productionDate, | |||||
| } as StockInLineEntry & ModalFormInput; | |||||
| ////////////////////////////////////////////////////////////////////// | |||||
| console.log(args); | |||||
| // return | |||||
| if (hasErrors) { | |||||
| setServerError(t("An error has occurred. Please try again later.")); | |||||
| return false; | |||||
| } | |||||
| return; | |||||
| const res = await updateStockInLine(args); | |||||
| if (Boolean(res.id)) { | |||||
| // update entries | |||||
| console.log(res); | |||||
| // add loading | |||||
| // closeHandler({}, "backdropClick"); | |||||
| } | |||||
| console.log(res); | |||||
| // if (res) | |||||
| } catch (e) { | |||||
| // server error | |||||
| setServerError(t("An error has occurred. Please try again later.")); | |||||
| console.log(e); | |||||
| } | |||||
| }, | |||||
| [t, itemDetail] | |||||
| ); | |||||
| return ( | |||||
| <FormProvider {...formProps}> | |||||
| <Modal open={open} onClose={closeHandler}> | |||||
| <Box | |||||
| sx={style} | |||||
| component="form" | |||||
| onSubmit={formProps.handleSubmit(onSubmit)} | |||||
| > | |||||
| <Grid container xs={12}> | |||||
| <Grid item xs={12}> | |||||
| {itemDetail != undefined ? ( | |||||
| <> | |||||
| <PutawayForm itemDetail={itemDetail} warehouse={warehouse} /> | |||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
| <Button | |||||
| name="submit" | |||||
| variant="contained" | |||||
| startIcon={<Check />} | |||||
| type="submit" | |||||
| // disabled={submitDisabled} | |||||
| > | |||||
| {t("submit")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </> | |||||
| ) : ( | |||||
| <ReactQrCodeScanner scannerConfig={scannerConfig} /> | |||||
| )} | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Box> | |||||
| </Modal> | |||||
| </FormProvider> | |||||
| ); | |||||
| }; | |||||
| export default QrModal; | |||||
| @@ -1,173 +1,234 @@ | |||||
| import { Autocomplete, Box, Button, Card, CardContent, Grid, Modal, ModalProps, Stack, SxProps, TextField, Typography } from "@mui/material"; | |||||
| import { CameraDevice, Html5Qrcode, Html5QrcodeCameraScanConfig, Html5QrcodeFullConfig, Html5QrcodeResult, Html5QrcodeScanner, Html5QrcodeScannerState, QrcodeErrorCallback, QrcodeSuccessCallback } from "html5-qrcode"; | |||||
| import { | |||||
| Autocomplete, | |||||
| Box, | |||||
| Button, | |||||
| Card, | |||||
| CardContent, | |||||
| Grid, | |||||
| Modal, | |||||
| ModalProps, | |||||
| Stack, | |||||
| SxProps, | |||||
| TextField, | |||||
| Typography, | |||||
| } from "@mui/material"; | |||||
| import { | |||||
| CameraDevice, | |||||
| Html5Qrcode, | |||||
| Html5QrcodeCameraScanConfig, | |||||
| Html5QrcodeFullConfig, | |||||
| Html5QrcodeResult, | |||||
| Html5QrcodeScanner, | |||||
| Html5QrcodeScannerState, | |||||
| QrcodeErrorCallback, | |||||
| QrcodeSuccessCallback, | |||||
| } from "html5-qrcode"; | |||||
| import { Html5QrcodeError } from "html5-qrcode/esm/core"; | import { Html5QrcodeError } from "html5-qrcode/esm/core"; | ||||
| import { Html5QrcodeScannerConfig } from "html5-qrcode/esm/html5-qrcode-scanner"; | import { Html5QrcodeScannerConfig } from "html5-qrcode/esm/html5-qrcode-scanner"; | ||||
| import React, { RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react"; | |||||
| import React, { | |||||
| RefObject, | |||||
| useCallback, | |||||
| useEffect, | |||||
| useMemo, | |||||
| useRef, | |||||
| useState, | |||||
| } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import QrCodeScannerIcon from '@mui/icons-material/QrCodeScanner'; | |||||
| import StopCircleOutlinedIcon from '@mui/icons-material/StopCircleOutlined'; | |||||
| import CloseOutlinedIcon from '@mui/icons-material/CloseOutlined'; | |||||
| import QrCodeScannerIcon from "@mui/icons-material/QrCodeScanner"; | |||||
| import StopCircleOutlinedIcon from "@mui/icons-material/StopCircleOutlined"; | |||||
| import CloseOutlinedIcon from "@mui/icons-material/CloseOutlined"; | |||||
| import { QrCodeInfo } from "@/app/api/qrcode"; | |||||
| const scannerSx: React.CSSProperties = { | const scannerSx: React.CSSProperties = { | ||||
| position: "absolute", | |||||
| // top: "50%", | |||||
| // left: "50%", | |||||
| // transform: "translate(-50%, -50%)", | |||||
| width: "90%", | |||||
| maxHeight: "10%", | |||||
| maxWidth: 1400, | |||||
| position: "absolute", | |||||
| // top: "50%", | |||||
| // left: "50%", | |||||
| // transform: "translate(-50%, -50%)", | |||||
| width: "90%", | |||||
| maxHeight: "10%", | |||||
| maxWidth: 1400, | |||||
| }; | }; | ||||
| type QrCodeScannerProps = { | type QrCodeScannerProps = { | ||||
| cameras: CameraDevice[] | |||||
| title?: string, | |||||
| contents?: string[], | |||||
| onScanSuccess: (result: string) => void, | |||||
| onScanError?: (error: string) => void, | |||||
| isOpen: boolean, | |||||
| onClose: () => void | |||||
| } | |||||
| cameras: CameraDevice[]; | |||||
| title?: string; | |||||
| contents?: string[]; | |||||
| onScanSuccess: (qrCodeInfo: QrCodeInfo) => void; | |||||
| onScanError?: (error: string) => void; | |||||
| isOpen: boolean; | |||||
| onClose: () => void; | |||||
| }; | |||||
| const QrCodeScanner: React.FC<QrCodeScannerProps> = ({ | const QrCodeScanner: React.FC<QrCodeScannerProps> = ({ | ||||
| title, | |||||
| contents, | |||||
| onScanSuccess, | |||||
| onScanError, | |||||
| isOpen, | |||||
| onClose | |||||
| title, | |||||
| contents, | |||||
| onScanSuccess, | |||||
| onScanError, | |||||
| isOpen, | |||||
| onClose, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation() | |||||
| const [isScanned, setIsScanned] = useState<boolean>(false) | |||||
| const [scanner, setScanner] = useState<Html5Qrcode | null>(null) | |||||
| const [cameraList, setCameraList] = useState<CameraDevice[]>([]) | |||||
| const [selectedCameraId, setSelectedCameraId] = useState<string | null>(null) | |||||
| const stringList = ["ABC: abc", "123:123", "ABC: abc", "123:123", "ABC: abc", "123:123"] | |||||
| const scannerConfig: Html5QrcodeFullConfig = { | |||||
| verbose: false | |||||
| const { t } = useTranslation(); | |||||
| const [isScanned, setIsScanned] = useState<boolean>(false); | |||||
| const [scanner, setScanner] = useState<Html5Qrcode | null>(null); | |||||
| const [cameraList, setCameraList] = useState<CameraDevice[]>([]); | |||||
| const [selectedCameraId, setSelectedCameraId] = useState<string | null>(null); | |||||
| const stringList = [ | |||||
| "ABC: abc", | |||||
| "123:123", | |||||
| "ABC: abc", | |||||
| "123:123", | |||||
| "ABC: abc", | |||||
| "123:123", | |||||
| ]; | |||||
| const scannerConfig: Html5QrcodeFullConfig = { | |||||
| verbose: false, | |||||
| }; | |||||
| const cameraConfig: Html5QrcodeCameraScanConfig = { | |||||
| fps: 10, | |||||
| qrbox: { width: 250, height: 250 }, | |||||
| // aspectRatio: cardRef.current ? (cardRef.current.offsetWidth / cardRef.current.offsetHeight) : 1.78, | |||||
| aspectRatio: (window.innerWidth / window.innerHeight) * 1.5, // can be better | |||||
| }; | |||||
| // MediaTrackConstraintSet | |||||
| const mediaTrackConstraintSet = { | |||||
| facingMode: "environment", | |||||
| }; | |||||
| const handleScanStart = useCallback(() => { | |||||
| if (scanner && selectedCameraId) { | |||||
| if (scanner.getState() === Html5QrcodeScannerState.SCANNING) { | |||||
| console.log("first"); | |||||
| scanner.stop(); | |||||
| } | |||||
| scanner.start( | |||||
| selectedCameraId, | |||||
| cameraConfig, | |||||
| handleScanSuccess, | |||||
| handleScanError | |||||
| ); | |||||
| } | } | ||||
| }, [selectedCameraId, scanner]); | |||||
| const cameraConfig: Html5QrcodeCameraScanConfig = { | |||||
| fps: 10, | |||||
| qrbox: { width: 250, height: 250 }, | |||||
| // aspectRatio: cardRef.current ? (cardRef.current.offsetWidth / cardRef.current.offsetHeight) : 1.78, | |||||
| aspectRatio: (window.innerWidth / window.innerHeight) * 1.5 // can be better | |||||
| }; | |||||
| const handleCameraList = useCallback(async () => { | |||||
| const cameras = await Html5Qrcode.getCameras(); | |||||
| setCameraList(cameras); | |||||
| if (cameras.length > 0) { | |||||
| handleCameraChange(cameras[cameras.length - 1].id); | |||||
| } | |||||
| }, []); | |||||
| const handleCameraChange = useCallback((id: string) => { | |||||
| setSelectedCameraId(id); | |||||
| }, []); | |||||
| // MediaTrackConstraintSet | |||||
| const mediaTrackConstraintSet = { | |||||
| facingMode: "environment" | |||||
| const switchScanStatus = useCallback(() => { | |||||
| if (scanner) { | |||||
| console.log(isScanned); | |||||
| switch (isScanned) { | |||||
| case true: | |||||
| setIsScanned(false); | |||||
| scanner.resume(); | |||||
| break; | |||||
| case false: | |||||
| setIsScanned(true); | |||||
| scanner.pause(true); | |||||
| break; | |||||
| } | |||||
| } | } | ||||
| }, [scanner, isScanned]); | |||||
| const handleScanStart = useCallback(() => { | |||||
| if (scanner && selectedCameraId) { | |||||
| if (scanner.getState() === Html5QrcodeScannerState.SCANNING) { | |||||
| console.log("first") | |||||
| scanner.stop() | |||||
| } | |||||
| const handleScanSuccess = useCallback<QrcodeSuccessCallback>( | |||||
| (decodedText, result) => { | |||||
| if (scanner) { | |||||
| console.log(`Decoded text: ${decodedText}`); | |||||
| const parseData: QrCodeInfo = JSON.parse(decodedText); | |||||
| console.log(parseData); | |||||
| // Handle the decoded text as needed | |||||
| switchScanStatus(); | |||||
| onScanSuccess(parseData); | |||||
| } | |||||
| }, | |||||
| [scanner, onScanSuccess] | |||||
| ); | |||||
| scanner.start( | |||||
| selectedCameraId, | |||||
| cameraConfig, | |||||
| handleScanSuccess, | |||||
| handleScanError | |||||
| ) | |||||
| } | |||||
| }, [selectedCameraId, scanner]) | |||||
| const handleCameraList = useCallback(async () => { | |||||
| const cameras = await Html5Qrcode.getCameras() | |||||
| setCameraList(cameras) | |||||
| if (cameras.length > 0) { | |||||
| handleCameraChange(cameras[cameras.length-1].id) | |||||
| } | |||||
| }, []) | |||||
| const handleCameraChange = useCallback((id: string) => { | |||||
| setSelectedCameraId(id) | |||||
| }, []) | |||||
| const switchScanStatus = useCallback(() => { | |||||
| if (scanner) { | |||||
| console.log(isScanned) | |||||
| switch (isScanned) { | |||||
| case true: | |||||
| setIsScanned(false); | |||||
| scanner.resume(); | |||||
| break; | |||||
| case false: | |||||
| setIsScanned(true); | |||||
| scanner.pause(true); | |||||
| break; | |||||
| } | |||||
| } | |||||
| }, [scanner, isScanned]) | |||||
| const handleScanSuccess = useCallback<QrcodeSuccessCallback>((decodedText, result) => { | |||||
| if (scanner) { | |||||
| console.log(`Decoded text: ${decodedText}`); | |||||
| const parseData = JSON.parse(decodedText) | |||||
| console.log(parseData) | |||||
| // Handle the decoded text as needed | |||||
| switchScanStatus() | |||||
| onScanSuccess(decodedText) | |||||
| } | |||||
| }, [scanner, onScanSuccess]) | |||||
| const handleScanError = useCallback<QrcodeErrorCallback>((errorMessage, error) => { | |||||
| // console.log(`Error: ${errorMessage}`); | |||||
| if (onScanError) { | |||||
| onScanError(errorMessage) | |||||
| } | |||||
| }, [scanner, onScanError]) | |||||
| const handleScanCloseButton = useCallback(async () => { | |||||
| if (scanner) { | |||||
| console.log("Cleaning up scanner..."); | |||||
| await scanner.stop() | |||||
| scanner.clear() | |||||
| onClose() | |||||
| } | |||||
| }, [scanner]) | |||||
| // close modal without using Cancel Button | |||||
| const handleScanClose = useCallback(async () => { | |||||
| if (scanner && !isOpen) { | |||||
| handleScanCloseButton() | |||||
| } | |||||
| }, [scanner, isOpen, handleScanCloseButton]) | |||||
| // -------------------------------------------------------// | |||||
| useEffect(() => { | |||||
| setScanner(new Html5Qrcode( | |||||
| "qr-reader", | |||||
| scannerConfig | |||||
| )) | |||||
| handleCameraList() | |||||
| }, []) | |||||
| useEffect(() => { | |||||
| handleScanStart() | |||||
| }, [scanner, selectedCameraId]); | |||||
| useEffect(() => { | |||||
| handleScanClose() | |||||
| }, [isOpen]) | |||||
| return ( | |||||
| <> | |||||
| <Stack spacing={2}> | |||||
| {title && <Typography variant="overline" display="block" marginBlockEnd={1} paddingLeft={2}> | |||||
| {"Title"} | |||||
| </Typography>} | |||||
| <Grid container alignItems="center" justifyContent="center" rowSpacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
| <Grid item xs={12}> | |||||
| <div style={{ textAlign: "center", margin: "auto", justifyContent: "center" }} id="qr-reader" hidden={isScanned} /> | |||||
| </Grid> | |||||
| {/* {cameraList.length > 0 && <Grid item xs={6} > | |||||
| const handleScanError = useCallback<QrcodeErrorCallback>( | |||||
| (errorMessage, error) => { | |||||
| // console.log(`Error: ${errorMessage}`); | |||||
| if (onScanError) { | |||||
| onScanError(errorMessage); | |||||
| } | |||||
| }, | |||||
| [scanner, onScanError] | |||||
| ); | |||||
| const handleScanCloseButton = useCallback(async () => { | |||||
| if (scanner) { | |||||
| console.log("Cleaning up scanner..."); | |||||
| await scanner.stop(); | |||||
| scanner.clear(); | |||||
| onClose(); | |||||
| } | |||||
| }, [scanner]); | |||||
| // close modal without using Cancel Button | |||||
| const handleScanClose = useCallback(async () => { | |||||
| if (scanner && !isOpen) { | |||||
| handleScanCloseButton(); | |||||
| } | |||||
| }, [scanner, isOpen, handleScanCloseButton]); | |||||
| // -------------------------------------------------------// | |||||
| useEffect(() => { | |||||
| setScanner(new Html5Qrcode("qr-reader", scannerConfig)); | |||||
| handleCameraList(); | |||||
| }, []); | |||||
| useEffect(() => { | |||||
| handleScanStart(); | |||||
| }, [scanner, selectedCameraId]); | |||||
| useEffect(() => { | |||||
| handleScanClose(); | |||||
| }, [isOpen]); | |||||
| return ( | |||||
| <> | |||||
| <Stack spacing={2}> | |||||
| {title && ( | |||||
| <Typography | |||||
| variant="overline" | |||||
| display="block" | |||||
| marginBlockEnd={1} | |||||
| paddingLeft={2} | |||||
| > | |||||
| {"Title"} | |||||
| </Typography> | |||||
| )} | |||||
| <Grid | |||||
| container | |||||
| alignItems="center" | |||||
| justifyContent="center" | |||||
| rowSpacing={2} | |||||
| columns={{ xs: 6, sm: 12 }} | |||||
| > | |||||
| <Grid item xs={12}> | |||||
| <div | |||||
| style={{ | |||||
| textAlign: "center", | |||||
| margin: "auto", | |||||
| justifyContent: "center", | |||||
| }} | |||||
| id="qr-reader" | |||||
| hidden={isScanned} | |||||
| /> | |||||
| </Grid> | |||||
| {/* {cameraList.length > 0 && <Grid item xs={6} > | |||||
| <Autocomplete | <Autocomplete | ||||
| disableClearable | disableClearable | ||||
| noOptionsText={t("No Options")} | noOptionsText={t("No Options")} | ||||
| @@ -185,36 +246,46 @@ const QrCodeScanner: React.FC<QrCodeScannerProps> = ({ | |||||
| )} | )} | ||||
| /> | /> | ||||
| </Grid>} */} | </Grid>} */} | ||||
| { | |||||
| contents && contents.map((string) => { | |||||
| return <Grid item xs={8}>{string}</Grid> | |||||
| }) | |||||
| } | |||||
| {contents && | |||||
| contents.map((string) => { | |||||
| return ( | |||||
| <Grid item xs={8}> | |||||
| {string} | |||||
| </Grid> | </Grid> | ||||
| <Stack direction="row" justifyContent={"flex-end"} spacing={2} sx={{ margin: 2 }}> | |||||
| <Button | |||||
| size="small" | |||||
| onClick={switchScanStatus} | |||||
| variant="contained" | |||||
| // sx={{ margin: 2 }} | |||||
| startIcon={isScanned ? <QrCodeScannerIcon /> : <StopCircleOutlinedIcon />} | |||||
| > | |||||
| {isScanned ? t("Start Scanning") : t("Stop Scanning")} | |||||
| </Button> | |||||
| <Button | |||||
| size="small" | |||||
| onClick={handleScanCloseButton} | |||||
| variant="outlined" | |||||
| // color="error" | |||||
| // sx={{ margin: 2 }} | |||||
| startIcon={<CloseOutlinedIcon />} | |||||
| > | |||||
| {t("Cancel")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </Stack> | |||||
| </> | |||||
| ) | |||||
| } | |||||
| export default QrCodeScanner | |||||
| ); | |||||
| })} | |||||
| </Grid> | |||||
| <Stack | |||||
| direction="row" | |||||
| justifyContent={"flex-end"} | |||||
| spacing={2} | |||||
| sx={{ margin: 2 }} | |||||
| > | |||||
| <Button | |||||
| size="small" | |||||
| onClick={switchScanStatus} | |||||
| variant="contained" | |||||
| // sx={{ margin: 2 }} | |||||
| startIcon={ | |||||
| isScanned ? <QrCodeScannerIcon /> : <StopCircleOutlinedIcon /> | |||||
| } | |||||
| > | |||||
| {isScanned ? t("Start Scanning") : t("Stop Scanning")} | |||||
| </Button> | |||||
| <Button | |||||
| size="small" | |||||
| onClick={handleScanCloseButton} | |||||
| variant="outlined" | |||||
| // color="error" | |||||
| // sx={{ margin: 2 }} | |||||
| startIcon={<CloseOutlinedIcon />} | |||||
| > | |||||
| {t("Cancel")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </Stack> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default QrCodeScanner; | |||||
| @@ -11,6 +11,7 @@ import { | |||||
| import QrCodeScanner from "./QrCodeScanner"; | import QrCodeScanner from "./QrCodeScanner"; | ||||
| import { useCallback, useEffect, useRef, useState } from "react"; | import { useCallback, useEffect, useRef, useState } from "react"; | ||||
| import { CameraDevice } from "html5-qrcode"; | import { CameraDevice } from "html5-qrcode"; | ||||
| import { QrCodeInfo } from "@/app/api/qrcode"; | |||||
| const modalSx: SxProps = { | const modalSx: SxProps = { | ||||
| position: "absolute", | position: "absolute", | ||||
| @@ -28,7 +29,7 @@ type QrCodeScannerModalProps = { | |||||
| contents?: string[]; | contents?: string[]; | ||||
| isOpen: boolean; | isOpen: boolean; | ||||
| onClose: () => void; | onClose: () => void; | ||||
| onScanSuccess: (result: string) => void; | |||||
| onScanSuccess: (qrCodeInfo: QrCodeInfo) => void; | |||||
| onScanError?: (error: string) => void; | onScanError?: (error: string) => void; | ||||
| }; | }; | ||||
| @@ -0,0 +1,91 @@ | |||||
| "use client"; | |||||
| import { QrCodeInfo } from "@/app/api/qrcode"; | |||||
| import { Box, Button, Modal, ModalProps } from "@mui/material"; | |||||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||||
| import BarcodeScanner, { BarcodeStringFormat } from "react-qr-barcode-scanner"; | |||||
| import { BarcodeFormat, Result } from "@zxing/library"; | |||||
| interface Props { | |||||
| scannerConfig: ScannerConfig; | |||||
| } | |||||
| // interface Props extends Omit<ModalProps, "children"> { | |||||
| // scannerConfig: ScannerConfig; | |||||
| // } | |||||
| const style = { | |||||
| position: "absolute", | |||||
| top: "50%", | |||||
| left: "50%", | |||||
| transform: "translate(-50%, -50%)", | |||||
| bgcolor: "background.paper", | |||||
| pt: 5, | |||||
| px: 5, | |||||
| pb: 10, | |||||
| width: { xs: "80%", sm: "80%", md: "80%" }, | |||||
| }; | |||||
| export var defaultScannerConfig: ScannerConfig = { | |||||
| onUpdate: (err, result) => { | |||||
| if (result) { | |||||
| const data = JSON.parse(result.getText()) | |||||
| console.log(data); | |||||
| } else return; | |||||
| }, | |||||
| width: 500, | |||||
| height: 500, | |||||
| facingMode: "environment", | |||||
| // torch: false | |||||
| }; | |||||
| export interface ScannerConfig { | |||||
| onUpdate: (arg0: unknown, arg1?: Result) => void; | |||||
| onError?: (arg0: string | DOMException) => void; | |||||
| width?: number | string; | |||||
| height?: number | string; | |||||
| facingMode?: "environment" | "user"; // default environment | |||||
| 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 | |||||
| } | |||||
| 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 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); | |||||
| }, []); | |||||
| const SwitchOnOffTorch = useCallback(() => { | |||||
| setTorchEnabled((prev) => !prev); | |||||
| }, []); | |||||
| return ( | |||||
| <> | |||||
| {!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; | |||||