From ef4cfc703d9b674c83af14d0ad292dfb39cafbfd Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Tue, 4 Mar 2025 15:21:58 +0800 Subject: [PATCH] base for useform --- src/app/(main)/material/page.tsx | 22 +-- .../(main)/settings/material/create/page.tsx | 21 +++ src/app/(main)/settings/material/page.tsx | 46 +++++ src/app/(main)/settings/page.tsx | 2 +- src/app/api/settings/material/actions.ts | 19 ++ src/app/api/settings/material/index.ts | 19 ++ src/app/utils/fetchUtil.ts | 26 +++ .../ControlledAutoComplete.tsx | 164 ++++++++++++++++++ .../ControlledAutoComplete/index.ts | 1 + .../CreateMaterial/CreateMaterial.tsx | 88 ++++++++++ .../CreateMaterial/CreateMaterialLoading.tsx | 40 +++++ .../CreateMaterial/CreateMaterialWrapper.tsx | 21 +++ .../CreateMaterial/MaterialDetails.tsx | 67 +++++++ src/components/CreateMaterial/index.ts | 1 + .../MaterialSearch/MaterialSearch.tsx | 94 ++++++++++ .../MaterialSearch/MaterialSearchLoading.tsx | 40 +++++ .../MaterialSearch/MaterialSearchWrapper.tsx | 22 +++ src/components/MaterialSearch/index.ts | 1 + .../NavigationContent/NavigationContent.tsx | 2 +- src/components/Swal/CustomAlerts.js | 18 ++ 20 files changed, 702 insertions(+), 12 deletions(-) create mode 100644 src/app/(main)/settings/material/create/page.tsx create mode 100644 src/app/(main)/settings/material/page.tsx create mode 100644 src/app/api/settings/material/actions.ts create mode 100644 src/app/api/settings/material/index.ts create mode 100644 src/components/ControlledAutoComplete/ControlledAutoComplete.tsx create mode 100644 src/components/ControlledAutoComplete/index.ts create mode 100644 src/components/CreateMaterial/CreateMaterial.tsx create mode 100644 src/components/CreateMaterial/CreateMaterialLoading.tsx create mode 100644 src/components/CreateMaterial/CreateMaterialWrapper.tsx create mode 100644 src/components/CreateMaterial/MaterialDetails.tsx create mode 100644 src/components/CreateMaterial/index.ts create mode 100644 src/components/MaterialSearch/MaterialSearch.tsx create mode 100644 src/components/MaterialSearch/MaterialSearchLoading.tsx create mode 100644 src/components/MaterialSearch/MaterialSearchWrapper.tsx create mode 100644 src/components/MaterialSearch/index.ts diff --git a/src/app/(main)/material/page.tsx b/src/app/(main)/material/page.tsx index a83f0ec..c6a847c 100644 --- a/src/app/(main)/material/page.tsx +++ b/src/app/(main)/material/page.tsx @@ -1,5 +1,5 @@ -import { preloadClaims } from "@/app/api/claims"; -import ClaimSearch from "@/components/ClaimSearch"; + +import { SearchParams } from "@/app/utils/fetchUtil"; import { getServerI18n } from "@/i18n"; import Add from "@mui/icons-material/Add"; import Button from "@mui/material/Button"; @@ -7,16 +7,18 @@ 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: "Claims", + title: "Material Setting", }; -const material: React.FC = async () => { - const { t } = await getServerI18n("claims"); -// preloadClaims(); +type Props = { + +} & SearchParams +const material: React.FC = async ({ searchParams }) => { + const { t } = await getServerI18n("material"); + console.log(searchParams) return ( <> { {t("Create Claim")} - }> - - + {/* }> + + */} ); }; diff --git a/src/app/(main)/settings/material/create/page.tsx b/src/app/(main)/settings/material/create/page.tsx new file mode 100644 index 0000000..505af93 --- /dev/null +++ b/src/app/(main)/settings/material/create/page.tsx @@ -0,0 +1,21 @@ +import { SearchParams } from "@/app/utils/fetchUtil"; +import CreateMaterial from "@/components/CreateMaterial"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import { Typography } from "@mui/material"; + +type Props = {} & SearchParams; + +const materialSetting: React.FC = async ({ searchParams }) => { + const { t } = await getServerI18n("materials"); + console.log(searchParams); + + return ( + <> + {/* {t("Create Material")} */} + + + + + ); +}; +export default materialSetting; diff --git a/src/app/(main)/settings/material/page.tsx b/src/app/(main)/settings/material/page.tsx new file mode 100644 index 0000000..b2549f7 --- /dev/null +++ b/src/app/(main)/settings/material/page.tsx @@ -0,0 +1,46 @@ +import MaterialSearch from "@/components/MaterialSearch"; +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: "Claims", +}; + +const materialSetting: React.FC = async () => { + const { t } = await getServerI18n("material"); +// preloadClaims(); + + return ( + <> + + + {t("Material")} + + + + }> + + + + ); +}; + +export default materialSetting; diff --git a/src/app/(main)/settings/page.tsx b/src/app/(main)/settings/page.tsx index c3b9620..fbb7d5e 100644 --- a/src/app/(main)/settings/page.tsx +++ b/src/app/(main)/settings/page.tsx @@ -5,7 +5,7 @@ export const metadata: Metadata = { }; const Settings: React.FC = async () => { - return "Settings"; + return null; }; export default Settings; diff --git a/src/app/api/settings/material/actions.ts b/src/app/api/settings/material/actions.ts new file mode 100644 index 0000000..c0b7329 --- /dev/null +++ b/src/app/api/settings/material/actions.ts @@ -0,0 +1,19 @@ +"use server"; +import { ServerFetchError, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; +import { revalidateTag } from "next/cache"; +import { BASE_API_URL } from "@/config/api"; + +export type CreateMaterialInputs = { + name: string; +} + +export const saveMaterial = async (data: CreateMaterialInputs) => { + // try { + const materials = await serverFetchJson(`${BASE_API_URL}/materials/save`, { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }); + revalidateTag("materials"); + return materials + }; diff --git a/src/app/api/settings/material/index.ts b/src/app/api/settings/material/index.ts new file mode 100644 index 0000000..b568198 --- /dev/null +++ b/src/app/api/settings/material/index.ts @@ -0,0 +1,19 @@ +import { cache } from "react"; +import "server-only"; +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; + +export type MaterialResult = { + id: number; + code: string; + name: string; + description: string; + action: any | undefined; +} + + +export const fetchMaterials = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/materials`, { + next: { tags: ["materials"] }, + }); + }); \ No newline at end of file diff --git a/src/app/utils/fetchUtil.ts b/src/app/utils/fetchUtil.ts index 40f6669..cfc352c 100644 --- a/src/app/utils/fetchUtil.ts +++ b/src/app/utils/fetchUtil.ts @@ -7,6 +7,32 @@ export type SearchParams = { searchParams: { [key: string]: string | string[] | undefined }; } +export class ServerFetchError extends Error { + public readonly response: Response | undefined; + constructor(message?: string, response?: Response) { + super(message); + this.response = response; + + Object.setPrototypeOf(this, ServerFetchError.prototype); + } +} + +export async function serverFetchWithNoContent(...args: FetchParams) { + const response = await serverFetch(...args); + + if (response.ok) { + return response.status; // 204 No Content, e.g. for delete data + } else { + switch (response.status) { + case 401: + signOutUser(); + default: + console.error(await response.text()); + throw Error("Something went wrong fetching data in server."); + } + } +} + export const serverFetch: typeof fetch = async (input, init) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const session = await getServerSession(authOptions); diff --git a/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx b/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx new file mode 100644 index 0000000..c2a2044 --- /dev/null +++ b/src/components/ControlledAutoComplete/ControlledAutoComplete.tsx @@ -0,0 +1,164 @@ +"use client"; + +import { + Autocomplete, + MenuItem, + TextField, + Checkbox, + Chip, +} from "@mui/material"; +import { + Controller, + FieldValues, + Path, + Control, + RegisterOptions, +} from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank"; +import CheckBoxIcon from "@mui/icons-material/CheckBox"; + +const icon = ; +const checkedIcon = ; +// label -> e.g. code - name -> 001 - WL +// name -> WL +interface Props< + T extends { id?: number | string | null; label?: string; name?: string }, + TField extends FieldValues, +> { + control: Control; + options: T[]; + name: Path; // register name + label?: string; // display label + noOptionsText?: string; + isMultiple?: boolean; + rules?: RegisterOptions; + disabled?: boolean; +} + +function ControlledAutoComplete< + T extends { id?: number | string; label?: string; name?: string }, + TField extends FieldValues, +>(props: Props) { + const { t } = useTranslation(); + const { + control, + options, + name, + label, + noOptionsText, + isMultiple, + rules, + disabled, + } = props; + + // set default value if value is null + if (!Boolean(isMultiple) && !Boolean(control._formValues[name])) { + control._formValues[name] = options[0]?.id ?? undefined; + } else if (Boolean(isMultiple) && !Boolean(control._formValues[name])) { + control._formValues[name] = []; + } + + return ( + { + return isMultiple ? ( + { + return field.value?.includes(option.id); + })} + options={options} + getOptionLabel={(option) => option.label ?? option.name!} + isOptionEqualToValue={(option, value) => option.id === value.id} + renderOption={(params, option, { selected }) => { + return ( +
  • + + {option.label ?? option.name} +
  • + ); + }} + renderTags={(tagValue, getTagProps) => { + return tagValue.map((option, index) => ( + + )); + }} + onChange={(event, value) => { + field.onChange(value?.map((v) => v.id)); + }} + onBlur={field.onBlur} + renderInput={(params) => ( + + )} + /> + ) : ( + option.id === field.value) ?? options[0] + } + options={options} + getOptionLabel={(option) => option.label ?? option.name!} + isOptionEqualToValue={(option, value) => option?.id === value?.id} + renderOption={(params, option) => { + return ( + + {option.label ?? option.name} + + ); + }} + renderTags={(tagValue, getTagProps) => { + return tagValue.map((option, index) => ( + + )); + }} + onChange={(event, value) => { + field.onChange(value?.id ?? null); + }} + onBlur={field.onBlur} + renderInput={(params) => ( + + )} + /> + ); + }} + /> + ); +} + +export default ControlledAutoComplete; diff --git a/src/components/ControlledAutoComplete/index.ts b/src/components/ControlledAutoComplete/index.ts new file mode 100644 index 0000000..7e12f58 --- /dev/null +++ b/src/components/ControlledAutoComplete/index.ts @@ -0,0 +1 @@ +export { default } from "./ControlledAutoComplete"; \ No newline at end of file diff --git a/src/components/CreateMaterial/CreateMaterial.tsx b/src/components/CreateMaterial/CreateMaterial.tsx new file mode 100644 index 0000000..8cc501a --- /dev/null +++ b/src/components/CreateMaterial/CreateMaterial.tsx @@ -0,0 +1,88 @@ +"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 = ({ isEditMode }) => { + const [serverError, setServerError] = useState(""); + const [tabIndex, setTabIndex] = useState(0); + const { t } = useTranslation(); + const router = useRouter(); + + const formProps = useForm({ + defaultValues: {}, + }); + + const handleCancel = () => { + router.replace("/materials"); + }; + const onSubmit = useCallback>( + async (data, event) => { + try { + console.log(data) + } catch (e) { + + } + }, + [router, t] + ); + const onSubmitError = useCallback>( + (errors) => {}, + [] + ); + const errors = formProps.formState.errors; + + return ( + <> + + + + + {isEditMode ? t("Edit Material") : t("Create Material")} + + + + + + + + + + + ); +}; +export default CreateStaff; diff --git a/src/components/CreateMaterial/CreateMaterialLoading.tsx b/src/components/CreateMaterial/CreateMaterialLoading.tsx new file mode 100644 index 0000000..159a97d --- /dev/null +++ b/src/components/CreateMaterial/CreateMaterialLoading.tsx @@ -0,0 +1,40 @@ +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const CreateMaterialLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + CreateMaterial + + + + + + + + + + + ); +}; + +export default CreateMaterialLoading; diff --git a/src/components/CreateMaterial/CreateMaterialWrapper.tsx b/src/components/CreateMaterial/CreateMaterialWrapper.tsx new file mode 100644 index 0000000..280d0ba --- /dev/null +++ b/src/components/CreateMaterial/CreateMaterialWrapper.tsx @@ -0,0 +1,21 @@ +import CreateStaff from "./CreateMaterial"; +import CreateMaterialLoading from "./CreateMaterialLoading"; + +interface SubComponents { + Loading: typeof CreateMaterialLoading; + } + +type CreateMaterialProps = { + isEditMode?: false; +}; + +type Props = CreateMaterialProps + +const CreateMaterialWrapper: React.FC & SubComponents = async (props) => { + console.log(props) + + return +} +CreateMaterialWrapper.Loading = CreateMaterialLoading; + +export default CreateMaterialWrapper \ No newline at end of file diff --git a/src/components/CreateMaterial/MaterialDetails.tsx b/src/components/CreateMaterial/MaterialDetails.tsx new file mode 100644 index 0000000..5be0a5d --- /dev/null +++ b/src/components/CreateMaterial/MaterialDetails.tsx @@ -0,0 +1,67 @@ +"use client"; +import { CreateMaterialInputs } from "@/app/api/settings/material/actions"; +import { Box, Card, CardContent, Grid, Stack, TextField, Typography } from "@mui/material"; +import { useFormContext } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import ControlledAutoComplete from "../ControlledAutoComplete"; + +type Props = { + isEditMode: boolean; +}; + +const MaterialDetails: React.FC = ({ + isEditMode, +}) => { + const { + t, + i18n: { language }, + } = useTranslation(); + + const { + register, + formState: { errors, defaultValues, touchedFields }, + watch, + control, + setValue, + getValues, + reset, + resetField, + setError, + clearErrors + } = useFormContext(); + + return ( + + + + + {t("Material Details")} + + + + + + + + + + + + + ); +}; +export default MaterialDetails; diff --git a/src/components/CreateMaterial/index.ts b/src/components/CreateMaterial/index.ts new file mode 100644 index 0000000..efb2111 --- /dev/null +++ b/src/components/CreateMaterial/index.ts @@ -0,0 +1 @@ +export { default } from "./CreateMaterialWrapper"; diff --git a/src/components/MaterialSearch/MaterialSearch.tsx b/src/components/MaterialSearch/MaterialSearch.tsx new file mode 100644 index 0000000..b67c98d --- /dev/null +++ b/src/components/MaterialSearch/MaterialSearch.tsx @@ -0,0 +1,94 @@ +"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"; + +type Props = { + materials: MaterialResult[] +} +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const MaterialSearch: React.FC = ({ + materials + }) => { +const [filteredMaterials, setFilteredMaterials] = useState([]) +const { t } = useTranslation("materials"); +const router = useRouter(); + + const searchCriteria: Criterion[] = 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[]>(() => [ + { + name: "id", + label: t("Details"), + onClick: onMaterialClick, + buttonIcon: , + }, + { + name: "code", + label: t("Code"), + }, + { + name: "name", + label: t("Name"), + }, + { + name: "action", + label: t(""), + onClick: onDeleteClick, + }, + + ], [filteredMaterials]) + + const onReset = useCallback(() => { + setFilteredMaterials(materials); + }, [materials]); + + return ( + <> + { + setFilteredMaterials( + materials.filter( + (mat) => + mat.code.toLowerCase().includes(query.code.toLowerCase()) && + mat.name.toLowerCase().includes(query.name.toLowerCase()) + ), + ); + }} + onReset={onReset} + /> + + items={filteredMaterials} + columns={columns} + /> + +) + } + + export default MaterialSearch \ No newline at end of file diff --git a/src/components/MaterialSearch/MaterialSearchLoading.tsx b/src/components/MaterialSearch/MaterialSearchLoading.tsx new file mode 100644 index 0000000..127a557 --- /dev/null +++ b/src/components/MaterialSearch/MaterialSearchLoading.tsx @@ -0,0 +1,40 @@ +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Skeleton from "@mui/material/Skeleton"; +import Stack from "@mui/material/Stack"; +import React from "react"; + +// Can make this nicer +export const MaterialSearchLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default MaterialSearchLoading; diff --git a/src/components/MaterialSearch/MaterialSearchWrapper.tsx b/src/components/MaterialSearch/MaterialSearchWrapper.tsx new file mode 100644 index 0000000..cfb62b3 --- /dev/null +++ b/src/components/MaterialSearch/MaterialSearchWrapper.tsx @@ -0,0 +1,22 @@ +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: MaterialResult[] = [] + // const materials = await fetchMaterials(); + + return ( + + ) +} + +MaterialSearchWrapper.Loading = MaterialSearchLoading; + +export default MaterialSearchWrapper; \ No newline at end of file diff --git a/src/components/MaterialSearch/index.ts b/src/components/MaterialSearch/index.ts new file mode 100644 index 0000000..b12f72b --- /dev/null +++ b/src/components/MaterialSearch/index.ts @@ -0,0 +1 @@ +export { default } from "./MaterialSearchWrapper"; diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 45790f0..3ba835a 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -184,7 +184,7 @@ const NavigationContent: React.FC = () => { { icon: , label: "Material", - path: "/settings/user", + path: "/settings/material", }, { icon: , diff --git a/src/components/Swal/CustomAlerts.js b/src/components/Swal/CustomAlerts.js index 62c853d..9839ffe 100644 --- a/src/components/Swal/CustomAlerts.js +++ b/src/components/Swal/CustomAlerts.js @@ -16,6 +16,24 @@ export const msg = (text) => { title: text, }); }; +export const deleteDialog = async (confirmAction, t) => { + // const { t } = useTranslation("common") + const result = await Swal.fire({ + icon: "question", + title: t("Do you want to delete?"), + cancelButtonText: t("Cancel"), + confirmButtonText: t("Delete"), + showCancelButton: true, + showConfirmButton: true, + customClass: { + container: "swal-container-class", // Add a custom class to the Swal.fire container element + popup: "swal-popup-class", // Add a custom class to the Swal.fire popup element + }, + }); + if (result.isConfirmed) { + confirmAction(); + } +} export const popup = (text) => { Swal.fire(text);