Bladeren bron

base for useform

feature/axios_provider
MSI\derek 5 maanden geleden
bovenliggende
commit
ef4cfc703d
20 gewijzigde bestanden met toevoegingen van 702 en 12 verwijderingen
  1. +12
    -10
      src/app/(main)/material/page.tsx
  2. +21
    -0
      src/app/(main)/settings/material/create/page.tsx
  3. +46
    -0
      src/app/(main)/settings/material/page.tsx
  4. +1
    -1
      src/app/(main)/settings/page.tsx
  5. +19
    -0
      src/app/api/settings/material/actions.ts
  6. +19
    -0
      src/app/api/settings/material/index.ts
  7. +26
    -0
      src/app/utils/fetchUtil.ts
  8. +164
    -0
      src/components/ControlledAutoComplete/ControlledAutoComplete.tsx
  9. +1
    -0
      src/components/ControlledAutoComplete/index.ts
  10. +88
    -0
      src/components/CreateMaterial/CreateMaterial.tsx
  11. +40
    -0
      src/components/CreateMaterial/CreateMaterialLoading.tsx
  12. +21
    -0
      src/components/CreateMaterial/CreateMaterialWrapper.tsx
  13. +67
    -0
      src/components/CreateMaterial/MaterialDetails.tsx
  14. +1
    -0
      src/components/CreateMaterial/index.ts
  15. +94
    -0
      src/components/MaterialSearch/MaterialSearch.tsx
  16. +40
    -0
      src/components/MaterialSearch/MaterialSearchLoading.tsx
  17. +22
    -0
      src/components/MaterialSearch/MaterialSearchWrapper.tsx
  18. +1
    -0
      src/components/MaterialSearch/index.ts
  19. +1
    -1
      src/components/NavigationContent/NavigationContent.tsx
  20. +18
    -0
      src/components/Swal/CustomAlerts.js

+ 12
- 10
src/app/(main)/material/page.tsx Bestand weergeven

@@ -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<Props> = async ({ searchParams }) => {
const { t } = await getServerI18n("material");
console.log(searchParams)
return (
<>
<Stack
@@ -37,9 +39,9 @@ const material: React.FC = async () => {
{t("Create Claim")}
</Button>
</Stack>
<Suspense fallback={<ClaimSearch.Loading />}>
<ClaimSearch />
</Suspense>
{/* <Suspense fallback={<MaterialSearch.Loading />}>
<MaterialSearch />
</Suspense> */}
</>
);
};


+ 21
- 0
src/app/(main)/settings/material/create/page.tsx Bestand weergeven

@@ -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<Props> = async ({ searchParams }) => {
const { t } = await getServerI18n("materials");
console.log(searchParams);

return (
<>
{/* <Typography variant="h4">{t("Create Material")}</Typography> */}
<I18nProvider namespaces={["materials"]}>
<CreateMaterial isEditMode={false} />
</I18nProvider>
</>
);
};
export default materialSetting;

+ 46
- 0
src/app/(main)/settings/material/page.tsx Bestand weergeven

@@ -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 (
<>
<Stack
direction="row"
justifyContent="space-between"
flexWrap="wrap"
rowGap={2}
>
<Typography variant="h4" marginInlineEnd={2}>
{t("Material")}
</Typography>
<Button
variant="contained"
startIcon={<Add />}
LinkComponent={Link}
href="material/create"
>
{t("Create material")}
</Button>
</Stack>
<Suspense fallback={<MaterialSearch.Loading />}>
<MaterialSearch />
</Suspense>
</>
);
};

export default materialSetting;

+ 1
- 1
src/app/(main)/settings/page.tsx Bestand weergeven

@@ -5,7 +5,7 @@ export const metadata: Metadata = {
};

const Settings: React.FC = async () => {
return "Settings";
return null;
};

export default Settings;

+ 19
- 0
src/app/api/settings/material/actions.ts Bestand weergeven

@@ -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
};

+ 19
- 0
src/app/api/settings/material/index.ts Bestand weergeven

@@ -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<MaterialResult[]>(`${BASE_API_URL}/materials`, {
next: { tags: ["materials"] },
});
});

+ 26
- 0
src/app/utils/fetchUtil.ts Bestand weergeven

@@ -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<any, SessionWithTokens>(authOptions);


+ 164
- 0
src/components/ControlledAutoComplete/ControlledAutoComplete.tsx Bestand weergeven

@@ -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 = <CheckBoxOutlineBlankIcon fontSize="medium" />;
const checkedIcon = <CheckBoxIcon fontSize="medium" />;
// 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<TField>;
options: T[];
name: Path<TField>; // register name
label?: string; // display label
noOptionsText?: string;
isMultiple?: boolean;
rules?: RegisterOptions<FieldValues>;
disabled?: boolean;
}

function ControlledAutoComplete<
T extends { id?: number | string; label?: string; name?: string },
TField extends FieldValues,
>(props: Props<T, TField>) {
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 (
<Controller
name={name}
control={control}
rules={rules}
render={({ field, fieldState, formState }) => {
return isMultiple ? (
<Autocomplete
multiple
disableClearable
disableCloseOnSelect
// disablePortal
disabled={disabled}
noOptionsText={noOptionsText ?? t("No Options")}
value={options.filter((option) => {
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 (
<li {...params} key={option?.id}>
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
checked={selected}
style={{ marginRight: 8 }}
/>
{option.label ?? option.name}
</li>
);
}}
renderTags={(tagValue, getTagProps) => {
return tagValue.map((option, index) => (
<Chip
{...getTagProps({ index })}
key={option?.id}
label={option.label ?? option.name}
/>
));
}}
onChange={(event, value) => {
field.onChange(value?.map((v) => v.id));
}}
onBlur={field.onBlur}
renderInput={(params) => (
<TextField
{...params}
error={Boolean(formState.errors[name])}
variant="outlined"
label={label}
/>
)}
/>
) : (
<Autocomplete
disableClearable
// disablePortal
disabled={disabled}
noOptionsText={noOptionsText ?? t("No Options")}
value={
options.find((option) => 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 (
<MenuItem {...params} key={option?.id} value={option.id}>
{option.label ?? option.name}
</MenuItem>
);
}}
renderTags={(tagValue, getTagProps) => {
return tagValue.map((option, index) => (
<Chip
{...getTagProps({ index })}
key={option?.id}
label={option.label ?? option.name}
/>
));
}}
onChange={(event, value) => {
field.onChange(value?.id ?? null);
}}
onBlur={field.onBlur}
renderInput={(params) => (
<TextField
{...params}
error={Boolean(formState.errors[name])}
variant="outlined"
label={label}
/>
)}
/>
);
}}
/>
);
}

export default ControlledAutoComplete;

+ 1
- 0
src/components/ControlledAutoComplete/index.ts Bestand weergeven

@@ -0,0 +1 @@
export { default } from "./ControlledAutoComplete";

+ 88
- 0
src/components/CreateMaterial/CreateMaterial.tsx Bestand weergeven

@@ -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<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;

+ 40
- 0
src/components/CreateMaterial/CreateMaterialLoading.tsx Bestand weergeven

@@ -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 (
<>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton
variant="rounded"
height={50}
width={100}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
</CardContent>
</Card>
<Card>CreateMaterial
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
</Stack>
</CardContent>
</Card>
</>
);
};

export default CreateMaterialLoading;

+ 21
- 0
src/components/CreateMaterial/CreateMaterialWrapper.tsx Bestand weergeven

@@ -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<Props> & SubComponents = async (props) => {
console.log(props)

return <CreateStaff isEditMode={Boolean(props.isEditMode)}/>
}
CreateMaterialWrapper.Loading = CreateMaterialLoading;

export default CreateMaterialWrapper

+ 67
- 0
src/components/CreateMaterial/MaterialDetails.tsx Bestand weergeven

@@ -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<Props> = ({
isEditMode,
}) => {
const {
t,
i18n: { language },
} = useTranslation();

const {
register,
formState: { errors, defaultValues, touchedFields },
watch,
control,
setValue,
getValues,
reset,
resetField,
setError,
clearErrors
} = useFormContext<CreateMaterialInputs>();
return (
<Card sx={{ display: "block" }}>
<CardContent component={Stack} spacing={4}>
<Box>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("Material Details")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<ControlledAutoComplete
control={control}
options={[]}
name="name"
label={t("name")}
noOptionsText={t("No Option")}
// disabled={isEditMode}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Name")}
fullWidth
{...register("name", {
required: "Project name required!",
})}
error={Boolean(errors.name)}
/>
</Grid>
</Grid>
</Box>
</CardContent>
</Card>
);
};
export default MaterialDetails;

+ 1
- 0
src/components/CreateMaterial/index.ts Bestand weergeven

@@ -0,0 +1 @@
export { default } from "./CreateMaterialWrapper";

+ 94
- 0
src/components/MaterialSearch/MaterialSearch.tsx Bestand weergeven

@@ -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<Omit<MaterialResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const MaterialSearch: React.FC<Props> = ({
materials
}) => {
const [filteredMaterials, setFilteredMaterials] = useState<MaterialResult[]>([])
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(""),
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

+ 40
- 0
src/components/MaterialSearch/MaterialSearchLoading.tsx Bestand weergeven

@@ -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 (
<>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton variant="rounded" height={60} />
<Skeleton
variant="rounded"
height={50}
width={100}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
</CardContent>
</Card>
<Card>
<CardContent>
<Stack spacing={2}>
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
<Skeleton variant="rounded" height={40} />
</Stack>
</CardContent>
</Card>
</>
);
};

export default MaterialSearchLoading;

+ 22
- 0
src/components/MaterialSearch/MaterialSearchWrapper.tsx Bestand weergeven

@@ -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 (
<MaterialSearch
materials={materials}
/>
)
}

MaterialSearchWrapper.Loading = MaterialSearchLoading;

export default MaterialSearchWrapper;

+ 1
- 0
src/components/MaterialSearch/index.ts Bestand weergeven

@@ -0,0 +1 @@
export { default } from "./MaterialSearchWrapper";

+ 1
- 1
src/components/NavigationContent/NavigationContent.tsx Bestand weergeven

@@ -184,7 +184,7 @@ const NavigationContent: React.FC = () => {
{
icon: <RequestQuote />,
label: "Material",
path: "/settings/user",
path: "/settings/material",
},
{
icon: <RequestQuote />,


+ 18
- 0
src/components/Swal/CustomAlerts.js Bestand weergeven

@@ -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);


Laden…
Annuleren
Opslaan