瀏覽代碼

update

create_edit_user
MSI\derek 2 月之前
父節點
當前提交
bf8c015b78
共有 15 個檔案被更改,包括 1004 行新增447 行删除
  1. +13
    -13
      src/app/api/po/actions.ts
  2. +1
    -0
      src/app/api/po/index.ts
  3. +11
    -0
      src/app/api/qrcode/index.ts
  4. +13
    -5
      src/components/InputDataGrid/InputDataGrid.tsx
  5. +90
    -28
      src/components/PoDetail/PoDetail.tsx
  6. +4
    -13
      src/components/PoDetail/PoDetailWrapper.tsx
  7. +71
    -71
      src/components/PoDetail/PoInputGrid.tsx
  8. +133
    -81
      src/components/PoDetail/PoQcStockInModal.tsx
  9. +101
    -26
      src/components/PoDetail/PutawayForm.tsx
  10. +9
    -24
      src/components/PoDetail/QcForm.tsx
  11. +42
    -0
      src/components/PoDetail/QrCoderScanner.tsx
  12. +167
    -0
      src/components/PoDetail/QrModal.tsx
  13. +256
    -185
      src/components/QrCodeScanner/QrCodeScanner.tsx
  14. +2
    -1
      src/components/QrCodeScanner/QrCodeScannerModal.tsx
  15. +91
    -0
      src/components/ReactQrCodeScanner/ReactQrCodeScanner.tsx

+ 13
- 13
src/app/api/po/actions.ts 查看文件

@@ -12,6 +12,7 @@ export interface PostStockInLiineResponse<T> {
id: number | null;
name: string;
code: string;
type?: string
message: string | null;
errorPosition: string | keyof T;
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) => {
const stockInLine = await serverFetchJson<PostStockInLiineResponse<StockInLineEntry>>(`${BASE_API_URL}/stockInLine/create`, {
method: "POST",
@@ -104,4 +95,13 @@ export const updateStockInLine = async (data: StockInLineEntry & ModalFormInput)
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
}



+ 1
- 0
src/app/api/po/index.ts 查看文件

@@ -48,6 +48,7 @@ export interface StockInLine {
lotNo: string
poCode: string
uom: Uom
defaultWarehouseId: number // id for now
}

export const fetchPoList = cache(async () => {


+ 11
- 0
src/app/api/qrcode/index.ts 查看文件

@@ -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
}

+ 13
- 5
src/components/InputDataGrid/InputDataGrid.tsx 查看文件

@@ -26,14 +26,14 @@ import {
GridValidRowModel,
useGridApiRef,
} 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 DeleteIcon from "@mui/icons-material/Delete";
import CancelIcon from "@mui/icons-material/Cancel";
import { Add } from "@mui/icons-material";
import { Box, Button, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
import { GridApiCommunity } from "@mui/x-data-grid/internals";
import { GridApiCommunity, GridSlotsComponentsProps } from "@mui/x-data-grid/internals";

interface ResultWithId {
id: string | number;
@@ -67,6 +67,7 @@ export interface InputDataGridProps<T, V, E> {
_formKey: keyof T;
columns: GridColDef[];
validateRow: (newRow: GridRowModel<TableRow<V, E>>) => E;
needAdd?: Boolean
};

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;
columns: GridColDef[];
validateRow: (newRow: GridRowModel<TableRow<V, E>>) => E;
needAdd?: Boolean
}

export type Props<T, V, E> = InputDataGridProps<T, V, E> | SelectionInputDataGridProps<T, V, E>
@@ -94,10 +96,11 @@ export class ProcessRowUpdateError<T, E> extends Error {
// E == error
function InputDataGrid<T, V, E>({
apiRef,
checkboxSelection,
checkboxSelection = false,
_formKey,
columns,
validateRow,
needAdd,
}: Props<T, V, E>) {
const {
t,
@@ -113,6 +116,7 @@ function InputDataGrid<T, V, E>({
[]
);
const list: TableRow<V, E>[] = getValues(formKey);
// console.log(list)
const [rows, setRows] = useState<TableRow<V, E>[]>(() => {
const list: TableRow<V, E>[] = getValues(formKey);
return list && list.length > 0 ? list : [];
@@ -348,9 +352,13 @@ function InputDataGrid<T, V, E>({
footer: FooterToolbar,
noRowsOverlay: NoRowsOverlay,
} : undefined}
slotProps={!checkboxSelection ? {
slotProps={!checkboxSelection && Boolean(needAdd) ? {
footer: { child: footer },
} : undefined}
}: undefined
// slotProps={renderFooter ? {
// footer: { child: footer },
// }: undefined
}
/>
)
}


+ 90
- 28
src/components/PoDetail/PoDetail.tsx 查看文件

@@ -28,9 +28,26 @@ import {
} from "@mui/material";
import { useTranslation } from "react-i18next";
// 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 KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
@@ -46,6 +63,13 @@ import QrCodeScanner from "../QrCodeScanner";
import { CameraDevice, Html5Qrcode } from "html5-qrcode";
import { CameraContext } from "../Cameras/CameraProvider";
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 = {
po: PoResult;
@@ -72,7 +96,6 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {
const [open, setOpen] = useState(false);
const [processedQty, setProcessedQty] = useState(row.processed);
const [currStatus, setCurrStatus] = useState(row.status);

useEffect(() => {
if (processedQty === row.qty) {
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 onOpenScanner = useCallback(() => {
@@ -155,17 +185,33 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {
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 (
<>
@@ -180,30 +226,34 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {
{po.code}
</Typography>
</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 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
value={tabIndex}
onChange={handleTabChange}
variant="scrollable"
>
<Tab label={t("General")} iconPosition="end" />
<Tab label={t("Bind Storage")} iconPosition="end" />
{/* <Tab label={t("Bind Storage")} iconPosition="end" /> */}
</Tabs>
</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" }}>
<TableContainer component={Paper}>
<Table aria-label="collapsible table">
@@ -238,6 +288,18 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {
/> */}
</Grid>
</Stack>
{itemInfo !== undefined && (
<>
<PoQcStockInModal
type={"putaway"}
open={putAwayOpen}
warehouse={warehouse}
setItemDetail={setItemInfo}
onClose={closePutAwayModal}
itemDetail={itemInfo}
/>
</>
)}
</>
);
};


+ 4
- 13
src/components/PoDetail/PoDetailWrapper.tsx 查看文件

@@ -20,23 +20,14 @@ type Props = {
};

const PoDetailWrapper: React.FC<Props> & SubComponents = async ({ id }) => {
const [
poWithStockInLine,
warehouse,
qc,
] = await Promise.all([
const [poWithStockInLine, warehouse, qc] = await Promise.all([
fetchPoWithStockInLines(id),
fetchWarehouseList(),
fetchQcItemCheck()
])
fetchQcItemCheck(),
]);
// 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;


+ 71
- 71
src/components/PoDetail/PoInputGrid.tsx 查看文件

@@ -33,17 +33,14 @@ import ShoppingCartIcon from "@mui/icons-material/ShoppingCart";
import { QcItemWithChecks } from "src/app/api/qc";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
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 {
returnWeightUnit,
calculateWeight,
stockInLineStatusMap,
} from "@/app/utils/formatUtil";
import PoQcStockInModal from "./PoQcStockInModal";
// import PoQcStockInModal from "./PoQcStockInModal";
import NotificationImportantIcon from "@mui/icons-material/NotificationImportant";
import { WarehouseResult } from "@/app/api/warehouse";
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 { fetchPoQrcode } from "@/app/api/pdf/actions";
import { fetchQcResult } from "@/app/api/qc/actions";
import PoQcStockInModal from "./PoQcStockInModal";

interface ResultWithId {
id: number;
}
@@ -116,7 +115,9 @@ function PoInputGrid({
);
console.log(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 [escalOpen, setEscalOpen] = useState(false);
const [stockInOpen, setStockInOpen] = useState(false);
@@ -176,19 +177,9 @@ function PoInputGrid({
},
[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(
(id: GridRowId, params: any) => async () => {
@@ -196,11 +187,12 @@ function PoInputGrid({
...prev,
[id]: { mode: GridRowModes.View },
}));
const qcResult = await fetchQcDefaultValue();
console.log(qcResult)
const qcResult = await fetchQcDefaultValue(id);
console.log(params.row);
console.log(qcResult);
setModalInfo({
...params.row,
qcResult: qcResult
qcResult: qcResult,
});
// set default values
setTimeout(() => {
@@ -414,7 +406,7 @@ function PoInputGrid({
}}
disabled={
stockInLineStatusMap[status] <= 0 ||
stockInLineStatusMap[status] >= 5
stockInLineStatusMap[status] >= 4
}
// set _isNew to false after posting
// or check status
@@ -429,7 +421,7 @@ function PoInputGrid({
color: "primary.main",
// marginRight: 1,
}}
disabled={stockInLineStatusMap[status] !== 6}
disabled={stockInLineStatusMap[status] <= 2 || stockInLineStatusMap[status] >= 7}
// set _isNew to false after posting
// or check status
onClick={handleStockIn(params.row.id, params)}
@@ -563,13 +555,6 @@ function PoInputGrid({
[apiRef]
);

// useEffect(() => {
// const total = entries.reduce(
// (acc, curr) => acc + (curr.acceptedQty || 0),
// 0
// );
// setDefaultQty(itemDetail.qty - total);
// }, [entries]);
const footer = (
<Box display="flex" gap={2} alignItems="center">
<Button
@@ -584,6 +569,9 @@ function PoInputGrid({
</Button>
</Box>
);
useEffect(() => {
console.log(modalInfo);
}, [modalInfo]);
return (
<>
<StyledDataGrid
@@ -631,46 +619,58 @@ function PoInputGrid({
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}
/>
</>
)}
</>
);
}


+ 133
- 81
src/components/PoDetail/PoQcStockInModal.tsx 查看文件

@@ -1,13 +1,27 @@
"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 { 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 { useTranslation } from "react-i18next";
import QcForm from "./QcForm";
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 { useSearchParams } from "next/navigation";
import { StockInLineRow } from "./PoInputGrid";
@@ -15,35 +29,38 @@ import EscalationForm from "./EscalationForm";
import StockInForm from "./StockInForm";
import PutawayForm from "./PutawayForm";
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"> {
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[];
warehouse?: any[];
type: "qc" | "stockIn" | "escalation" | "putaway"
type: "qc" | "stockIn" | "escalation" | "putaway";
}
interface QcProps extends CommonProps {
qc: QcItemWithChecks[];
type: "qc"
}
type: "qc";
}
interface StockInProps extends CommonProps {
// naming
type: "stockIn"
type: "stockIn";
}
interface PutawayProps extends CommonProps {
// naming
// warehouse: any[];
warehouse: any[];
type: "putaway"
type: "putaway";
}
interface EscalationProps extends CommonProps {
// naming
type: "escalation"
type: "escalation";
}

type Props = QcProps | StockInProps | EscalationProps | PutawayProps;

type Props = QcProps | StockInProps | PutawayProps | EscalationProps;
const style = {
position: "absolute",
top: "50%",
@@ -55,35 +72,29 @@ const style = {
pb: 10,
width: { xs: "80%", sm: "80%", md: "80%" },
};

const PoQcStockInModal: React.FC<Props> = ({
type,
setEntries,
open,
onClose,
itemDetail,
onClose,
itemDetail,
setItemDetail,
qc,
warehouse,
}) => {
console.log(itemDetail)
}) => {
const [serverError, setServerError] = useState("");
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>({
defaultValues: defaultValue
defaultValues: {
...itemDetail,
},
});
// console.log(formProps);
const errors = formProps.formState.errors;
const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>(
(...args) => {
@@ -94,27 +105,52 @@ const PoQcStockInModal: React.FC<Props> = ({
);

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 & {}>>(
async (data, event) => {
let hasErrors = false;
console.log(errors);
console.log(data);
console.log(itemDetail);
console.log(data.receiptDate)
console.log(fix0IndexedDate(data.receiptDate))
try {
// add checking
// const qty = data.sampleRate
//////////////////////// 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) {
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) {
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 = {
id: itemDetail.id,
@@ -123,36 +159,43 @@ const PoQcStockInModal: React.FC<Props> = ({
itemId: itemDetail.itemId,
...data,
productionDate: productionDate,
expiryDate: expiryDate,
receiptDate: receiptDate,
} as StockInLineEntry & ModalFormInput;
//////////////////////////////////////////////////////////////////////
console.log(args)
console.log(args);
// return
if (hasErrors) {
setServerError(t("An error has occurred. Please try again later."));
return false;
}
const res = await updateStockInLine(args)
const res = await updateStockInLine(args);
if (Boolean(res.id)) {
// 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
closeHandler({}, "backdropClick")
setItemDetail(undefined)
closeHandler({}, "backdropClick");
}
console.log(res)
console.log(res);
// if (res)
} catch (e) {
// server error
@@ -162,39 +205,49 @@ const PoQcStockInModal: React.FC<Props> = ({
},
[t, itemDetail]
);
const renderSubmitButton = useMemo((): Boolean => {
if (itemDetail) {
const status = itemDetail.status
console.log(status)
const status = itemDetail.status;
console.log(status);
switch (type) {
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":
return stockInLineStatusMap[status] === 6
return stockInLineStatusMap[status] >= 3 && stockInLineStatusMap[status] <= 6;
case "putaway":
return stockInLineStatusMap[status] === 7;
default:
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 (
<>
<Modal open={open} onClose={closeHandler}>
<FormProvider {...formProps}>
<FormProvider {...formProps}>
<Modal open={open} onClose={closeHandler}>
<Box
sx={style}
component="form"
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 ? (
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
@@ -207,11 +260,10 @@ const PoQcStockInModal: React.FC<Props> = ({
{t("submit")}
</Button>
</Stack>
) : undefined
}
) : undefined}
</Box>
</FormProvider>
</Modal>
</Modal>
</FormProvider>
</>
);
};


+ 101
- 26
src/components/PoDetail/PutawayForm.tsx 查看文件

@@ -4,10 +4,13 @@ import { PurchaseQcResult, PutawayInput } from "@/app/api/po/actions";
import {
Autocomplete,
Box,
Button,
Card,
CardContent,
FormControl,
Grid,
Modal,
ModalProps,
Stack,
TextField,
Tooltip,
@@ -35,8 +38,10 @@ import { GridEditInputCell } from "@mui/x-data-grid";
import { StockInLine } from "@/app/api/po";
import { WarehouseResult } from "@/app/api/warehouse";
import { stockInLineStatusMap } from "@/app/utils/formatUtil";
import { QRCodeSVG } from 'qrcode.react';
import { QRCodeSVG } from "qrcode.react";
import { QrCode } from "../QrCode";
import ReactQrCodeScanner, { ScannerConfig } from "../ReactQrCodeScanner/ReactQrCodeScanner";
import { QrCodeInfo } from "@/app/api/qrcode";

interface Props {
itemDetail: StockInLine;
@@ -51,6 +56,18 @@ type 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 { t } = useTranslation();
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(() => {
setValue("status", "completed")
}, [])
setValue("status", "completed");
}, []);

return (
<Grid container justifyContent="flex-start" alignItems="flex-start">
<Grid item xs={12}>
@@ -190,7 +245,7 @@ const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse }) => {
disabled
/>
</Grid>
<Grid item xs={12}>
<Grid item xs={6}>
<TextField
label={t("expiryDate")}
fullWidth
@@ -199,6 +254,21 @@ const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse }) => {
/>
</Grid>
<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
label={t("acceptedQty")}
fullWidth
@@ -213,7 +283,10 @@ const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse }) => {
helperText={errors.acceptedQty?.message}
/>
</Grid>
<Grid item xs={6}>
<Grid item xs={1}>
<Button onClick={onOpenScanner}>bind</Button>
</Grid>
<Grid item xs={5.5}>
<FormControl fullWidth>
<Autocomplete
noOptionsText={t("No Warehouse")}
@@ -227,27 +300,29 @@ const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse }) => {
/>
</FormControl>
</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
container
justifyContent="flex-start"
alignItems="flex-start"
spacing={2}
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>
);
};


+ 9
- 24
src/components/PoDetail/QcForm.tsx 查看文件

@@ -31,14 +31,14 @@ import QcSelect from "./QcSelect";
import { GridEditInputCell } from "@mui/x-data-grid";
import { StockInLine } from "@/app/api/po";
import { stockInLineStatusMap } from "@/app/utils/formatUtil";
import { fetchQcItemCheck } from "@/app/api/qc/actions";
import { fetchQcItemCheck, fetchQcResult } from "@/app/api/qc/actions";
import { QcItemWithChecks } from "@/app/api/qc";
import axios from "@/app/(main)/axios/axiosInstance";
import { NEXT_PUBLIC_API_URL } from "@/config/api";
import axiosInstance from "@/app/(main)/axios/axiosInstance";

interface Props {
itemDetail: StockInLine;
itemDetail: StockInLine
qc: QcItemWithChecks[];
}
type EntryError =
@@ -65,24 +65,8 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail }) => {
clearErrors,
} = useFormContext<PurchaseQCInput>();
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 columns = useMemo<GridColDef[]>(
() => [
@@ -203,7 +187,7 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail }) => {
spacing={2}
sx={{ mt: 0.5 }}
>
<Grid item xs={6}>
<Grid item xs={12} lg={6}>
<TextField
label={t("accepted Qty")}
fullWidth
@@ -217,7 +201,7 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail }) => {
helperText={errors.acceptedQty?.message}
/>
</Grid>
<Grid item xs={6}>
<Grid item xs={12} lg={6}>
<TextField
label={t("Total record qty")}
fullWidth
@@ -230,7 +214,7 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail }) => {
// helperText={errors.sampleRate?.message}
/>
</Grid>
<Grid item xs={4}>
<Grid item xs={12} lg={6}>
<TextField
label={t("sampleRate")}
fullWidth
@@ -242,7 +226,7 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail }) => {
helperText={errors.sampleRate?.message}
/>
</Grid>
<Grid item xs={4}>
<Grid item xs={12} lg={6}>
<TextField
label={t("sampleWeight")}
fullWidth
@@ -253,7 +237,7 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail }) => {
helperText={errors.sampleWeight?.message}
/>
</Grid>
<Grid item xs={4}>
<Grid item xs={12} lg={6}>
<TextField
label={t("totalWeight")}
fullWidth
@@ -279,6 +263,7 @@ const QcForm: React.FC<Props> = ({ qc, itemDetail }) => {
_formKey={"qcResult"}
columns={columns}
validateRow={validation}
needAdd={itemDetail.status === "qc" || itemDetail.status === "pending"}
/>
</Grid>
</Grid>


+ 42
- 0
src/components/PoDetail/QrCoderScanner.tsx 查看文件

@@ -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

+ 167
- 0
src/components/PoDetail/QrModal.tsx 查看文件

@@ -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;

+ 256
- 185
src/components/QrCodeScanner/QrCodeScanner.tsx 查看文件

@@ -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 { 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 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 = {
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 = {
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> = ({
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
disableClearable
noOptionsText={t("No Options")}
@@ -185,36 +246,46 @@ const QrCodeScanner: React.FC<QrCodeScannerProps> = ({
)}
/>
</Grid>} */}
{
contents && contents.map((string) => {
return <Grid item xs={8}>{string}</Grid>
})
}
{contents &&
contents.map((string) => {
return (
<Grid item xs={8}>
{string}
</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;

+ 2
- 1
src/components/QrCodeScanner/QrCodeScannerModal.tsx 查看文件

@@ -11,6 +11,7 @@ import {
import QrCodeScanner from "./QrCodeScanner";
import { useCallback, useEffect, useRef, useState } from "react";
import { CameraDevice } from "html5-qrcode";
import { QrCodeInfo } from "@/app/api/qrcode";

const modalSx: SxProps = {
position: "absolute",
@@ -28,7 +29,7 @@ type QrCodeScannerModalProps = {
contents?: string[];
isOpen: boolean;
onClose: () => void;
onScanSuccess: (result: string) => void;
onScanSuccess: (qrCodeInfo: QrCodeInfo) => void;
onScanError?: (error: string) => void;
};



+ 91
- 0
src/components/ReactQrCodeScanner/ReactQrCodeScanner.tsx 查看文件

@@ -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;

Loading…
取消
儲存