diff --git a/src/app/(main)/settings/qcItem/page.tsx b/src/app/(main)/settings/qcItem/page.tsx new file mode 100644 index 0000000..00a041e --- /dev/null +++ b/src/app/(main)/settings/qcItem/page.tsx @@ -0,0 +1,44 @@ +import { Metadata } from "next"; +import { getServerI18n, I18nProvider } from "@/i18n"; +import Typography from "@mui/material/Typography"; +import { Button, Link, Stack } from "@mui/material"; +import { Add } from "@mui/icons-material"; +import { Suspense } from "react"; +import { preloadQcItem } from "@/app/api/settings/qcItem"; +import QcItemSearch from "@/components/QcItemSearch"; + +export const metadata: Metadata = { + title: "Qc Item", +}; + +const qcItem: React.FC = async () => { + const { t } = await getServerI18n("qcItem") + + preloadQcItem() + + return <> + + + {t("Qc Item")} + + + + }> + + + ; +}; + +export default qcItem; \ No newline at end of file diff --git a/src/app/api/settings/qcItem/actions.ts b/src/app/api/settings/qcItem/actions.ts new file mode 100644 index 0000000..bb42451 --- /dev/null +++ b/src/app/api/settings/qcItem/actions.ts @@ -0,0 +1,32 @@ +"use server" + +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { revalidateTag } from "next/cache"; + +export interface SaveQcItemInputs { + id?: number; + code: string; + name: string; + description?: string; +} + +export interface SaveQcItemResponse { + id?: number; + code: string; + name: string; + description?: string; + errors: Record +} + +export const saveQcItem = async (data: SaveQcItemInputs) => { + const response = await serverFetchJson(`${BASE_API_URL}/qcItems/save`, { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }) + + revalidateTag(`qcItems`) + + return response +} \ No newline at end of file diff --git a/src/app/api/settings/qcItem/index.ts b/src/app/api/settings/qcItem/index.ts new file mode 100644 index 0000000..e0b06f9 --- /dev/null +++ b/src/app/api/settings/qcItem/index.ts @@ -0,0 +1,32 @@ +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { cache } from "react"; +import "server-only"; +import { SaveQcItemInputs } from "./actions"; +import next from "next"; + +export interface QcItemResult { + id: number; + code: string; + name: string; + description: string; +} + +export const preloadQcItem = () => { + fetchQcItems(); +}; + +export const fetchQcItems = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/qcItems`, { + next: { tags: ["qcItems"] } + }); +}); + +export const fetchQcItemDetail = cache(async (qcItemId: string) => { + return serverFetchJson( + `${BASE_API_URL}/qcItems/qcItemDetail/${qcItemId}`, + { + next: { tags: [`qcItemDetail_${qcItemId}`] } + } + ) +}) \ No newline at end of file diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 8b4d131..fa93e47 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -12,6 +12,8 @@ const pathToLabelMap: { [path: string]: string } = { "/projects/create": "Create Project", "/tasks": "Task Template", "/tasks/create": "Create Task Template", + "/settings/qcItem": "Qc Item", + }; const Breadcrumb = () => { diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 46c042c..b46469f 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -224,12 +224,12 @@ const NavigationContent: React.FC = () => { { icon: , label: "QC Check Item", - path: "/settings/user", + path: "/settings/qcItem", }, { icon: , label: "QC Category", - path: "/settings/user", + path: "/settings/qcCategory", }, { icon: , diff --git a/src/components/QcItemSave/QcItemDetails.tsx b/src/components/QcItemSave/QcItemDetails.tsx new file mode 100644 index 0000000..4d2b703 --- /dev/null +++ b/src/components/QcItemSave/QcItemDetails.tsx @@ -0,0 +1,57 @@ +import { SaveQcItemInputs } from "@/app/api/settings/qcItem/actions" +import { Box, Card, CardContent, Grid, Stack, TextField, Typography } from "@mui/material" +import { useFormContext } from "react-hook-form" +import { useTranslation } from "react-i18next" + +const QcItemDetails = () => { + + const { t } = useTranslation() + const { + register + } = useFormContext() + + return ( + + + + + {t("Qc Item Details")} + + + + + + + + + + + + + + + + ) +} + +export default QcItemDetails \ No newline at end of file diff --git a/src/components/QcItemSave/QcItemSave.tsx b/src/components/QcItemSave/QcItemSave.tsx new file mode 100644 index 0000000..3875ebe --- /dev/null +++ b/src/components/QcItemSave/QcItemSave.tsx @@ -0,0 +1,70 @@ +"use client" + +import { saveQcItem, SaveQcItemInputs } from "@/app/api/settings/qcItem/actions"; +import { Stack } from "@mui/material"; +import { useCallback } from "react"; +import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; +import { errorDialogWithContent, submitDialog } from "../Swal/CustomAlerts"; +import { useTranslation } from "react-i18next"; +import { useRouter } from "next/navigation"; + +interface Props { + defaultInputs?: SaveQcItemInputs; +} + +const QcItemSave: React.FC = ({ + defaultInputs, +}) => { + + const { t } = useTranslation("qcItem") + const router = useRouter() + + const formProps = useForm({ + defaultValues: { + ...defaultInputs + } + }) + + const handleSubmit = useCallback(async (data: SaveQcItemInputs) => { + const response = await saveQcItem(data) + + const errors = response.errors + if (errors) { + let errorContents = "" + for (const [key, value] of Object.entries(errors)) { + formProps.setError(key as keyof SaveQcItemInputs, { type: "custom", message: value }) + errorContents = errorContents + t(value) + "
" + } + + errorDialogWithContent(t("Submit Error"), errorContents, t) + } else { + router.push("/settings/qcItem") + } + }, + [] + ) + + const onSubmit = useCallback>( + async (data) => { + + await submitDialog(() => handleSubmit(data), t) + }, [] + ) + + return ( + <> + + + + + + + ) +}; + + +export default QcItemSave; \ No newline at end of file diff --git a/src/components/QcItemSave/QcItemSaveLoading.tsx b/src/components/QcItemSave/QcItemSaveLoading.tsx new file mode 100644 index 0000000..01c9782 --- /dev/null +++ b/src/components/QcItemSave/QcItemSaveLoading.tsx @@ -0,0 +1,40 @@ +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const QcItemSaveLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default QcItemSaveLoading; diff --git a/src/components/QcItemSave/QcItemSaveWrapper.tsx b/src/components/QcItemSave/QcItemSaveWrapper.tsx new file mode 100644 index 0000000..b0f3104 --- /dev/null +++ b/src/components/QcItemSave/QcItemSaveWrapper.tsx @@ -0,0 +1,26 @@ +import React from "react" +import QcItemSaveLoading from "./QcItemSaveLoading" +import QcItemSave from "./QcItemSave"; +import { fetchQcItemDetail } from "@/app/api/settings/qcItem"; + +interface SubComponents { + Loading: typeof QcItemSaveLoading; +} + +type SaveQcItemProps = { + id?: string +} + +type Props = SaveQcItemProps + +const QcItemSaveWrapper: React.FC & SubComponents = async ( + props +) => { + const qcItem = props.id ? await fetchQcItemDetail(props.id) : undefined; + + return ; +}; + +QcItemSaveWrapper.Loading = QcItemSaveLoading; + +export default QcItemSaveWrapper; \ No newline at end of file diff --git a/src/components/QcItemSave/index.ts b/src/components/QcItemSave/index.ts new file mode 100644 index 0000000..7c6812d --- /dev/null +++ b/src/components/QcItemSave/index.ts @@ -0,0 +1 @@ +export { default } from "./QcItemSaveWrapper"; \ No newline at end of file diff --git a/src/components/QcItemSearch/QcItemSearch.tsx b/src/components/QcItemSearch/QcItemSearch.tsx new file mode 100644 index 0000000..80af2e1 --- /dev/null +++ b/src/components/QcItemSearch/QcItemSearch.tsx @@ -0,0 +1,97 @@ +"use client"; +import { Html5QrcodeResult, Html5QrcodeScanner, QrcodeErrorCallback, QrcodeSuccessCallback } from "html5-qrcode"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import SearchBox, { Criterion } from "../SearchBox"; +import { useTranslation } from "react-i18next"; +import SearchResults, { Column } from "../SearchResults"; +import EditNote from "@mui/icons-material/EditNote"; +import { QcItemResult } from "@/app/api/settings/qcItem"; +import { useRouter } from "next/navigation"; +import QrCodeScanner from "../QrCodeScanner"; +import { Button } from "@mui/material"; + +interface Props { + qcItems: QcItemResult[]; +} + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const qcItemSearch: React.FC = ({ qcItems }) => { + const { t } = useTranslation("qcItems"); + const router = useRouter(); + + // If qcItem searching is done on the server-side, then no need for this. + const [filteredQcItems, setFilteredQcItems] = useState(qcItems); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { label: t("Code"), paramName: "code", type: "text" }, + { label: t("Name"), paramName: "name", type: "text" }, + ], + [t] + ); + + const onReset = useCallback( + () => { + setFilteredQcItems(qcItems); + }, [qcItems] + ); + + const onQcItemClick = useCallback( + (qcItem: QcItemResult) => { + router.push(`/edit/${qcItem.id}`); + }, + [router] + ); + + const columns = useMemo[]>( + () => [ + { + name: "id", + label: t("Details"), + onClick: onQcItemClick, + buttonIcon: , + }, + { name: "code", label: t("Code") }, + { name: "name", label: t("Name") }, + ], + [t, onQcItemClick] + ); + + const [isOpenScanner, setOpenScanner] = useState(false) + const onOpenScanner = useCallback(() => { + setOpenScanner(true) + }, []) + + const onCloseScanner = useCallback(() => { + setOpenScanner(false) + }, []) + + const handleScanSuccess = useCallback((result: string) => { + console.log(result) + }, []) + + return ( + <> + + + {/* { + setFilteredQcItems( + qcItems.filter( + (qi) => + qi.code.toLowerCase().includes(query.code.toLowerCase()) && + qi.name.toLowerCase().includes(query.name.toLowerCase()) + ) + ); + }} + onReset={onReset} + /> + items={filteredQcItems} columns={columns} /> */} + + ) +}; + +export default qcItemSearch; diff --git a/src/components/QcItemSearch/QcItemSearchLoading.tsx b/src/components/QcItemSearch/QcItemSearchLoading.tsx new file mode 100644 index 0000000..944e059 --- /dev/null +++ b/src/components/QcItemSearch/QcItemSearchLoading.tsx @@ -0,0 +1,40 @@ +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const QcCategorySearchLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default QcCategorySearchLoading; diff --git a/src/components/QcItemSearch/QcItemSearchWrapper.tsx b/src/components/QcItemSearch/QcItemSearchWrapper.tsx new file mode 100644 index 0000000..1242174 --- /dev/null +++ b/src/components/QcItemSearch/QcItemSearchWrapper.tsx @@ -0,0 +1,23 @@ +import React from "react" +import QcItemSearchLoading from "./QcItemSearchLoading" +import QcItemSearch from "./QcItemSearch"; +import { fetchQcItems } from "@/app/api/settings/qcItem"; + +interface SubComponents { + Loading: typeof QcItemSearchLoading; +} + +const QcItemSearchWrapper: React.FC & SubComponents = async () => { + + const [ + qcItems + ] = await Promise.all([ + fetchQcItems() + ]); + + return ; +}; + +QcItemSearchWrapper.Loading = QcItemSearchLoading; + +export default QcItemSearchWrapper; \ No newline at end of file diff --git a/src/components/QcItemSearch/index.ts b/src/components/QcItemSearch/index.ts new file mode 100644 index 0000000..e4237cd --- /dev/null +++ b/src/components/QcItemSearch/index.ts @@ -0,0 +1 @@ +export { default } from "./QcItemSearchWrapper"; \ No newline at end of file diff --git a/src/components/QrCodeScanner/QrCodeScanner.tsx b/src/components/QrCodeScanner/QrCodeScanner.tsx index 2675eeb..b95ee1c 100644 --- a/src/components/QrCodeScanner/QrCodeScanner.tsx +++ b/src/components/QrCodeScanner/QrCodeScanner.tsx @@ -1,8 +1,8 @@ -import { Button, Card, CardContent, Modal, ModalProps, SxProps } from "@mui/material"; -import { Html5QrcodeResult, Html5QrcodeScanner, QrcodeErrorCallback, QrcodeSuccessCallback } from "html5-qrcode"; +import { Button, Card, CardContent, Grid, Modal, ModalProps, Stack, SxProps, Typography } from "@mui/material"; +import { Html5Qrcode, Html5QrcodeCameraScanConfig, Html5QrcodeFullConfig, Html5QrcodeResult, Html5QrcodeScanner, QrcodeErrorCallback, QrcodeSuccessCallback } from "html5-qrcode"; import { Html5QrcodeError } from "html5-qrcode/esm/core"; import { Html5QrcodeScannerConfig } from "html5-qrcode/esm/html5-qrcode-scanner"; -import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import React, { RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react"; const scannerSx: React.CSSProperties = { position: "absolute", @@ -16,82 +16,115 @@ const scannerSx: React.CSSProperties = { type QrCodeScannerProps = { onScanSuccess: (result: string) => void, - onScanError?: (error: string) => void -} - -const scannerConfig: Html5QrcodeScannerConfig = { - fps: 10, - qrbox: { width: 400, height: 400 }, - aspectRatio: 2.5, + onScanError?: (error: string) => void, + isOpen: boolean } const QrCodeScanner: React.FC = ({ onScanSuccess, - onScanError + onScanError, + isOpen }) => { const [isScanned, setIsScanned] = useState(false) - const [scanner, setScanner] = useState(null) + const [scanner, setScanner] = useState(null) + const scannerConfig: Html5QrcodeFullConfig = { + verbose: false + } + + const cameraConfig: Html5QrcodeCameraScanConfig = { + fps: 10, + qrbox: { width: 400, height: 400 }, + // aspectRatio: cardRef.current ? (cardRef.current.offsetWidth / cardRef.current.offsetHeight) : 1.78, + aspectRatio: 2.5 + }; + + // MediaTrackConstraintSet + const mediaTrackConstraintSet = { + facingMode: "environment" + } useEffect(() => { - setScanner(new Html5QrcodeScanner( + setScanner(new Html5Qrcode( "qr-reader", - scannerConfig, - false + scannerConfig )) }, []) const handleStartScan = useCallback(() => { - setIsScanned(false) - scanner?.resume(); + if (scanner) { + setIsScanned(false) + scanner.resume(); + } + }, [scanner]) + + const handleScanSuccess = useCallback((decodedText, result) => { + if (scanner) { + console.log(`Decoded text: ${decodedText}`); + // Handle the decoded text as needed + setIsScanned(true) + scanner.pause(); + onScanSuccess(decodedText) + } + }, [scanner, onScanSuccess]) + + const handleScanError = useCallback((errorMessage, error) => { + console.log(`Error: ${errorMessage}`); + + if (onScanError) { + onScanError(errorMessage) + } + }, [scanner, onScanError]) + + const handleScanClose = useCallback(async () => { + if (scanner) { + console.log("Cleaning up scanner..."); + await scanner.stop() + await scanner.clear() + } }, [scanner]) useEffect(() => { if (scanner) { console.log("Scanner Instance:", scanner); - const success: QrcodeSuccessCallback = (decodedText, result) => { - console.log(`Decoded text: ${decodedText}`); - // Handle the decoded text as needed - setIsScanned(true) - scanner.pause(); - onScanSuccess(decodedText) - }; - const error: QrcodeErrorCallback = (errorMessage, error) => { - console.log(`Error: ${errorMessage}`); + scanner.start( + mediaTrackConstraintSet, + cameraConfig, + handleScanSuccess, + handleScanError + ) - if (onScanError) { - onScanError(errorMessage) - } - }; - - try { - scanner.render(success, error); - console.log("Scanner render called"); - } catch (err) { - console.error("Failed to render scanner:", err); - } - return () => { - console.log("Cleaning up scanner..."); - scanner.clear().catch((error) => { - console.error("Failed to clear html5QrcodeScanner. ", error); - }); + handleScanClose() }; } }, [scanner]); - return ( <> -