@@ -1,19 +1,22 @@ | |||||
import { SearchParams } from "@/app/utils/fetchUtil"; | 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 { I18nProvider, getServerI18n } from "@/i18n"; | ||||
import { Typography } from "@mui/material"; | import { Typography } from "@mui/material"; | ||||
import { isString } from "lodash"; | |||||
type Props = {} & SearchParams; | type Props = {} & SearchParams; | ||||
const materialSetting: React.FC<Props> = async ({ 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); | console.log(searchParams); | ||||
return ( | return ( | ||||
<> | <> | ||||
{/* <Typography variant="h4">{t("Create Material")}</Typography> */} | {/* <Typography variant="h4">{t("Create Material")}</Typography> */} | ||||
<I18nProvider namespaces={["materials"]}> | |||||
<CreateMaterial isEditMode={false} /> | |||||
<I18nProvider namespaces={[type]}> | |||||
<CreateProductMaterial type={type} /> | |||||
</I18nProvider> | </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 { getServerI18n } from "@/i18n"; | ||||
import Add from "@mui/icons-material/Add"; | import Add from "@mui/icons-material/Add"; | ||||
import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||
@@ -13,8 +14,9 @@ export const metadata: Metadata = { | |||||
}; | }; | ||||
const materialSetting: React.FC = async () => { | const materialSetting: React.FC = async () => { | ||||
const { t } = await getServerI18n("material"); | |||||
// preloadClaims(); | |||||
const material = TypeEnum.MATERIAL | |||||
const { t } = await getServerI18n(material); | |||||
// preloadClaims(); | |||||
return ( | return ( | ||||
<> | <> | ||||
@@ -37,7 +39,7 @@ const materialSetting: React.FC = async () => { | |||||
</Button> | </Button> | ||||
</Stack> | </Stack> | ||||
<Suspense fallback={<MaterialSearch.Loading />}> | <Suspense fallback={<MaterialSearch.Loading />}> | ||||
<MaterialSearch /> | |||||
<MaterialSearch type={material} /> | |||||
</Suspense> | </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 { ServerFetchError, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | ||||
import { revalidateTag } from "next/cache"; | import { revalidateTag } from "next/cache"; | ||||
import { BASE_API_URL } from "@/config/api"; | 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 = { | export type CreateMaterialInputs = { | ||||
id?: string | number | id?: string | number | ||||
code: string; | code: string; | ||||
name: string; | name: string; | ||||
isConsumables: boolean; | isConsumables: boolean; | ||||
// same goes for other props | |||||
// change backend for null or not null | |||||
description?: string | undefined; | description?: string | undefined; | ||||
type?: string | undefined; | |||||
remarks?: string | undefined; | remarks?: string | undefined; | ||||
shelfLife?: Number | undefined; | shelfLife?: Number | undefined; | ||||
countryOfOrigin?: string | undefined; | countryOfOrigin?: string | undefined; | ||||
@@ -20,17 +32,25 @@ export type CreateMaterialInputs = { | |||||
sampleRate?: number | undefined; | sampleRate?: number | undefined; | ||||
passingRate?: number | undefined; | passingRate?: number | undefined; | ||||
netWeight?: 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) => { | export const saveMaterial = async (data: CreateMaterialInputs) => { | ||||
// try { | // try { | ||||
const materials = await serverFetchJson(`${BASE_API_URL}/materials/save`, { | |||||
const material = await serverFetchJson<CreateProductMaterialResponse>(`${BASE_API_URL}/material/new`, { | |||||
method: "POST", | method: "POST", | ||||
body: JSON.stringify(data), | body: JSON.stringify(data), | ||||
headers: { "Content-Type": "application/json" }, | 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 "server-only"; | ||||
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 { TypeInputs, UomInputs, WeightUnitInputs } from "./actions"; | |||||
export type MaterialResult = { | export type MaterialResult = { | ||||
id: number; | |||||
id: string | number | |||||
code: string; | code: string; | ||||
name: 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"; | import React from "react"; | ||||
// Can make this nicer | // Can make this nicer | ||||
export const CreateMaterialLoading: React.FC = () => { | |||||
export const CreateProductMaterialLoading: React.FC = () => { | |||||
return ( | return ( | ||||
<> | <> | ||||
<Card> | <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 InputDataGrid from "../InputDataGrid"; | ||||
import { useCallback, useMemo, useState } from "react"; | import { useCallback, useMemo, useState } from "react"; | ||||
import { GridColDef, GridRowModel } from "@mui/x-data-grid"; | 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 = { | type Props = { | ||||
isEditMode: boolean; | |||||
// isEditMode: boolean; | |||||
// type: TypeEnum; | |||||
}; | }; | ||||
export type EntryError = | export type EntryError = | ||||
| { | | { | ||||
[field in keyof CreateMaterialInputs]?: string; | |||||
[field in keyof CreateProductInputs]?: string; | |||||
} | } | ||||
| undefined; | | undefined; | ||||
const MaterialDetails: React.FC<Props> = ({ isEditMode }) => { | |||||
const ProductDetails: React.FC<Props> = ({ | |||||
}) => { | |||||
const { | const { | ||||
t, | t, | ||||
i18n: { language }, | i18n: { language }, | ||||
@@ -44,59 +49,65 @@ const MaterialDetails: React.FC<Props> = ({ isEditMode }) => { | |||||
resetField, | resetField, | ||||
setError, | setError, | ||||
clearErrors, | 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 ( | return ( | ||||
<Card sx={{ display: "block" }}> | <Card sx={{ display: "block" }}> | ||||
<CardContent component={Stack} spacing={4}> | <CardContent component={Stack} spacing={4}> | ||||
<Box> | <Box> | ||||
<Typography variant="overline" display="block" marginBlockEnd={1}> | <Typography variant="overline" display="block" marginBlockEnd={1}> | ||||
{t("Material Details")} | |||||
{t("Product Details")} | |||||
</Typography> | </Typography> | ||||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
@@ -107,6 +118,7 @@ const MaterialDetails: React.FC<Props> = ({ isEditMode }) => { | |||||
required: "name required!", | required: "name required!", | ||||
})} | })} | ||||
error={Boolean(errors.name)} | error={Boolean(errors.name)} | ||||
helperText={errors.name?.message} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
@@ -117,26 +129,27 @@ const MaterialDetails: React.FC<Props> = ({ isEditMode }) => { | |||||
required: "code required!", | required: "code required!", | ||||
})} | })} | ||||
error={Boolean(errors.code)} | error={Boolean(errors.code)} | ||||
helperText={errors.code?.message} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<TextField | <TextField | ||||
label={t("description")} | label={t("description")} | ||||
fullWidth | fullWidth | ||||
{...register("description", { | |||||
required: "description required!", | |||||
})} | |||||
error={Boolean(errors.description)} | |||||
{...register("description")} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<TextField | <TextField | ||||
label={t("shelfLife")} | label={t("shelfLife")} | ||||
type="number" | |||||
fullWidth | fullWidth | ||||
{...register("shelfLife", { | {...register("shelfLife", { | ||||
valueAsNumber: true, | |||||
required: "shelfLife required!", | required: "shelfLife required!", | ||||
})} | })} | ||||
error={Boolean(errors.shelfLife)} | error={Boolean(errors.shelfLife)} | ||||
helperText={errors.shelfLife?.message} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
@@ -147,66 +160,97 @@ const MaterialDetails: React.FC<Props> = ({ isEditMode }) => { | |||||
required: "countryOfOrigin required!", | required: "countryOfOrigin required!", | ||||
})} | })} | ||||
error={Boolean(errors.countryOfOrigin)} | error={Boolean(errors.countryOfOrigin)} | ||||
helperText={errors.countryOfOrigin?.message} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<TextField | <TextField | ||||
label={t("minHumid")} | label={t("minHumid")} | ||||
type="number" | |||||
fullWidth | fullWidth | ||||
inputProps={NumberInputProps} | |||||
{...register("minHumid", { | {...register("minHumid", { | ||||
valueAsNumber: true, | |||||
// min: 0, | |||||
// max: watch("maxHumid") || 100, | |||||
required: "minHumid required!", | required: "minHumid required!", | ||||
})} | })} | ||||
error={Boolean(errors.minHumid)} | error={Boolean(errors.minHumid)} | ||||
helperText={errors.minHumid?.message} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<TextField | <TextField | ||||
label={t("maxHumid")} | label={t("maxHumid")} | ||||
type="number" | |||||
fullWidth | fullWidth | ||||
inputProps={NumberInputProps} | |||||
{...register("maxHumid", { | {...register("maxHumid", { | ||||
required: "maxHumid required!", | |||||
valueAsNumber: true, | |||||
})} | })} | ||||
error={Boolean(errors.maxHumid)} | error={Boolean(errors.maxHumid)} | ||||
helperText={errors.maxHumid?.message} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<TextField | <TextField | ||||
label={t("minTemp")} | label={t("minTemp")} | ||||
type="number" | |||||
fullWidth | fullWidth | ||||
inputProps={NumberInputProps} | |||||
{...register("minTemp", { | {...register("minTemp", { | ||||
valueAsNumber: true, | |||||
required: "minTemp required!", | required: "minTemp required!", | ||||
})} | })} | ||||
error={Boolean(errors.minTemp)} | error={Boolean(errors.minTemp)} | ||||
helperText={errors.minTemp?.message} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<TextField | <TextField | ||||
label={t("maxTemp")} | label={t("maxTemp")} | ||||
type="number" | |||||
fullWidth | fullWidth | ||||
inputProps={NumberInputProps} | |||||
{...register("maxTemp", { | {...register("maxTemp", { | ||||
valueAsNumber: true, | |||||
// min: watch("minTemp"), | |||||
required: "maxTemp required!", | required: "maxTemp required!", | ||||
})} | })} | ||||
error={Boolean(errors.maxTemp)} | error={Boolean(errors.maxTemp)} | ||||
helperText={errors.maxTemp?.message} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<TextField | <TextField | ||||
label={t("sampleRate")} | label={t("sampleRate")} | ||||
type="number" | |||||
fullWidth | fullWidth | ||||
inputProps={NumberInputProps} | |||||
{...register("sampleRate", { | {...register("sampleRate", { | ||||
valueAsNumber: true, | |||||
min: 0, | |||||
max: 100, | |||||
required: "sampleRate required!", | required: "sampleRate required!", | ||||
})} | })} | ||||
error={Boolean(errors.sampleRate)} | error={Boolean(errors.sampleRate)} | ||||
helperText={errors.sampleRate?.message} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<TextField | <TextField | ||||
label={t("passingRate")} | label={t("passingRate")} | ||||
type="number" | |||||
fullWidth | fullWidth | ||||
inputProps={NumberInputProps} | |||||
{...register("passingRate", { | {...register("passingRate", { | ||||
valueAsNumber: true, | |||||
min: 0, | |||||
max: 100, | |||||
required: "passingRate required!", | required: "passingRate required!", | ||||
})} | })} | ||||
error={Boolean(errors.passingRate)} | |||||
error={Boolean(errors.passingRate)} // change backend for null or not null | |||||
helperText={errors.passingRate?.message} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={6} /> | <Grid item xs={6} /> | ||||
@@ -215,39 +259,52 @@ const MaterialDetails: React.FC<Props> = ({ isEditMode }) => { | |||||
label={t("remarks")} | label={t("remarks")} | ||||
fullWidth | fullWidth | ||||
{...register("remarks", { | {...register("remarks", { | ||||
required: "remarks required!", | |||||
// required: "remarks required!", | |||||
})} | })} | ||||
error={Boolean(errors.remarks)} | error={Boolean(errors.remarks)} | ||||
helperText={errors.remarks?.message} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<TextField | <TextField | ||||
label={t("netWeight")} | label={t("netWeight")} | ||||
type="number" | |||||
fullWidth | fullWidth | ||||
inputProps={NumberInputProps} | |||||
{...register("netWeight", { | {...register("netWeight", { | ||||
valueAsNumber: true, | |||||
min: 0, | |||||
required: "netWeight required!", | required: "netWeight required!", | ||||
})} | })} | ||||
error={Boolean(errors.netWeight)} | error={Boolean(errors.netWeight)} | ||||
helperText={errors.netWeight?.message} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<InputDataGrid<CreateMaterialInputs, EntryError> | |||||
<InputDataGrid<CreateProductInputs, EntryError> | |||||
_formKey={"type"} | _formKey={"type"} | ||||
columns={typeColumns} | columns={typeColumns} | ||||
validateRow={validationTest} | validateRow={validationTest} | ||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<InputDataGrid<CreateMaterialInputs, EntryError> | |||||
<InputDataGrid<CreateProductInputs, EntryError> | |||||
_formKey={"uom"} | _formKey={"uom"} | ||||
columns={uomColumns} | columns={uomColumns} | ||||
validateRow={validationTest} | validateRow={validationTest} | ||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={12}> | |||||
<InputDataGrid<CreateProductInputs, EntryError> | |||||
_formKey={"weightUnit"} | |||||
columns={weightUnitColumns} | |||||
validateRow={validationTest} | |||||
/> | |||||
</Grid> | |||||
</Grid> | </Grid> | ||||
</Box> | </Box> | ||||
</CardContent> | </CardContent> | ||||
</Card> | </Card> | ||||
); | ); | ||||
}; | }; | ||||
export default MaterialDetails; | |||||
export default ProductDetails; |
@@ -0,0 +1 @@ | |||||
export { default } from "./CreateProductMaterialWrapper"; |
@@ -107,34 +107,31 @@ function InputDataGrid<T, E>({ | |||||
[apiRef, rowModesModel] | [apiRef, rowModesModel] | ||||
); | ); | ||||
console.log("asdasdasd") | |||||
const processRowUpdate = useCallback( | 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 | // validation here | ||||
// const errors = validateRow(newRow); | |||||
const errors = validateRow(newRow); | |||||
console.log(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 { _isNew, _error, ...updatedRow } = newRow; | ||||
const rowToSave = { | const rowToSave = { | ||||
...updatedRow, | ...updatedRow, | ||||
} as TableRow<T, E>; /// test | } as TableRow<T, E>; /// test | ||||
console.log(rowToSave) | 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; | return rowToSave; | ||||
}, | }, | ||||
[validateRow, getRowId] | [validateRow, getRowId] | ||||
@@ -190,7 +187,7 @@ function InputDataGrid<T, E>({ | |||||
field: "actions", | field: "actions", | ||||
type: "actions", | type: "actions", | ||||
headerName: "", | headerName: "", | ||||
width: 100, | |||||
flex: 0.5, | |||||
cellClassName: "actions", | cellClassName: "actions", | ||||
getActions: ({ id }: { id: GridRowId }) => { | getActions: ({ id }: { id: GridRowId }) => { | ||||
const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; | const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit; | ||||
@@ -230,18 +227,10 @@ function InputDataGrid<T, E>({ | |||||
], | ], | ||||
[columns,rowModesModel, handleSave, handleCancel, handleDelete] | [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 | // sync useForm | ||||
useEffect(() => { | useEffect(() => { | ||||
console.log(formKey) | |||||
console.log(rows) | |||||
// console.log(formKey) | |||||
// console.log(rows) | |||||
setValue(formKey, rows); | setValue(formKey, rows); | ||||
}, [formKey, rows]); | }, [formKey, rows]); | ||||
@@ -294,7 +283,7 @@ function InputDataGrid<T, E>({ | |||||
}, | }, | ||||
}} | }} | ||||
disableColumnMenu | disableColumnMenu | ||||
processRowUpdate={processRowUpdate} | |||||
processRowUpdate={processRowUpdate as any} | |||||
// onRowEditStop={handleRowEditStop} | // onRowEditStop={handleRowEditStop} | ||||
rowModesModel={rowModesModel} | rowModesModel={rowModesModel} | ||||
onRowModesModelChange={setRowModesModel} | 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 />, | icon: <RequestQuote />, | ||||
label: "Finished Goods", | |||||
path: "/settings/user", | |||||
label: "Product", | |||||
path: "/settings/product", | |||||
}, | }, | ||||
{ | { | ||||
icon: <RequestQuote />, | 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"; | import React from "react"; | ||||
// Can make this nicer | // Can make this nicer | ||||
export const MaterialSearchLoading: React.FC = () => { | |||||
export const ProductMaterialSearchLoading: React.FC = () => { | |||||
return ( | return ( | ||||
<> | <> | ||||
<Card> | <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"; |