Browse Source

update qc master & qr scanner

feature/axios_provider
cyril.tsui 5 months ago
parent
commit
19e3013a53
10 changed files with 141 additions and 47 deletions
  1. +15
    -2
      src/app/api/settings/qcItem/actions.ts
  2. +3
    -3
      src/app/api/settings/qcItem/index.ts
  3. +6
    -1
      src/app/utils/fetchUtil.ts
  4. +2
    -1
      src/components/QcItemSave/QcItemDetails.tsx
  5. +51
    -5
      src/components/QcItemSave/QcItemSave.tsx
  6. +2
    -2
      src/components/QcItemSave/QcItemSaveWrapper.tsx
  7. +41
    -17
      src/components/QcItemSearch/QcItemSearch.tsx
  8. +10
    -10
      src/components/QrCodeScanner/QrCodeScanner.tsx
  9. +3
    -2
      src/components/SearchResults/SearchResults.tsx
  10. +8
    -4
      src/components/Swal/CustomAlerts.tsx

+ 15
- 2
src/app/api/settings/qcItem/actions.ts View File

@@ -2,7 +2,8 @@


import { serverFetchJson } from "@/app/utils/fetchUtil"; import { serverFetchJson } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api"; import { BASE_API_URL } from "@/config/api";
import { revalidateTag } from "next/cache";
import { revalidatePath, revalidateTag } from "next/cache";
import { QcItemResult } from '@/app/api/settings/qcItem';


export interface SaveQcItemInputs { export interface SaveQcItemInputs {
id?: number; id?: number;
@@ -26,7 +27,19 @@ export const saveQcItem = async (data: SaveQcItemInputs) => {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
}) })


revalidateTag(`qcItems`)
revalidateTag("qcItems")

return response
}

export const deleteQcItem = async (id: number) => {
const response = await serverFetchJson<QcItemResult[]>(`${BASE_API_URL}/qcItems/${id}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
})

revalidateTag("qcItems")
revalidatePath("/(main)/settings/qcItem")


return response return response
} }

+ 3
- 3
src/app/api/settings/qcItem/index.ts View File

@@ -22,11 +22,11 @@ export const fetchQcItems = cache(async () => {
}); });
}); });


export const fetchQcItemDetail = cache(async (qcItemId: string) => {
export const fetchQcItemDetails = cache(async (qcItemId: string) => {
return serverFetchJson<SaveQcItemInputs>( return serverFetchJson<SaveQcItemInputs>(
`${BASE_API_URL}/qcItems/qcItemDetail/${qcItemId}`,
`${BASE_API_URL}/qcItems/qcItemDetails/${qcItemId}`,
{ {
next: { tags: [`qcItemDetail_${qcItemId}`] }
next: { tags: [`qcItemDetails_${qcItemId}`] }
} }
) )
}) })

+ 6
- 1
src/app/utils/fetchUtil.ts View File

@@ -45,6 +45,7 @@ export const serverFetch: typeof fetch = async (input, init) => {
...(accessToken ...(accessToken
? { ? {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
} }
: {}), : {}),
}, },
@@ -56,13 +57,17 @@ type FetchParams = Parameters<typeof fetch>;
export async function serverFetchJson<T>(...args: FetchParams) { export async function serverFetchJson<T>(...args: FetchParams) {
const response = await serverFetch(...args); const response = await serverFetch(...args);
if (response.ok) { if (response.ok) {
if (response.status === 204) {
return response.status as T
}
return response.json() as T; return response.json() as T;
} else { } else {
switch (response.status) { switch (response.status) {
case 401: case 401:
signOutUser(); signOutUser();
default: default:
throw Error("Something went wrong fetching data in server.");
throw new ServerFetchError("Something went wrong fetching data in server.", response);
} }
} }
} }


+ 2
- 1
src/components/QcItemSave/QcItemDetails.tsx View File

@@ -38,9 +38,10 @@ const QcItemDetails = () => {
})} })}
/> />
</Grid> </Grid>
<Grid item xs={6}>
<Grid item xs={12}>
<TextField <TextField
label={t("Description")} label={t("Description")}
// multiline
fullWidth fullWidth
{...register("description", { {...register("description", {
maxLength: 100 maxLength: 100


+ 51
- 5
src/components/QcItemSave/QcItemSave.tsx View File

@@ -1,12 +1,14 @@
"use client" "use client"


import { saveQcItem, SaveQcItemInputs } from "@/app/api/settings/qcItem/actions";
import { Stack } from "@mui/material";
import { deleteQcItem, saveQcItem, SaveQcItemInputs } from "@/app/api/settings/qcItem/actions";
import { Button, Stack } from "@mui/material";
import { useCallback } from "react"; import { useCallback } from "react";
import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import { errorDialogWithContent, submitDialog } from "../Swal/CustomAlerts";
import { deleteDialog, errorDialogWithContent, submitDialog, successDialog } from "../Swal/CustomAlerts";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import QcItemDetails from "./QcItemDetails";
import { Check, Close, Delete } from "@mui/icons-material";


interface Props { interface Props {
defaultInputs?: SaveQcItemInputs; defaultInputs?: SaveQcItemInputs;
@@ -38,7 +40,10 @@ const QcItemSave: React.FC<Props> = ({


errorDialogWithContent(t("Submit Error"), errorContents, t) errorDialogWithContent(t("Submit Error"), errorContents, t)
} else { } else {
router.push("/settings/qcItem")
await successDialog(
t("Submit Success"),
t,
() => router.push("/settings/qcItem"))
} }
}, },
[] []
@@ -51,6 +56,22 @@ const QcItemSave: React.FC<Props> = ({
}, [] }, []
) )


const handleCancel = () => {
router.replace("/qcItem");
};

const handleDelete = () => {
deleteDialog(async () => {
await deleteQcItem(formProps.getValues("id")!);

await successDialog(
t("Delete Success"),
t,
() => router.replace("/settings/qcItem")
);
}, t);
};

return ( return (
<> <>
<FormProvider {...formProps}> <FormProvider {...formProps}>
@@ -59,7 +80,32 @@ const QcItemSave: React.FC<Props> = ({
component={"form"} component={"form"}
onSubmit={formProps.handleSubmit(onSubmit)} onSubmit={formProps.handleSubmit(onSubmit)}
> >

<QcItemDetails />
<Stack direction="row" justifyContent="flex-end" gap={1}>
{defaultInputs?.id && <Button
variant="outlined"
color="error"
startIcon={<Delete />}
onClick={handleDelete}
>
{t("Delete")}
</Button>}
<Button
variant="outlined"
startIcon={<Close />}
onClick={handleCancel}
>
{t("Cancel")}
</Button>
<Button
name="submit"
variant="contained"
startIcon={<Check />}
type="submit"
>
{t("Submit")}
</Button>
</Stack>
</Stack> </Stack>
</FormProvider> </FormProvider>
</> </>


+ 2
- 2
src/components/QcItemSave/QcItemSaveWrapper.tsx View File

@@ -1,7 +1,7 @@
import React from "react" import React from "react"
import QcItemSaveLoading from "./QcItemSaveLoading" import QcItemSaveLoading from "./QcItemSaveLoading"
import QcItemSave from "./QcItemSave"; import QcItemSave from "./QcItemSave";
import { fetchQcItemDetail } from "@/app/api/settings/qcItem";
import { fetchQcItemDetails } from "@/app/api/settings/qcItem";


interface SubComponents { interface SubComponents {
Loading: typeof QcItemSaveLoading; Loading: typeof QcItemSaveLoading;
@@ -16,7 +16,7 @@ type Props = SaveQcItemProps
const QcItemSaveWrapper: React.FC<Props> & SubComponents = async ( const QcItemSaveWrapper: React.FC<Props> & SubComponents = async (
props props
) => { ) => {
const qcItem = props.id ? await fetchQcItemDetail(props.id) : undefined;
const qcItem = props.id ? await fetchQcItemDetails(props.id) : undefined;


return <QcItemSave defaultInputs={qcItem} />; return <QcItemSave defaultInputs={qcItem} />;
}; };


+ 41
- 17
src/components/QcItemSearch/QcItemSearch.tsx View File

@@ -6,9 +6,12 @@ import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults"; import SearchResults, { Column } from "../SearchResults";
import EditNote from "@mui/icons-material/EditNote"; import EditNote from "@mui/icons-material/EditNote";
import { QcItemResult } from "@/app/api/settings/qcItem"; import { QcItemResult } from "@/app/api/settings/qcItem";
import { useRouter } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import QrCodeScanner from "../QrCodeScanner"; import QrCodeScanner from "../QrCodeScanner";
import { Button } from "@mui/material"; import { Button } from "@mui/material";
import { Delete } from "@mui/icons-material";
import { deleteDialog, successDialog } from "../Swal/CustomAlerts";
import { deleteQcItem } from "@/app/api/settings/qcItem/actions";


interface Props { interface Props {
qcItems: QcItemResult[]; qcItems: QcItemResult[];
@@ -20,6 +23,7 @@ type SearchParamNames = keyof SearchQuery;
const qcItemSearch: React.FC<Props> = ({ qcItems }) => { const qcItemSearch: React.FC<Props> = ({ qcItems }) => {
const { t } = useTranslation("qcItems"); const { t } = useTranslation("qcItems");
const router = useRouter(); const router = useRouter();
const pathname = usePathname()


// If qcItem searching is done on the server-side, then no need for this. // If qcItem searching is done on the server-side, then no need for this.
const [filteredQcItems, setFilteredQcItems] = useState(qcItems); const [filteredQcItems, setFilteredQcItems] = useState(qcItems);
@@ -40,11 +44,24 @@ const qcItemSearch: React.FC<Props> = ({ qcItems }) => {


const onQcItemClick = useCallback( const onQcItemClick = useCallback(
(qcItem: QcItemResult) => { (qcItem: QcItemResult) => {
router.push(`/edit/${qcItem.id}`);
router.push(`${pathname}/edit?id=${qcItem.id}`);
}, },
[router] [router]
); );


const handleDelete = useCallback(
(qcItem: QcItemResult) => {
deleteDialog(async () => {
qcItems = await deleteQcItem(qcItem.id);
setFilteredQcItems(qcItems)

await successDialog(
t("Delete Success"),
t
);
}, t);
}, [])

const columns = useMemo<Column<QcItemResult>[]>( const columns = useMemo<Column<QcItemResult>[]>(
() => [ () => [
{ {
@@ -55,28 +72,35 @@ const qcItemSearch: React.FC<Props> = ({ qcItems }) => {
}, },
{ name: "code", label: t("Code") }, { name: "code", label: t("Code") },
{ name: "name", label: t("Name") }, { name: "name", label: t("Name") },
{
name: "id",
label: t("Delete"),
onClick: handleDelete,
buttonIcon: <Delete />,
buttonColor: "error"
}
], ],
[t, onQcItemClick] [t, onQcItemClick]
); );


const [isOpenScanner, setOpenScanner] = useState(false)
const onOpenScanner = useCallback(() => {
setOpenScanner(true)
}, [])
// const [isOpenScanner, setOpenScanner] = useState(false)
// const onOpenScanner = useCallback(() => {
// setOpenScanner(true)
// }, [])


const onCloseScanner = useCallback(() => {
setOpenScanner(false)
}, [])
// const onCloseScanner = useCallback(() => {
// setOpenScanner(false)
// }, [])


const handleScanSuccess = useCallback((result: string) => {
console.log(result)
}, [])
// const handleScanSuccess = useCallback((result: string) => {
// console.log(result)
// }, [])


return ( return (
<> <>
<QrCodeScanner isOpen={isOpenScanner} onClose={onCloseScanner} onScanSuccess={handleScanSuccess} />
<Button onClick={onOpenScanner}>abc</Button>
{/* <SearchBox
{/* <QrCodeScanner isOpen={isOpenScanner} onClose={onCloseScanner} onScanSuccess={handleScanSuccess} />
<Button onClick={onOpenScanner}>abc</Button> */}
<SearchBox
criteria={searchCriteria} criteria={searchCriteria}
onSearch={(query) => { onSearch={(query) => {
setFilteredQcItems( setFilteredQcItems(
@@ -89,9 +113,9 @@ const qcItemSearch: React.FC<Props> = ({ qcItems }) => {
}} }}
onReset={onReset} onReset={onReset}
/> />
<SearchResults<QcItemResult> items={filteredQcItems} columns={columns} /> */}
<SearchResults<QcItemResult> items={filteredQcItems} columns={columns} />
</> </>
)
)
}; };


export default qcItemSearch; export default qcItemSearch;

+ 10
- 10
src/components/QrCodeScanner/QrCodeScanner.tsx View File

@@ -159,14 +159,14 @@ const QrCodeScanner: React.FC<QrCodeScannerProps> = ({
return ( return (
<> <>
<Stack spacing={2}> <Stack spacing={2}>
{title ? <Typography variant="overline" display="block" marginBlockEnd={1} paddingLeft={2}>
{title && <Typography variant="overline" display="block" marginBlockEnd={1} paddingLeft={2}>
{"Title"} {"Title"}
</Typography> : null}
</Typography>}
<Grid container alignItems="center" justifyContent="center" rowSpacing={2} columns={{ xs: 6, sm: 12 }}> <Grid container alignItems="center" justifyContent="center" rowSpacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={12}> <Grid item xs={12}>
<div style={{ textAlign: "center", margin: "auto", justifyContent: "center" }} id="qr-reader" hidden={isScanned} /> <div style={{ textAlign: "center", margin: "auto", justifyContent: "center" }} id="qr-reader" hidden={isScanned} />
</Grid> </Grid>
{cameraList.length > 0 ? <Grid item xs={6} >
{cameraList.length > 0 && <Grid item xs={6} >
<Autocomplete <Autocomplete
disableClearable disableClearable
noOptionsText={t("No Options")} noOptionsText={t("No Options")}
@@ -183,11 +183,11 @@ const QrCodeScanner: React.FC<QrCodeScannerProps> = ({
/> />
)} )}
/> />
</Grid> : null}
</Grid>}
{ {
contents ? contents.map((string) => {
contents && contents.map((string) => {
return <Grid item xs={8}>{string}</Grid> return <Grid item xs={8}>{string}</Grid>
}) : null
})
} }
</Grid> </Grid>
<Stack direction="row" justifyContent={"flex-end"} spacing={2} sx={{ margin: 2 }}> <Stack direction="row" justifyContent={"flex-end"} spacing={2} sx={{ margin: 2 }}>
@@ -195,7 +195,7 @@ const QrCodeScanner: React.FC<QrCodeScannerProps> = ({
size="small" size="small"
onClick={switchScanStatus} onClick={switchScanStatus}
variant="contained" variant="contained"
sx={{ margin: 2 }}
// sx={{ margin: 2 }}
startIcon={isScanned ? <QrCodeScannerIcon /> : <StopCircleOutlinedIcon />} startIcon={isScanned ? <QrCodeScannerIcon /> : <StopCircleOutlinedIcon />}
> >
{isScanned ? t("Start Scanning") : t("Stop Scanning")} {isScanned ? t("Start Scanning") : t("Stop Scanning")}
@@ -203,9 +203,9 @@ const QrCodeScanner: React.FC<QrCodeScannerProps> = ({
<Button <Button
size="small" size="small"
onClick={handleScanCloseButton} onClick={handleScanCloseButton}
variant="contained"
color="error"
sx={{ margin: 2 }}
variant="outlined"
// color="error"
// sx={{ margin: 2 }}
startIcon={<CloseOutlinedIcon />} startIcon={<CloseOutlinedIcon />}
> >
{t("Cancel")} {t("Cancel")}


+ 3
- 2
src/components/SearchResults/SearchResults.tsx View File

@@ -11,7 +11,7 @@ import TablePagination, {
TablePaginationProps, TablePaginationProps,
} from "@mui/material/TablePagination"; } from "@mui/material/TablePagination";
import TableRow from "@mui/material/TableRow"; import TableRow from "@mui/material/TableRow";
import IconButton from "@mui/material/IconButton";
import IconButton, { IconButtonOwnProps } from "@mui/material/IconButton";


export interface ResultWithId { export interface ResultWithId {
id: string | number; id: string | number;
@@ -25,6 +25,7 @@ interface BaseColumn<T extends ResultWithId> {
interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> { interface ColumnWithAction<T extends ResultWithId> extends BaseColumn<T> {
onClick: (item: T) => void; onClick: (item: T) => void;
buttonIcon: React.ReactNode; buttonIcon: React.ReactNode;
buttonColor?: IconButtonOwnProps["color"];
} }


export type Column<T extends ResultWithId> = export type Column<T extends ResultWithId> =
@@ -91,7 +92,7 @@ function SearchResults<T extends ResultWithId>({
<TableCell key={`${columnName.toString()}-${idx}`}> <TableCell key={`${columnName.toString()}-${idx}`}>
{isActionColumn(column) ? ( {isActionColumn(column) ? (
<IconButton <IconButton
color="primary"
color={column.buttonColor ?? "primary"}
onClick={() => column.onClick(item)} onClick={() => column.onClick(item)}
> >
{column.buttonIcon} {column.buttonIcon}


+ 8
- 4
src/components/Swal/CustomAlerts.tsx View File

@@ -2,7 +2,7 @@ import Swal, { SweetAlertOptions } from "sweetalert2";
import "./sweetalert2.css"; import "./sweetalert2.css";
import { TFunction } from "i18next"; import { TFunction } from "i18next";


export type SweetAlertTitle = string | HTMLElement | JQuery | undefined
export type SweetAlertTitle = string | HTMLElement | JQuery | undefined
export type SweetAlertHtml = string | HTMLElement | JQuery | undefined export type SweetAlertHtml = string | HTMLElement | JQuery | undefined
export type SweetAlertConfirmButtonText = string | undefined export type SweetAlertConfirmButtonText = string | undefined


@@ -29,13 +29,17 @@ export const popup = (options: SweetAlertOptions) => {
Swal.fire(options); Swal.fire(options);
}; };


export const successDialog = (title: SweetAlertTitle, t: Transaction) => {
return Swal.fire({
export const successDialog = async (title: SweetAlertTitle, t: Transaction, confirmAction?: () => void) => {
const result = await Swal.fire({
icon: "success", icon: "success",
title: title, title: title,
confirmButtonText: t("Confirm"), confirmButtonText: t("Confirm"),
showConfirmButton: true, showConfirmButton: true,
}); });

if (result.isConfirmed && confirmAction) {
confirmAction();
}
}; };


export const successDialogWithContent = (title: SweetAlertTitle, html: SweetAlertHtml, t: Transaction) => { export const successDialogWithContent = (title: SweetAlertTitle, html: SweetAlertHtml, t: Transaction) => {
@@ -81,7 +85,7 @@ export const submitDialog = async (
t: Transaction, t: Transaction,
{ ...props } = { { ...props } = {
title: t("Do you want to submit?") as SweetAlertTitle, title: t("Do you want to submit?") as SweetAlertTitle,
confirmButtonText: t("Submit") as SweetAlertConfirmButtonText,
confirmButtonText: t("Submit") as SweetAlertConfirmButtonText,
} }
) => { ) => {
// console.log(props) // console.log(props)


Loading…
Cancel
Save