| @@ -1,19 +1,22 @@ | |||
| import { SearchParams } from "@/app/utils/fetchUtil"; | |||
| import CreateMaterial from "@/components/CreateMaterial"; | |||
| import { TypeEnum } from "@/app/utils/typeEnum"; | |||
| import CreateProductMaterial from "@/components/CreateProductMaterial"; | |||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||
| import { Typography } from "@mui/material"; | |||
| import { isString } from "lodash"; | |||
| type Props = {} & SearchParams; | |||
| const materialSetting: React.FC<Props> = async ({ searchParams }) => { | |||
| const { t } = await getServerI18n("materials"); | |||
| const type = TypeEnum.MATERIAL; | |||
| const { t } = await getServerI18n(type); | |||
| console.log(searchParams); | |||
| return ( | |||
| <> | |||
| {/* <Typography variant="h4">{t("Create Material")}</Typography> */} | |||
| <I18nProvider namespaces={["materials"]}> | |||
| <CreateMaterial isEditMode={false} /> | |||
| <I18nProvider namespaces={[type]}> | |||
| <CreateProductMaterial type={type} /> | |||
| </I18nProvider> | |||
| </> | |||
| ); | |||
| @@ -0,0 +1,31 @@ | |||
| import { SearchParams } from "@/app/utils/fetchUtil"; | |||
| import { TypeEnum } from "@/app/utils/typeEnum"; | |||
| import CreateProductMaterial from "@/components/CreateProductMaterial"; | |||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||
| import { Typography } from "@mui/material"; | |||
| import { isString } from "lodash"; | |||
| import { notFound } from "next/navigation"; | |||
| type Props = {} & SearchParams; | |||
| const materialSetting: React.FC<Props> = async ({ searchParams }) => { | |||
| const type = TypeEnum.MATERIAL; | |||
| const { t } = await getServerI18n(type); | |||
| console.log(searchParams); | |||
| const id = isString(searchParams["id"]) | |||
| ? parseInt(searchParams["id"]) | |||
| : undefined; | |||
| console.log(id) | |||
| if (!id) { | |||
| notFound(); | |||
| } | |||
| return ( | |||
| <> | |||
| {/* <Typography variant="h4">{t("Create Material")}</Typography> */} | |||
| <I18nProvider namespaces={[type]}> | |||
| <CreateProductMaterial type={type} id={id} /> | |||
| </I18nProvider> | |||
| </> | |||
| ); | |||
| }; | |||
| export default materialSetting; | |||
| @@ -1,4 +1,5 @@ | |||
| import MaterialSearch from "@/components/MaterialSearch"; | |||
| import { TypeEnum } from "@/app/utils/typeEnum"; | |||
| import MaterialSearch from "@/components/ProductMaterialSearch"; | |||
| import { getServerI18n } from "@/i18n"; | |||
| import Add from "@mui/icons-material/Add"; | |||
| import Button from "@mui/material/Button"; | |||
| @@ -13,8 +14,9 @@ export const metadata: Metadata = { | |||
| }; | |||
| const materialSetting: React.FC = async () => { | |||
| const { t } = await getServerI18n("material"); | |||
| // preloadClaims(); | |||
| const material = TypeEnum.MATERIAL | |||
| const { t } = await getServerI18n(material); | |||
| // preloadClaims(); | |||
| return ( | |||
| <> | |||
| @@ -37,7 +39,7 @@ const materialSetting: React.FC = async () => { | |||
| </Button> | |||
| </Stack> | |||
| <Suspense fallback={<MaterialSearch.Loading />}> | |||
| <MaterialSearch /> | |||
| <MaterialSearch type={material} /> | |||
| </Suspense> | |||
| </> | |||
| ); | |||
| @@ -0,0 +1,22 @@ | |||
| import { SearchParams } from "@/app/utils/fetchUtil"; | |||
| import { TypeEnum } from "@/app/utils/typeEnum"; | |||
| import CreateProductMaterial from "@/components/CreateProductMaterial"; | |||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||
| import { Typography } from "@mui/material"; | |||
| import isString from "lodash/isString"; | |||
| type Props = {} & SearchParams; | |||
| const materialSetting: React.FC<Props> = async ({ searchParams }) => { | |||
| const type = TypeEnum.PRODUCT; | |||
| const { t } = await getServerI18n(type); | |||
| return ( | |||
| <> | |||
| {/* <Typography variant="h4">{t("Create Material")}</Typography> */} | |||
| <I18nProvider namespaces={[type]}> | |||
| <CreateProductMaterial type={type}/> | |||
| </I18nProvider> | |||
| </> | |||
| ); | |||
| }; | |||
| export default materialSetting; | |||
| @@ -0,0 +1,29 @@ | |||
| import { SearchParams } from "@/app/utils/fetchUtil"; | |||
| import { TypeEnum } from "@/app/utils/typeEnum"; | |||
| import CreateProductMaterial from "@/components/CreateProductMaterial"; | |||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||
| import { Typography } from "@mui/material"; | |||
| import isString from "lodash/isString"; | |||
| import { notFound } from "next/navigation"; | |||
| type Props = {} & SearchParams; | |||
| const productSetting: React.FC<Props> = async ({ searchParams }) => { | |||
| const type = TypeEnum.PRODUCT; | |||
| const { t } = await getServerI18n(type); | |||
| const id = isString(searchParams["id"]) | |||
| ? parseInt(searchParams["id"]) | |||
| : undefined; | |||
| if (!id) { | |||
| notFound(); | |||
| } | |||
| return ( | |||
| <> | |||
| {/* <Typography variant="h4">{t("Create Material")}</Typography> */} | |||
| <I18nProvider namespaces={[type]}> | |||
| <CreateProductMaterial type={type} id={id} /> | |||
| </I18nProvider> | |||
| </> | |||
| ); | |||
| }; | |||
| export default productSetting; | |||
| @@ -0,0 +1,48 @@ | |||
| import { TypeEnum } from "@/app/utils/typeEnum"; | |||
| import MaterialSearch from "@/components/ProductMaterialSearch"; | |||
| import { getServerI18n } from "@/i18n"; | |||
| import Add from "@mui/icons-material/Add"; | |||
| import Button from "@mui/material/Button"; | |||
| import Stack from "@mui/material/Stack"; | |||
| import Typography from "@mui/material/Typography"; | |||
| import { Metadata } from "next"; | |||
| import Link from "next/link"; | |||
| import { Suspense } from "react"; | |||
| export const metadata: Metadata = { | |||
| title: "Product", | |||
| }; | |||
| const productSetting: React.FC = async () => { | |||
| const project = TypeEnum.PRODUCT | |||
| const { t } = await getServerI18n(project); | |||
| // preloadClaims(); | |||
| return ( | |||
| <> | |||
| <Stack | |||
| direction="row" | |||
| justifyContent="space-between" | |||
| flexWrap="wrap" | |||
| rowGap={2} | |||
| > | |||
| <Typography variant="h4" marginInlineEnd={2}> | |||
| {t("Product")} | |||
| </Typography> | |||
| <Button | |||
| variant="contained" | |||
| startIcon={<Add />} | |||
| LinkComponent={Link} | |||
| href="product/create" | |||
| > | |||
| {t("Create product")} | |||
| </Button> | |||
| </Stack> | |||
| <Suspense fallback={<MaterialSearch.Loading />}> | |||
| <MaterialSearch type={project} /> | |||
| </Suspense> | |||
| </> | |||
| ); | |||
| }; | |||
| export default productSetting; | |||
| @@ -2,14 +2,26 @@ | |||
| import { ServerFetchError, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | |||
| import { revalidateTag } from "next/cache"; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| import { HTMLInputTypeAttribute } from "react"; | |||
| export type TypeInputs = { | |||
| type: string | |||
| } | |||
| export type UomInputs = { | |||
| uom: string | |||
| } | |||
| export type WeightUnitInputs = { | |||
| weightUnit: string | |||
| conversion: number | |||
| } | |||
| export type CreateMaterialInputs = { | |||
| id?: string | number | |||
| code: string; | |||
| name: string; | |||
| isConsumables: boolean; | |||
| // same goes for other props | |||
| // change backend for null or not null | |||
| description?: string | undefined; | |||
| type?: string | undefined; | |||
| remarks?: string | undefined; | |||
| shelfLife?: Number | undefined; | |||
| countryOfOrigin?: string | undefined; | |||
| @@ -20,17 +32,25 @@ export type CreateMaterialInputs = { | |||
| sampleRate?: number | undefined; | |||
| passingRate?: number | undefined; | |||
| netWeight?: number | undefined; | |||
| uom?: any; | |||
| weightUnit?: any; | |||
| type?: TypeInputs[]; | |||
| uom?: UomInputs[]; | |||
| weightUnit?: WeightUnitInputs[]; | |||
| } | |||
| export interface CreateProductMaterialResponse { | |||
| id: number | null; | |||
| name: string; | |||
| code: string; | |||
| message: string | null; | |||
| errorPosition: keyof CreateMaterialInputs; | |||
| } | |||
| export const saveMaterial = async (data: CreateMaterialInputs) => { | |||
| // try { | |||
| const materials = await serverFetchJson(`${BASE_API_URL}/materials/save`, { | |||
| const material = await serverFetchJson<CreateProductMaterialResponse>(`${BASE_API_URL}/material/new`, { | |||
| method: "POST", | |||
| body: JSON.stringify(data), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }); | |||
| revalidateTag("materials"); | |||
| return materials | |||
| revalidateTag("material"); | |||
| return material | |||
| }; | |||
| @@ -2,18 +2,40 @@ import { cache } from "react"; | |||
| import "server-only"; | |||
| import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| import { TypeInputs, UomInputs, WeightUnitInputs } from "./actions"; | |||
| export type MaterialResult = { | |||
| id: number; | |||
| id: string | number | |||
| code: string; | |||
| name: string; | |||
| description: string; | |||
| action: any | undefined; | |||
| isConsumables: boolean; | |||
| description: string | undefined; | |||
| remarks: string | undefined; | |||
| shelfLife: Number | undefined; | |||
| countryOfOrigin: string | undefined; | |||
| minHumid: number | undefined; | |||
| maxHumid: number | undefined; | |||
| minTemp: number | undefined; | |||
| maxTemp: number | undefined; | |||
| sampleRate: number | undefined; | |||
| passingRate: number | undefined; | |||
| netWeight: number | undefined; | |||
| uom: UomInputs[]; | |||
| weightUnit: WeightUnitInputs[]; | |||
| type: TypeInputs[]; | |||
| action?: any | |||
| } | |||
| export const fetchMaterials = cache(async () => { | |||
| return serverFetchJson<MaterialResult[]>(`${BASE_API_URL}/materials`, { | |||
| next: { tags: ["materials"] }, | |||
| export const fetchAllMaterials = cache(async () => { | |||
| return serverFetchJson<MaterialResult[]>(`${BASE_API_URL}/material`, { | |||
| next: { tags: ["material"] }, | |||
| }); | |||
| }); | |||
| }); | |||
| export const fetchMaterial = cache(async (id: number) => { | |||
| return serverFetchJson<MaterialResult>(`${BASE_API_URL}/material/details/${id}`, { | |||
| next: { tags: ["material"] }, | |||
| }); | |||
| }); | |||
| @@ -0,0 +1,37 @@ | |||
| "use server"; | |||
| import { ServerFetchError, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | |||
| import { revalidateTag } from "next/cache"; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| import { CreateProductMaterialResponse, TypeInputs, UomInputs, WeightUnitInputs } from "../material/actions"; | |||
| export type CreateProductInputs = { | |||
| id?: string | number | |||
| code: string; | |||
| name: string; | |||
| isConsumables: boolean; | |||
| description?: string | undefined; | |||
| remarks?: string | undefined; | |||
| shelfLife?: number | undefined; | |||
| countryOfOrigin?: string | undefined; | |||
| minHumid?: number | undefined; | |||
| maxHumid?: number | undefined; | |||
| minTemp?: number | undefined; | |||
| maxTemp?: number | undefined; | |||
| sampleRate?: number | undefined; | |||
| passingRate?: number | undefined; | |||
| netWeight?: number | undefined; | |||
| type?: TypeInputs[]; | |||
| uom?: UomInputs[]; | |||
| weightUnit?: WeightUnitInputs[]; | |||
| } | |||
| export const saveProduct = async (data: CreateProductInputs) => { | |||
| // try { | |||
| const material = await serverFetchJson<CreateProductMaterialResponse>(`${BASE_API_URL}/product/new`, { | |||
| method: "POST", | |||
| body: JSON.stringify(data), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }); | |||
| revalidateTag("product"); | |||
| return material | |||
| }; | |||
| @@ -0,0 +1,40 @@ | |||
| import { cache } from "react"; | |||
| import "server-only"; | |||
| import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| export type ProductResult = { | |||
| id: string | number | |||
| code: string; | |||
| name: string; | |||
| isConsumables: boolean; | |||
| description: string | undefined; | |||
| type: string | undefined; | |||
| remarks: string | undefined; | |||
| shelfLife: Number | undefined; | |||
| countryOfOrigin: string | undefined; | |||
| minHumid: number | undefined; | |||
| maxHumid: number | undefined; | |||
| minTemp: number | undefined; | |||
| maxTemp: number | undefined; | |||
| sampleRate: number | undefined; | |||
| passingRate: number | undefined; | |||
| netWeight: number | undefined; | |||
| uom: string[] | any[]; | |||
| weightUnit: string[] | any[]; | |||
| action?: any; | |||
| } | |||
| export const fetchAllMaterials = cache(async () => { | |||
| return serverFetchJson<ProductResult[]>(`${BASE_API_URL}/product`, { | |||
| next: { tags: ["product"] }, | |||
| }); | |||
| }); | |||
| export const fetchMaterial = cache(async (id: number) => { | |||
| return serverFetchJson<ProductResult>(`${BASE_API_URL}/product/details/${id}`, { | |||
| next: { tags: ["product"] }, | |||
| }); | |||
| }); | |||
| @@ -0,0 +1,4 @@ | |||
| export enum TypeEnum { | |||
| PRODUCT = "product", | |||
| MATERIAL = "material", | |||
| } | |||
| @@ -1,88 +0,0 @@ | |||
| "use client"; | |||
| import { useCallback, useState } from "react"; | |||
| import { useRouter, useSearchParams } from "next/navigation"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { CreateMaterialInputs } from "@/app/api/settings/material/actions"; | |||
| import { | |||
| FormProvider, | |||
| SubmitErrorHandler, | |||
| SubmitHandler, | |||
| useForm, | |||
| } from "react-hook-form"; | |||
| import { deleteDialog } from "../Swal/CustomAlerts"; | |||
| import { Box, Button, Grid, Stack, Typography } from "@mui/material"; | |||
| import MaterialDetails from "./MaterialDetails"; | |||
| import { Check, Close, EditNote } from "@mui/icons-material"; | |||
| type Props = { | |||
| isEditMode: boolean; | |||
| }; | |||
| const CreateStaff: React.FC<Props> = ({ isEditMode }) => { | |||
| const [serverError, setServerError] = useState(""); | |||
| const [tabIndex, setTabIndex] = useState(0); | |||
| const { t } = useTranslation(); | |||
| const router = useRouter(); | |||
| const formProps = useForm<CreateMaterialInputs>({ | |||
| defaultValues: {}, | |||
| }); | |||
| const handleCancel = () => { | |||
| router.replace("/materials"); | |||
| }; | |||
| const onSubmit = useCallback<SubmitHandler<CreateMaterialInputs>>( | |||
| async (data, event) => { | |||
| try { | |||
| console.log(data) | |||
| } catch (e) { | |||
| } | |||
| }, | |||
| [router, t] | |||
| ); | |||
| const onSubmitError = useCallback<SubmitErrorHandler<CreateMaterialInputs>>( | |||
| (errors) => {}, | |||
| [] | |||
| ); | |||
| const errors = formProps.formState.errors; | |||
| return ( | |||
| <> | |||
| <FormProvider {...formProps}> | |||
| <Stack | |||
| spacing={2} | |||
| component="form" | |||
| onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | |||
| > | |||
| <Grid> | |||
| <Typography mb={2} variant="h4"> | |||
| {isEditMode ? t("Edit Material") : t("Create Material")} | |||
| </Typography> | |||
| </Grid> | |||
| <MaterialDetails isEditMode={isEditMode} /> | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| <Button | |||
| name="submit" | |||
| variant="contained" | |||
| startIcon={<Check />} | |||
| type="submit" | |||
| // disabled={submitDisabled} | |||
| > | |||
| {isEditMode ? t("Save") : t("Confirm")} | |||
| </Button> | |||
| <Button | |||
| variant="outlined" | |||
| startIcon={<Close />} | |||
| onClick={handleCancel} | |||
| > | |||
| {t("Cancel")} | |||
| </Button> | |||
| </Stack> | |||
| </Stack> | |||
| </FormProvider> | |||
| </> | |||
| ); | |||
| }; | |||
| export default CreateStaff; | |||
| @@ -1,23 +0,0 @@ | |||
| import CreateStaff from "./CreateMaterial"; | |||
| import CreateMaterialLoading from "./CreateMaterialLoading"; | |||
| import { cookies } from 'next/headers' | |||
| interface SubComponents { | |||
| Loading: typeof CreateMaterialLoading; | |||
| } | |||
| type CreateMaterialProps = { | |||
| isEditMode?: false; | |||
| }; | |||
| type Props = CreateMaterialProps | |||
| const CreateMaterialWrapper: React.FC<Props> & SubComponents = async (props) => { | |||
| const cookieStore = await cookies() | |||
| // console.log("==================cookieStore==================") | |||
| // console.log(cookieStore) | |||
| return <CreateStaff isEditMode={Boolean(props.isEditMode)}/> | |||
| } | |||
| CreateMaterialWrapper.Loading = CreateMaterialLoading; | |||
| export default CreateMaterialWrapper | |||
| @@ -1 +0,0 @@ | |||
| export { default } from "./CreateMaterialWrapper"; | |||
| @@ -0,0 +1,202 @@ | |||
| "use client"; | |||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { useRouter, useSearchParams } from "next/navigation"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { | |||
| CreateMaterialInputs, | |||
| CreateProductMaterialResponse, | |||
| saveMaterial, | |||
| } from "@/app/api/settings/material/actions"; | |||
| import { | |||
| FormProvider, | |||
| SubmitErrorHandler, | |||
| SubmitHandler, | |||
| useForm, | |||
| } from "react-hook-form"; | |||
| import { deleteDialog } from "../Swal/CustomAlerts"; | |||
| import { Box, Button, Grid, Stack, Typography } from "@mui/material"; | |||
| import { Check, Close, EditNote } from "@mui/icons-material"; | |||
| import { TypeEnum } from "@/app/utils/typeEnum"; | |||
| import { | |||
| CreateProductInputs, | |||
| saveProduct, | |||
| } from "@/app/api/settings/product/actions"; | |||
| import { CreateInputsFields } from "./CreateProductMaterialWrapper"; | |||
| import MaterialDetails from "./MaterialDetails"; | |||
| import ProductDetails from "./ProductDetails"; | |||
| type Props = { | |||
| isEditMode: boolean; | |||
| type: TypeEnum; | |||
| defaultValues: Partial<CreateInputsFields> | undefined; | |||
| }; | |||
| const CreateProductMaterial: React.FC<Props> = ({ | |||
| isEditMode, | |||
| type, | |||
| defaultValues, | |||
| }) => { | |||
| const [serverError, setServerError] = useState(""); | |||
| const [tabIndex, setTabIndex] = useState(0); | |||
| const { t } = useTranslation(); | |||
| const router = useRouter(); | |||
| const [title, mode, redirPath] = useMemo(() => { | |||
| var title = ""; | |||
| var mode = ""; | |||
| var redirPath = ""; | |||
| if (type === TypeEnum.MATERIAL) { | |||
| title = "Material"; | |||
| redirPath = "/material"; | |||
| } | |||
| if (type === TypeEnum.PRODUCT) { | |||
| title = "Project"; | |||
| redirPath = "/project"; | |||
| } | |||
| if (isEditMode) { | |||
| mode = "Edit"; | |||
| } else { | |||
| mode = "Create"; | |||
| } | |||
| return [title, mode, redirPath]; | |||
| }, [type, isEditMode]); | |||
| const formProps = useForm<CreateInputsFields>({ | |||
| defaultValues: defaultValues ? defaultValues : {}, | |||
| }); | |||
| const errors = formProps.formState.errors; | |||
| const handleCancel = () => { | |||
| router.replace(`/settings/${type}`); | |||
| }; | |||
| const onSubmit = useCallback<SubmitHandler<CreateInputsFields>>( | |||
| async (data, event) => { | |||
| let hasErrors = false; | |||
| try { | |||
| // checking humid input | |||
| if (data.maxHumid && data.minHumid!! > data.maxHumid!!) { | |||
| const message = "minHumid should not be greater than maxHumid"; | |||
| formProps.setError("minHumid", { | |||
| message: message, | |||
| type: "required", | |||
| }); | |||
| formProps.setError("maxHumid", { | |||
| message: message, | |||
| type: "required", | |||
| }); | |||
| hasErrors = true; | |||
| } | |||
| // checking temp input | |||
| if (data.maxTemp && data.minTemp!! > data.maxTemp!!) { | |||
| const message = "minTemp should not be greater than maxTemp"; | |||
| formProps.setError("minTemp", { | |||
| message: message, | |||
| type: "required", | |||
| }); | |||
| formProps.setError("maxTemp", { | |||
| message: message, | |||
| type: "required", | |||
| }); | |||
| hasErrors = true; | |||
| } | |||
| // checking passing rate | |||
| if (data.passingRate && data.passingRate > 100) { | |||
| const message = "passingRate should not be greater than 100%"; | |||
| formProps.setError("passingRate", { | |||
| message: message, | |||
| type: "required", | |||
| }); | |||
| } | |||
| // checking sampling rate | |||
| if (data.sampleRate && data.sampleRate > 100) { | |||
| const message = "sampleRate should not be greater than 100%"; | |||
| formProps.setError("sampleRate", { | |||
| message: message, | |||
| type: "required", | |||
| }); | |||
| } | |||
| if (hasErrors) { | |||
| setServerError(t("An error has occurred. Please try again later.")); | |||
| return false; | |||
| } | |||
| console.log("data posted"); | |||
| console.log(data); | |||
| // do api | |||
| var response: CreateProductMaterialResponse | undefined; | |||
| if (type === TypeEnum.MATERIAL) { | |||
| response = await saveMaterial(data as CreateMaterialInputs); | |||
| } else if (type === TypeEnum.PRODUCT) { | |||
| response = await saveProduct(data as CreateProductInputs); | |||
| } | |||
| if (response) { | |||
| if (!Boolean(response.id)) { | |||
| formProps.setError(response.errorPosition!!, { | |||
| message: response.message!!, | |||
| type: "required", | |||
| }); | |||
| throw response; | |||
| } else if (Boolean(response.id)) { | |||
| router.replace(redirPath); | |||
| } | |||
| } else { | |||
| throw "no response"; | |||
| } | |||
| } catch (e) { | |||
| // backend error | |||
| setServerError(t("An error has occurred. Please try again later.")); | |||
| console.log(e); | |||
| } | |||
| }, | |||
| [router, t] | |||
| ); | |||
| // multiple tabs | |||
| const onSubmitError = useCallback<SubmitErrorHandler<CreateInputsFields>>( | |||
| (errors) => {}, | |||
| [] | |||
| ); | |||
| return ( | |||
| <> | |||
| <FormProvider {...formProps}> | |||
| <Stack | |||
| spacing={2} | |||
| component="form" | |||
| onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | |||
| > | |||
| <Grid> | |||
| <Typography mb={2} variant="h4"> | |||
| {t(`${mode} ${title}`)} | |||
| </Typography> | |||
| </Grid> | |||
| {serverError && ( | |||
| <Typography variant="body2" color="error" alignSelf="flex-end"> | |||
| {serverError} | |||
| </Typography> | |||
| )} | |||
| {type === TypeEnum.MATERIAL && <MaterialDetails />} | |||
| {type === TypeEnum.PRODUCT && <ProductDetails />} | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| <Button | |||
| name="submit" | |||
| variant="contained" | |||
| startIcon={<Check />} | |||
| type="submit" | |||
| // disabled={submitDisabled} | |||
| > | |||
| {isEditMode ? t("Save") : t("Confirm")} | |||
| </Button> | |||
| <Button | |||
| variant="outlined" | |||
| startIcon={<Close />} | |||
| onClick={handleCancel} | |||
| > | |||
| {t("Cancel")} | |||
| </Button> | |||
| </Stack> | |||
| </Stack> | |||
| </FormProvider> | |||
| </> | |||
| ); | |||
| }; | |||
| export default CreateProductMaterial; | |||
| @@ -5,7 +5,7 @@ import Stack from "@mui/material/Stack"; | |||
| import React from "react"; | |||
| // Can make this nicer | |||
| export const CreateMaterialLoading: React.FC = () => { | |||
| export const CreateProductMaterialLoading: React.FC = () => { | |||
| return ( | |||
| <> | |||
| <Card> | |||
| @@ -37,4 +37,4 @@ export const CreateMaterialLoading: React.FC = () => { | |||
| ); | |||
| }; | |||
| export default CreateMaterialLoading; | |||
| export default CreateProductMaterialLoading; | |||
| @@ -0,0 +1,77 @@ | |||
| import { TypeEnum } from "@/app/utils/typeEnum"; | |||
| import CreateProductMaterial from "./CreateProductMaterial"; | |||
| import CreateMaterialLoading from "./CreateProductMaterialLoading"; | |||
| import { CreateMaterialInputs } from "@/app/api/settings/material/actions"; | |||
| import { CreateProductInputs } from "@/app/api/settings/product/actions"; | |||
| import { fetchMaterial, MaterialResult } from "@/app/api/settings/material"; | |||
| interface SubComponents { | |||
| Loading: typeof CreateMaterialLoading; | |||
| } | |||
| type CreateMaterialProps = { | |||
| // isEditMode: false; | |||
| type: TypeEnum; | |||
| }; | |||
| type EditMaterialProps = { | |||
| // isEditMode: true; | |||
| type: TypeEnum; | |||
| }; | |||
| type CreateProductProps = { | |||
| // isEditMode: false; | |||
| type: TypeEnum; | |||
| }; | |||
| type EditProductProps = { | |||
| // isEditMode: true; | |||
| type: TypeEnum; | |||
| }; | |||
| type Props = | |||
| | CreateMaterialProps | |||
| | EditMaterialProps | |||
| | CreateProductProps | |||
| | EditProductProps; | |||
| export type CreateInputsFields = CreateMaterialInputs | CreateProductInputs; | |||
| const CreateProductMaterialWrapper: React.FC<Props & { id?: number }> & | |||
| SubComponents = async ({ type, id }) => { | |||
| var defaultValues: Partial<CreateInputsFields> = {}; | |||
| if (id && type === TypeEnum.MATERIAL) { | |||
| const result = await fetchMaterial(id); | |||
| console.log(result) | |||
| defaultValues = { | |||
| id: result.id, | |||
| code: result.name, | |||
| name: result.name, | |||
| isConsumables: result.isConsumables, | |||
| description: result.description, | |||
| type: result.type, | |||
| remarks: result.remarks, | |||
| shelfLife: result.shelfLife, | |||
| countryOfOrigin: result.countryOfOrigin, | |||
| minHumid: result.minHumid, | |||
| maxHumid: result.maxHumid, | |||
| minTemp: result.minTemp, | |||
| maxTemp: result.maxTemp, | |||
| sampleRate: result.sampleRate, | |||
| passingRate: result.passingRate, | |||
| netWeight: result.netWeight, | |||
| uom: result.uom, | |||
| weightUnit: result.weightUnit, | |||
| }; | |||
| } | |||
| if (id && type === TypeEnum.PRODUCT) { | |||
| defaultValues = {}; | |||
| } | |||
| return ( | |||
| <CreateProductMaterial | |||
| isEditMode={Boolean(id)} | |||
| type={type} | |||
| defaultValues={defaultValues} | |||
| /> | |||
| ); | |||
| }; | |||
| CreateProductMaterialWrapper.Loading = CreateMaterialLoading; | |||
| export default CreateProductMaterialWrapper; | |||
| @@ -0,0 +1,340 @@ | |||
| "use client"; | |||
| import { CreateMaterialInputs } from "@/app/api/settings/material/actions"; | |||
| import { | |||
| Box, | |||
| Card, | |||
| CardContent, | |||
| Checkbox, | |||
| FilledInputProps, | |||
| FormControlLabel, | |||
| FormGroup, | |||
| Grid, | |||
| InputBaseComponentProps, | |||
| OutlinedInputProps, | |||
| Stack, | |||
| TextField, | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import { Controller, useFormContext } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import InputDataGrid from "../InputDataGrid"; | |||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { GridColDef, GridRowModel } from "@mui/x-data-grid"; | |||
| import { InputDataGridProps, TableRow } from "../InputDataGrid/InputDataGrid"; | |||
| import { TypeEnum } from "@/app/utils/typeEnum"; | |||
| import { NumberInputProps } from "./NumberInputProps"; | |||
| type Props = { | |||
| // isEditMode: boolean; | |||
| // type: TypeEnum; | |||
| }; | |||
| export type EntryError = | |||
| | { | |||
| [field in keyof CreateMaterialInputs]?: string; | |||
| } | |||
| | undefined; | |||
| const MaterialDetails: React.FC<Props> = ({}) => { | |||
| const { | |||
| t, | |||
| i18n: { language }, | |||
| } = useTranslation(); | |||
| const { | |||
| register, | |||
| formState: { errors, defaultValues, touchedFields }, | |||
| watch, | |||
| control, | |||
| setValue, | |||
| getValues, | |||
| reset, | |||
| resetField, | |||
| setError, | |||
| clearErrors, | |||
| } = useFormContext<CreateMaterialInputs>(); | |||
| // textfield error msg locale problem | |||
| const typeColumns = useMemo<GridColDef[]>( | |||
| () => [ | |||
| { | |||
| field: "type", | |||
| headerName: "type", | |||
| flex: 1, | |||
| editable: true, | |||
| }, | |||
| ], | |||
| [] | |||
| ); | |||
| const weightUnitColumns = useMemo<GridColDef[]>( | |||
| () => [ | |||
| { | |||
| field: "weightUnit", | |||
| headerName: "Weight Unit", | |||
| flex: 1, | |||
| editable: true, | |||
| }, | |||
| { | |||
| field: "conversion", | |||
| headerName: "conversion", // show base unit | |||
| flex: 1, | |||
| type: "number", | |||
| editable: true, | |||
| }, | |||
| ], | |||
| [] | |||
| ); | |||
| const uomColumns = useMemo<GridColDef[]>( | |||
| () => [ | |||
| { | |||
| field: "uom", | |||
| headerName: "uom", | |||
| flex: 1, | |||
| editable: true, | |||
| }, | |||
| ], | |||
| [] | |||
| ); | |||
| const validationTest = useCallback( | |||
| ( | |||
| newRow: GridRowModel<TableRow<Partial<CreateMaterialInputs>, EntryError>> | |||
| ): EntryError => { | |||
| const error: EntryError = {}; | |||
| console.log(newRow); | |||
| return Object.keys(error).length > 0 ? error : undefined; | |||
| }, | |||
| [] | |||
| ); | |||
| return ( | |||
| <Card sx={{ display: "block" }}> | |||
| <CardContent component={Stack} spacing={4}> | |||
| <Box> | |||
| <Stack direction="row" justifyContent="space-between" gap={1}> | |||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | |||
| {t("Material Details")} | |||
| </Typography> | |||
| <FormControlLabel | |||
| control={ | |||
| <Controller | |||
| name={"isConsumables"} | |||
| control={control} | |||
| render={({ field: props }) => ( | |||
| <Checkbox | |||
| {...props} | |||
| checked={props.value} | |||
| onChange={(e) => props.onChange(e.target.checked)} | |||
| /> | |||
| )} | |||
| /> | |||
| } | |||
| label={ | |||
| <Typography | |||
| variant="overline" | |||
| display="block" | |||
| marginBlockEnd={1} | |||
| > | |||
| {t("Consumables")} | |||
| </Typography> | |||
| } | |||
| /> | |||
| </Stack> | |||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Name")} | |||
| fullWidth | |||
| {...register("name", { | |||
| required: "name required!", | |||
| })} | |||
| error={Boolean(errors.name)} | |||
| helperText={errors.name?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Code")} | |||
| fullWidth | |||
| {...register("code", { | |||
| required: "code required!", | |||
| })} | |||
| error={Boolean(errors.code)} | |||
| helperText={errors.code?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("description")} | |||
| fullWidth | |||
| {...register("description")} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("shelfLife")} | |||
| type="number" | |||
| fullWidth | |||
| {...register("shelfLife", { | |||
| valueAsNumber: true, | |||
| required: "shelfLife required!", | |||
| })} | |||
| error={Boolean(errors.shelfLife)} | |||
| helperText={errors.shelfLife?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("countryOfOrigin")} | |||
| fullWidth | |||
| {...register("countryOfOrigin", { | |||
| required: "countryOfOrigin required!", | |||
| })} | |||
| error={Boolean(errors.countryOfOrigin)} | |||
| helperText={errors.countryOfOrigin?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("minHumid")} | |||
| type="number" | |||
| fullWidth | |||
| inputProps={NumberInputProps} | |||
| {...register("minHumid", { | |||
| valueAsNumber: true, | |||
| // min: 0, | |||
| // max: watch("maxHumid") || 100, | |||
| required: "minHumid required!", | |||
| })} | |||
| error={Boolean(errors.minHumid)} | |||
| helperText={errors.minHumid?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("maxHumid")} | |||
| type="number" | |||
| fullWidth | |||
| inputProps={NumberInputProps} | |||
| {...register("maxHumid", { | |||
| valueAsNumber: true, | |||
| })} | |||
| error={Boolean(errors.maxHumid)} | |||
| helperText={errors.maxHumid?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("minTemp")} | |||
| type="number" | |||
| fullWidth | |||
| inputProps={NumberInputProps} | |||
| {...register("minTemp", { | |||
| valueAsNumber: true, | |||
| required: "minTemp required!", | |||
| })} | |||
| error={Boolean(errors.minTemp)} | |||
| helperText={errors.minTemp?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("maxTemp")} | |||
| type="number" | |||
| fullWidth | |||
| inputProps={NumberInputProps} | |||
| {...register("maxTemp", { | |||
| valueAsNumber: true, | |||
| // min: watch("minTemp"), | |||
| required: "maxTemp required!", | |||
| })} | |||
| error={Boolean(errors.maxTemp)} | |||
| helperText={errors.maxTemp?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("sampleRate")} | |||
| type="number" | |||
| fullWidth | |||
| inputProps={NumberInputProps} | |||
| {...register("sampleRate", { | |||
| valueAsNumber: true, | |||
| min: 0, | |||
| max: 100, | |||
| required: "sampleRate required!", | |||
| })} | |||
| error={Boolean(errors.sampleRate)} | |||
| helperText={errors.sampleRate?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("passingRate")} | |||
| type="number" | |||
| fullWidth | |||
| inputProps={NumberInputProps} | |||
| {...register("passingRate", { | |||
| valueAsNumber: true, | |||
| min: 0, | |||
| max: 100, | |||
| required: "passingRate required!", | |||
| })} | |||
| error={Boolean(errors.passingRate)} // change backend for null or not null | |||
| helperText={errors.passingRate?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6} /> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("remarks")} | |||
| fullWidth | |||
| {...register("remarks", { | |||
| // required: "remarks required!", | |||
| })} | |||
| error={Boolean(errors.remarks)} | |||
| helperText={errors.remarks?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("netWeight")} | |||
| type="number" | |||
| fullWidth | |||
| inputProps={NumberInputProps} | |||
| {...register("netWeight", { | |||
| valueAsNumber: true, | |||
| min: 0, | |||
| required: "netWeight required!", | |||
| })} | |||
| error={Boolean(errors.netWeight)} | |||
| helperText={errors.netWeight?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <InputDataGrid<CreateMaterialInputs, EntryError> | |||
| _formKey={"type"} | |||
| columns={typeColumns} | |||
| validateRow={validationTest} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <InputDataGrid<CreateMaterialInputs, EntryError> | |||
| _formKey={"uom"} | |||
| columns={uomColumns} | |||
| validateRow={validationTest} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <InputDataGrid<CreateMaterialInputs, EntryError> | |||
| _formKey={"weightUnit"} | |||
| columns={weightUnitColumns} | |||
| validateRow={validationTest} | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| </Box> | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| }; | |||
| export default MaterialDetails; | |||
| @@ -0,0 +1,5 @@ | |||
| import { InputBaseComponentProps } from "@mui/material"; | |||
| export var NumberInputProps: InputBaseComponentProps = { | |||
| step: 0.01, | |||
| }; | |||
| @@ -14,20 +14,25 @@ import { useTranslation } from "react-i18next"; | |||
| import InputDataGrid from "../InputDataGrid"; | |||
| import { useCallback, useMemo, useState } from "react"; | |||
| import { GridColDef, GridRowModel } from "@mui/x-data-grid"; | |||
| import { | |||
| InputDataGridProps, | |||
| TableRow, | |||
| } from "../InputDataGrid/InputDataGrid"; | |||
| import { InputDataGridProps, TableRow } from "../InputDataGrid/InputDataGrid"; | |||
| import { TypeEnum } from "@/app/utils/typeEnum"; | |||
| import { CreateProductInputs } from "@/app/api/settings/product/actions"; | |||
| import { NumberInputProps } from "./NumberInputProps"; | |||
| type Props = { | |||
| isEditMode: boolean; | |||
| // isEditMode: boolean; | |||
| // type: TypeEnum; | |||
| }; | |||
| export type EntryError = | |||
| | { | |||
| [field in keyof CreateMaterialInputs]?: string; | |||
| [field in keyof CreateProductInputs]?: string; | |||
| } | |||
| | undefined; | |||
| const MaterialDetails: React.FC<Props> = ({ isEditMode }) => { | |||
| const ProductDetails: React.FC<Props> = ({ | |||
| }) => { | |||
| const { | |||
| t, | |||
| i18n: { language }, | |||
| @@ -44,59 +49,65 @@ const MaterialDetails: React.FC<Props> = ({ isEditMode }) => { | |||
| resetField, | |||
| setError, | |||
| clearErrors, | |||
| } = useFormContext<CreateMaterialInputs>(); | |||
| const typeColumns = useMemo<GridColDef[]>(() => [ | |||
| { | |||
| field: "type", | |||
| headerName: 'type', | |||
| flex: 1, | |||
| editable: true, | |||
| } | |||
| ], []); | |||
| const uomColumns = useMemo<GridColDef[]>(() => [ | |||
| { | |||
| field: "uom", | |||
| headerName: 'uom', | |||
| flex: 1, | |||
| editable: true, | |||
| } | |||
| ], []); | |||
| const validationTest = useCallback((newRow: GridRowModel<TableRow<Partial<CreateMaterialInputs>, EntryError>>): EntryError => { | |||
| const error: EntryError = {}; | |||
| console.log(newRow) | |||
| return Object.keys(error).length > 0 ? error : undefined; | |||
| // return undefined | |||
| }, []); | |||
| // const MaterialTypeInputGridProps: InputDataGridProps< | |||
| // Partial<CreateMaterialInputs>, | |||
| // EntryError | |||
| // > = { | |||
| // _formKey: "type", | |||
| // columns: typeColumns, | |||
| // validateRow: validationTest, | |||
| // }; | |||
| // const MaterialUomInputGridProps: InputDataGridProps< | |||
| // Partial<CreateMaterialInputs>, | |||
| // EntryError | |||
| // > = { | |||
| // _formKey: "uom", | |||
| // columns: uomColumns, | |||
| // validateRow: validationTest, | |||
| // }; | |||
| // const MaterialTypeInputGrid = useInputDataGrid( | |||
| // MaterialTypeInputGridProps | |||
| // ); | |||
| // const MaterialUomInputGrid = useInputDataGrid( | |||
| // MaterialUomInputGridProps | |||
| // ); | |||
| } = useFormContext<CreateProductInputs>(); | |||
| const typeColumns = useMemo<GridColDef[]>( | |||
| () => [ | |||
| { | |||
| field: "type", | |||
| headerName: "type", | |||
| flex: 1, | |||
| editable: true, | |||
| }, | |||
| ], | |||
| [] | |||
| ); | |||
| const weightUnitColumns = useMemo<GridColDef[]>( | |||
| () => [ | |||
| { | |||
| field: "weightUnit", | |||
| headerName: "Weight Unit", | |||
| flex: 1, | |||
| editable: true, | |||
| }, | |||
| { | |||
| field: "conversion", | |||
| headerName: "conversion", // show base unit | |||
| flex: 1, | |||
| type: "number", | |||
| editable: true, | |||
| }, | |||
| ], | |||
| [] | |||
| ); | |||
| const uomColumns = useMemo<GridColDef[]>( | |||
| () => [ | |||
| { | |||
| field: "uom", | |||
| headerName: "uom", | |||
| flex: 1, | |||
| editable: true, | |||
| }, | |||
| ], | |||
| [] | |||
| ); | |||
| const validationTest = useCallback( | |||
| ( | |||
| newRow: GridRowModel<TableRow<Partial<CreateProductInputs>, EntryError>> | |||
| ): EntryError => { | |||
| const error: EntryError = {}; | |||
| console.log(newRow); | |||
| return Object.keys(error).length > 0 ? error : undefined; | |||
| }, | |||
| [] | |||
| ); | |||
| return ( | |||
| <Card sx={{ display: "block" }}> | |||
| <CardContent component={Stack} spacing={4}> | |||
| <Box> | |||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | |||
| {t("Material Details")} | |||
| {t("Product Details")} | |||
| </Typography> | |||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
| <Grid item xs={6}> | |||
| @@ -107,6 +118,7 @@ const MaterialDetails: React.FC<Props> = ({ isEditMode }) => { | |||
| required: "name required!", | |||
| })} | |||
| error={Boolean(errors.name)} | |||
| helperText={errors.name?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| @@ -117,26 +129,27 @@ const MaterialDetails: React.FC<Props> = ({ isEditMode }) => { | |||
| required: "code required!", | |||
| })} | |||
| error={Boolean(errors.code)} | |||
| helperText={errors.code?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("description")} | |||
| fullWidth | |||
| {...register("description", { | |||
| required: "description required!", | |||
| })} | |||
| error={Boolean(errors.description)} | |||
| {...register("description")} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("shelfLife")} | |||
| type="number" | |||
| fullWidth | |||
| {...register("shelfLife", { | |||
| valueAsNumber: true, | |||
| required: "shelfLife required!", | |||
| })} | |||
| error={Boolean(errors.shelfLife)} | |||
| helperText={errors.shelfLife?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| @@ -147,66 +160,97 @@ const MaterialDetails: React.FC<Props> = ({ isEditMode }) => { | |||
| required: "countryOfOrigin required!", | |||
| })} | |||
| error={Boolean(errors.countryOfOrigin)} | |||
| helperText={errors.countryOfOrigin?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("minHumid")} | |||
| type="number" | |||
| fullWidth | |||
| inputProps={NumberInputProps} | |||
| {...register("minHumid", { | |||
| valueAsNumber: true, | |||
| // min: 0, | |||
| // max: watch("maxHumid") || 100, | |||
| required: "minHumid required!", | |||
| })} | |||
| error={Boolean(errors.minHumid)} | |||
| helperText={errors.minHumid?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("maxHumid")} | |||
| type="number" | |||
| fullWidth | |||
| inputProps={NumberInputProps} | |||
| {...register("maxHumid", { | |||
| required: "maxHumid required!", | |||
| valueAsNumber: true, | |||
| })} | |||
| error={Boolean(errors.maxHumid)} | |||
| helperText={errors.maxHumid?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("minTemp")} | |||
| type="number" | |||
| fullWidth | |||
| inputProps={NumberInputProps} | |||
| {...register("minTemp", { | |||
| valueAsNumber: true, | |||
| required: "minTemp required!", | |||
| })} | |||
| error={Boolean(errors.minTemp)} | |||
| helperText={errors.minTemp?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("maxTemp")} | |||
| type="number" | |||
| fullWidth | |||
| inputProps={NumberInputProps} | |||
| {...register("maxTemp", { | |||
| valueAsNumber: true, | |||
| // min: watch("minTemp"), | |||
| required: "maxTemp required!", | |||
| })} | |||
| error={Boolean(errors.maxTemp)} | |||
| helperText={errors.maxTemp?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("sampleRate")} | |||
| type="number" | |||
| fullWidth | |||
| inputProps={NumberInputProps} | |||
| {...register("sampleRate", { | |||
| valueAsNumber: true, | |||
| min: 0, | |||
| max: 100, | |||
| required: "sampleRate required!", | |||
| })} | |||
| error={Boolean(errors.sampleRate)} | |||
| helperText={errors.sampleRate?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("passingRate")} | |||
| type="number" | |||
| fullWidth | |||
| inputProps={NumberInputProps} | |||
| {...register("passingRate", { | |||
| valueAsNumber: true, | |||
| min: 0, | |||
| max: 100, | |||
| required: "passingRate required!", | |||
| })} | |||
| error={Boolean(errors.passingRate)} | |||
| error={Boolean(errors.passingRate)} // change backend for null or not null | |||
| helperText={errors.passingRate?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6} /> | |||
| @@ -215,39 +259,52 @@ const MaterialDetails: React.FC<Props> = ({ isEditMode }) => { | |||
| label={t("remarks")} | |||
| fullWidth | |||
| {...register("remarks", { | |||
| required: "remarks required!", | |||
| // required: "remarks required!", | |||
| })} | |||
| error={Boolean(errors.remarks)} | |||
| helperText={errors.remarks?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("netWeight")} | |||
| type="number" | |||
| fullWidth | |||
| inputProps={NumberInputProps} | |||
| {...register("netWeight", { | |||
| valueAsNumber: true, | |||
| min: 0, | |||
| required: "netWeight required!", | |||
| })} | |||
| error={Boolean(errors.netWeight)} | |||
| helperText={errors.netWeight?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <InputDataGrid<CreateMaterialInputs, EntryError> | |||
| <InputDataGrid<CreateProductInputs, EntryError> | |||
| _formKey={"type"} | |||
| columns={typeColumns} | |||
| validateRow={validationTest} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <InputDataGrid<CreateMaterialInputs, EntryError> | |||
| <InputDataGrid<CreateProductInputs, EntryError> | |||
| _formKey={"uom"} | |||
| columns={uomColumns} | |||
| validateRow={validationTest} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <InputDataGrid<CreateProductInputs, EntryError> | |||
| _formKey={"weightUnit"} | |||
| columns={weightUnitColumns} | |||
| validateRow={validationTest} | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| </Box> | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| }; | |||
| export default MaterialDetails; | |||
| export default ProductDetails; | |||
| @@ -0,0 +1 @@ | |||
| export { default } from "./CreateProductMaterialWrapper"; | |||
| @@ -107,34 +107,31 @@ function InputDataGrid<T, E>({ | |||
| [apiRef, rowModesModel] | |||
| ); | |||
| console.log("asdasdasd") | |||
| const processRowUpdate = useCallback( | |||
| ( | |||
| newRow: GridValidRowModel, | |||
| originalRow: GridValidRowModel, | |||
| // GridRowModel<TableRow<T, E>>, | |||
| // originalRow: GridRowModel<TableRow<T, E>> | |||
| newRow: GridRowModel<TableRow<T, E>>, | |||
| originalRow: GridRowModel<TableRow<T, E>> | |||
| ) => { | |||
| ///////////////// | |||
| // validation here | |||
| // const errors = validateRow(newRow); | |||
| const errors = validateRow(newRow); | |||
| console.log(newRow) | |||
| // if (errors) { | |||
| // throw new ProcessRowUpdateError( | |||
| // originalRow, | |||
| // "validation error", | |||
| // errors, | |||
| // ); | |||
| // } | |||
| if (errors) { | |||
| throw new ProcessRowUpdateError( | |||
| originalRow, | |||
| "validation error", | |||
| errors, | |||
| ); | |||
| } | |||
| ///////////////// | |||
| const { _isNew, _error, ...updatedRow } = newRow; | |||
| const rowToSave = { | |||
| ...updatedRow, | |||
| } as TableRow<T, E>; /// test | |||
| console.log(rowToSave) | |||
| // setRows((rw) => | |||
| // rw.map((r) => (getRowId(r) === getRowId(originalRow) ? rowToSave : r)) | |||
| // ); | |||
| setRows((rw) => | |||
| rw.map((r) => (getRowId(r) === getRowId(originalRow) ? rowToSave : r)) | |||
| ); | |||
| return rowToSave; | |||
| }, | |||
| [validateRow, getRowId] | |||
| @@ -190,7 +187,7 @@ function InputDataGrid<T, E>({ | |||
| field: "actions", | |||
| type: "actions", | |||
| headerName: "", | |||
| width: 100, | |||
| flex: 0.5, | |||
| cellClassName: "actions", | |||
| getActions: ({ id }: { id: GridRowId }) => { | |||
| const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; | |||
| @@ -230,18 +227,10 @@ function InputDataGrid<T, E>({ | |||
| ], | |||
| [columns,rowModesModel, handleSave, handleCancel, handleDelete] | |||
| ); | |||
| // const processRowUpdate = (newRow: GridRowModel) => { | |||
| // const updatedRow = { ...newRow, isNew: false }; | |||
| // console.log("asdasdasdsda") | |||
| // // setRows(rows.map((row) => (row.id === newRow.id ? updatedRow : row))); | |||
| // return updatedRow; | |||
| // }; | |||
| // sync useForm | |||
| useEffect(() => { | |||
| console.log(formKey) | |||
| console.log(rows) | |||
| // console.log(formKey) | |||
| // console.log(rows) | |||
| setValue(formKey, rows); | |||
| }, [formKey, rows]); | |||
| @@ -294,7 +283,7 @@ function InputDataGrid<T, E>({ | |||
| }, | |||
| }} | |||
| disableColumnMenu | |||
| processRowUpdate={processRowUpdate} | |||
| processRowUpdate={processRowUpdate as any} | |||
| // onRowEditStop={handleRowEditStop} | |||
| rowModesModel={rowModesModel} | |||
| onRowModesModelChange={setRowModesModel} | |||
| @@ -1,96 +0,0 @@ | |||
| "use client" | |||
| import { useCallback, useMemo, useState } from "react"; | |||
| import SearchBox, { Criterion } from "../SearchBox"; | |||
| import { MaterialResult } from "@/app/api/settings/material"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import SearchResults, { Column } from "../SearchResults"; | |||
| import { EditNote } from "@mui/icons-material"; | |||
| import { useRouter, useSearchParams } from "next/navigation"; | |||
| import { GridDeleteIcon } from "@mui/x-data-grid"; | |||
| type Props = { | |||
| materials: MaterialResult[] | |||
| } | |||
| type SearchQuery = Partial<Omit<MaterialResult, "id">>; | |||
| type SearchParamNames = keyof SearchQuery; | |||
| const MaterialSearch: React.FC<Props> = ({ | |||
| materials | |||
| }) => { | |||
| const [filteredMaterials, setFilteredMaterials] = useState<MaterialResult[]>(materials) | |||
| const { t } = useTranslation("materials"); | |||
| const router = useRouter(); | |||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||
| () => [ | |||
| { label: t("Code"), paramName: "code", type: "text" }, | |||
| { label: t("Name"), paramName: "name", type: "text" }, | |||
| ], [t, materials]) | |||
| const onMaterialClick = useCallback( | |||
| (material: MaterialResult) => { | |||
| }, | |||
| [router], | |||
| ); | |||
| const onDeleteClick = useCallback( | |||
| (material: MaterialResult) => { | |||
| }, | |||
| [router], | |||
| ); | |||
| const columns = useMemo<Column<MaterialResult>[]>(() => [ | |||
| { | |||
| name: "id", | |||
| label: t("Details"), | |||
| onClick: onMaterialClick, | |||
| buttonIcon: <EditNote />, | |||
| }, | |||
| { | |||
| name: "code", | |||
| label: t("Code"), | |||
| }, | |||
| { | |||
| name: "name", | |||
| label: t("Name"), | |||
| }, | |||
| { | |||
| name: "action", | |||
| label: t(""), | |||
| buttonIcon: <GridDeleteIcon />, | |||
| onClick: onDeleteClick, | |||
| }, | |||
| ], [filteredMaterials]) | |||
| const onReset = useCallback(() => { | |||
| setFilteredMaterials(materials); | |||
| }, [materials]); | |||
| return ( | |||
| <> | |||
| <SearchBox | |||
| criteria={searchCriteria} | |||
| onSearch={(query) => { | |||
| setFilteredMaterials( | |||
| materials.filter( | |||
| (mat) => | |||
| mat.code.toLowerCase().includes(query.code.toLowerCase()) && | |||
| mat.name.toLowerCase().includes(query.name.toLowerCase()) | |||
| ), | |||
| ); | |||
| }} | |||
| onReset={onReset} | |||
| /> | |||
| <SearchResults<MaterialResult> | |||
| items={filteredMaterials} | |||
| columns={columns} | |||
| /> | |||
| </> | |||
| ) | |||
| } | |||
| export default MaterialSearch | |||
| @@ -1,22 +0,0 @@ | |||
| import { fetchMaterials, MaterialResult } from "@/app/api/settings/material"; | |||
| import MaterialSearch from "./MaterialSearch"; | |||
| import MaterialSearchLoading from "./MaterialSearchLoading"; | |||
| interface SubComponents { | |||
| Loading: typeof MaterialSearchLoading; | |||
| } | |||
| const MaterialSearchWrapper: React.FC & SubComponents = async () => { | |||
| const materials = await fetchMaterials(); | |||
| console.log(materials) | |||
| return ( | |||
| <MaterialSearch | |||
| materials={materials} | |||
| /> | |||
| ) | |||
| } | |||
| MaterialSearchWrapper.Loading = MaterialSearchLoading; | |||
| export default MaterialSearchWrapper; | |||
| @@ -1 +0,0 @@ | |||
| export { default } from "./MaterialSearchWrapper"; | |||
| @@ -193,8 +193,8 @@ const NavigationContent: React.FC = () => { | |||
| }, | |||
| { | |||
| icon: <RequestQuote />, | |||
| label: "Finished Goods", | |||
| path: "/settings/user", | |||
| label: "Product", | |||
| path: "/settings/product", | |||
| }, | |||
| { | |||
| icon: <RequestQuote />, | |||
| @@ -0,0 +1,114 @@ | |||
| "use client"; | |||
| import { useCallback, useMemo, useState } from "react"; | |||
| import SearchBox, { Criterion } from "../SearchBox"; | |||
| import { MaterialResult } from "@/app/api/settings/material"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import SearchResults, { Column } from "../SearchResults"; | |||
| import { EditNote } from "@mui/icons-material"; | |||
| import { useRouter, useSearchParams } from "next/navigation"; | |||
| import { GridDeleteIcon } from "@mui/x-data-grid"; | |||
| import { TypeEnum } from "@/app/utils/typeEnum"; | |||
| import { ProductResult } from "@/app/api/settings/product"; | |||
| export type ListResult = MaterialResult | ProductResult; | |||
| type Props = { | |||
| productMaterial: ListResult[]; | |||
| type: TypeEnum; | |||
| }; | |||
| type SearchQuery = Partial<Omit<ListResult, "id">>; | |||
| type SearchParamNames = keyof SearchQuery; | |||
| const MaterialSearch: React.FC<Props> = ({ productMaterial, type }) => { | |||
| const [filteredItems, setFilteredItems] = useState<ListResult[]>(productMaterial); | |||
| const { t } = useTranslation(type); | |||
| const router = useRouter(); | |||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||
| () => { | |||
| var searchCriteria: Criterion<SearchParamNames>[] = [] | |||
| if (type === TypeEnum.MATERIAL) { | |||
| searchCriteria = [ | |||
| { label: t("Code"), paramName: "code", type: "text" }, | |||
| { label: t("Name"), paramName: "name", type: "text" }, | |||
| ] | |||
| } | |||
| if (type === TypeEnum.MATERIAL) { | |||
| } | |||
| return searchCriteria | |||
| }, | |||
| [t, productMaterial] | |||
| ); | |||
| const onDetailClick = useCallback( | |||
| (productMaterial: ListResult) => { | |||
| router.push(`/settings/${type}/edit?id=${productMaterial.id}`); | |||
| }, | |||
| [type, router] | |||
| ); | |||
| const onDeleteClick = useCallback( | |||
| (productMaterial: ListResult) => {}, | |||
| [router] | |||
| ); | |||
| const columns = useMemo<Column<ListResult>[]>( | |||
| () => [ | |||
| { | |||
| name: "id", | |||
| label: t("Details"), | |||
| onClick: onDetailClick, | |||
| buttonIcon: <EditNote />, | |||
| }, | |||
| { | |||
| name: "code", | |||
| label: t("Code"), | |||
| }, | |||
| { | |||
| name: "name", | |||
| label: t("Name"), | |||
| }, | |||
| { | |||
| name: "action", | |||
| label: t(""), | |||
| buttonIcon: <GridDeleteIcon />, | |||
| onClick: onDeleteClick, | |||
| }, | |||
| ], | |||
| [filteredItems] | |||
| ); | |||
| const onReset = useCallback(() => { | |||
| setFilteredItems(productMaterial); | |||
| }, [productMaterial]); | |||
| return ( | |||
| <> | |||
| <SearchBox | |||
| criteria={searchCriteria} | |||
| onSearch={(query) => { | |||
| setFilteredItems( | |||
| productMaterial.filter((pm) => { | |||
| if (type === TypeEnum.MATERIAL) { | |||
| return ( | |||
| pm.code.toLowerCase().includes(query.code.toLowerCase()) && | |||
| pm.name.toLowerCase().includes(query.name.toLowerCase()) | |||
| ); | |||
| } else if (type === TypeEnum.PRODUCT) { | |||
| return ( | |||
| pm.code.toLowerCase().includes(query.code.toLowerCase()) && | |||
| pm.name.toLowerCase().includes(query.name.toLowerCase()) | |||
| ); | |||
| } | |||
| }) | |||
| ); | |||
| }} | |||
| onReset={onReset} | |||
| /> | |||
| <SearchResults<ListResult> items={filteredItems} columns={columns} /> | |||
| </> | |||
| ); | |||
| }; | |||
| export default MaterialSearch; | |||
| @@ -5,7 +5,7 @@ import Stack from "@mui/material/Stack"; | |||
| import React from "react"; | |||
| // Can make this nicer | |||
| export const MaterialSearchLoading: React.FC = () => { | |||
| export const ProductMaterialSearchLoading: React.FC = () => { | |||
| return ( | |||
| <> | |||
| <Card> | |||
| @@ -37,4 +37,4 @@ export const MaterialSearchLoading: React.FC = () => { | |||
| ); | |||
| }; | |||
| export default MaterialSearchLoading; | |||
| export default ProductMaterialSearchLoading; | |||
| @@ -0,0 +1,30 @@ | |||
| import { fetchAllMaterials, MaterialResult } from "@/app/api/settings/material"; | |||
| import ProductMaterialSearch, { ListResult } from "./ProductMaterialSearch"; | |||
| import MaterialSearchLoading from "./ProductMaterialSearchLoading"; | |||
| import { SearchParams } from "@/app/utils/fetchUtil"; | |||
| import { TypeEnum } from "@/app/utils/typeEnum"; | |||
| interface SubComponents { | |||
| Loading: typeof MaterialSearchLoading; | |||
| } | |||
| type Props = { | |||
| type: TypeEnum; | |||
| }; | |||
| const ProductMaterialSearchWrapper: React.FC<Props> & SubComponents = async ({ | |||
| type | |||
| }) => { | |||
| var result: ListResult[] = [] | |||
| if (TypeEnum.PRODUCT === type) { | |||
| result = [] | |||
| } else if (TypeEnum.MATERIAL === type) { | |||
| result = await fetchAllMaterials(); | |||
| } | |||
| return <ProductMaterialSearch productMaterial={result} type={TypeEnum.MATERIAL} />; | |||
| }; | |||
| ProductMaterialSearchWrapper.Loading = MaterialSearchLoading; | |||
| export default ProductMaterialSearchWrapper; | |||
| @@ -0,0 +1 @@ | |||
| export { default } from "./ProductMaterialSearchWrapper"; | |||