Skip to content

Commit

Permalink
Feat: Role for employee
Browse files Browse the repository at this point in the history
  • Loading branch information
duchuy124 committed Jun 15, 2024
1 parent 38b3f80 commit e302931
Show file tree
Hide file tree
Showing 12 changed files with 120 additions and 55 deletions.
41 changes: 33 additions & 8 deletions src/app/dashboard/employees/_components/employee-form.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
"use client";
import { Employee, employeeSchema } from "@/types/employee";
import { Employee, EmployeeFormSchema, employeeSchema } from "@/types/employee";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import ModalCard from "@/components/form/modal-card";
import ReactHookForm from "@/components/form/react-hook-form";
import { useCreateEmployee, useEditEmployee } from "@/services/employee-service";
import DynamicFormFields from "@/components/form/dynamic-form-fields";
import { PasswordInput } from "@/components/ui/toggle-able-password";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { useGetRoles } from "@/services/role-service";
import BlurryLoader from "@/components/blurry-loader";

export default function EmployeeForm({ type, initialData = {} }: { type: "Create" | "Edit"; initialData?: any }) {
const { mutate: mutateCreate, isPending: isPendingCreate, isSuccess: isSuccessCreate } = useCreateEmployee();
const { mutate: mutateEdit, isPending: isPendingEdit, isSuccess: isSuccessEdit } = useEditEmployee();
const { data: roles, isFetching: isFetchingRole } = useGetRoles();

const form = useForm<Employee>({
resolver: zodResolver(employeeSchema),
resolver: zodResolver(EmployeeFormSchema),
defaultValues: initialData,
});

Expand All @@ -34,12 +38,33 @@ export default function EmployeeForm({ type, initialData = {} }: { type: "Create
onClose={isSuccessCreate || isSuccessEdit}
>
<ReactHookForm formId={`${type}-employee-form`} form={form} onSubmit={handleSubmit} schema={employeeSchema}>
<DynamicFormFields
schema={employeeSchema}
<DynamicFormFields schema={employeeSchema} control={form.control} />
<FormField
control={form.control}
overrides={{
password: <PasswordInput name="password" />,
}}
name="role_id"
render={({ field }) => (
<FormItem>
<FormLabel className="capitalize">Role</FormLabel>
<Select onValueChange={field.onChange}>
<FormControl>
<div className="relative">
<BlurryLoader shouldShow={isFetchingRole} dimensions="w-5 h-5" />
<SelectTrigger>
<SelectValue placeholder="Select Role" />
</SelectTrigger>
</div>
</FormControl>
<SelectContent>
{roles?.map((role, index) => (
<SelectItem key={index} value={role.id.toString()}>
{role.display_name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</ReactHookForm>
</ModalCard>
Expand Down
26 changes: 26 additions & 0 deletions src/app/dashboard/employees/_components/select-box-roles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { useGetRoles } from "@/services/role-service";
import BlurryLoader from "@/components/blurry-loader";

export default function SelectBoxRoles({ form }) {
const { data: roles, isFetching: isFetchingRole } = useGetRoles();

return (
<Select>
<div className="relative">
<BlurryLoader shouldShow={isFetchingRole} dimensions="w-5 h-5" />
<SelectTrigger>
<SelectValue placeholder="Select Role" />
</SelectTrigger>
</div>
<SelectContent>
{roles?.map((role, index) => (
<SelectItem key={index} value={role.id.toString()}>
{role.display_name}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
4 changes: 2 additions & 2 deletions src/components/blurry-loader.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import LoadingCircle from "@/components/icons/loading-circle";

export default function BlurryLoader({ shouldShow }) {
export default function BlurryLoader({ shouldShow, dimensions = "w-10 h-10" }) {
return (
shouldShow && (
<tr>
<th className="absolute inset-0 z-30 bg-gray-100 bg-opacity-10 backdrop-blur"></th>
<th className="absolute left-1/2 top-1/2 z-40 -translate-x-1/2 -translate-y-1/2 transform">
<LoadingCircle dimensions="w-10 h-10" />
<LoadingCircle dimensions={dimensions} />
</th>
</tr>
)
Expand Down
12 changes: 6 additions & 6 deletions src/components/form/antd-image-upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useSession } from "next-auth/react";
const { Dragger } = Upload;

export default function ImageUpload({ onUpload }: { onUpload: any }) {
const session = useSession();
// const session = useSession();

const handleSubmit = () => {};

Expand All @@ -31,11 +31,11 @@ export default function ImageUpload({ onUpload }: { onUpload: any }) {
name="image"
multiple={true}
action={`${process.env.NEXT_PUBLIC_BE_URL}/api/v1/files/uploadImage`}
headers={
{
Authorization: `Bearer ${session.data?.accessToken}`,
} as AxiosRequestHeaders
}
// headers={
// {
// Authorization: `Bearer ${session.data?.accessToken}`,
// } as AxiosRequestHeaders
// }
onChange={handleOnChange}
onDrop={handleOnDrop}
// beforeUpload={() => false} // return false so that antd doesn't upload the picture right away
Expand Down
26 changes: 4 additions & 22 deletions src/components/form/modal-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,7 @@
import * as React from "react";

import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { useModal } from "@/components/modal/provider";
import { LoadingButton } from "@/components/ui/button-with-loading";
import { UseFormReturn } from "react-hook-form";
Expand All @@ -27,14 +20,7 @@ type ModalCardProps = {
isLoading?: boolean;
};

export default function ModalCard({
form,
formId,
children,
metadata,
onClose,
isLoading,
}: ModalCardProps) {
export default function ModalCard({ form, formId, children, metadata, onClose, isLoading }: ModalCardProps) {
const modal = useModal();

// Hide modal after submitted
Expand All @@ -46,7 +32,7 @@ export default function ModalCard({
}, [onClose]);

return (
<Card className="min-w-[350px]">
<Card className="min-w-[450px]">
<CardHeader>
<CardTitle>{metadata.title}</CardTitle>
<CardDescription>{metadata.description}</CardDescription>
Expand All @@ -56,11 +42,7 @@ export default function ModalCard({
<Button variant="outline" onClick={() => modal?.hide()}>
Cancel
</Button>
<LoadingButton
type="submit"
form={formId}
loading={form?.formState.isSubmitting || isLoading}
>
<LoadingButton type="submit" form={formId} loading={form?.formState.isSubmitting || isLoading}>
{metadata.buttonLabel}
</LoadingButton>
</CardFooter>
Expand Down
2 changes: 1 addition & 1 deletion src/lib/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,4 @@ export const CREATE_EDIT_FIELDS_EXCLUDE = [
"role",
"permission_groups",
];
export const READ_FIELDS_EXCLUDE = ["password", "permission_ids"];
export const READ_FIELDS_EXCLUDE = ["password", "permission_ids", "id"];
2 changes: 1 addition & 1 deletion src/lib/hooks/use-sorting.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from "react";

export default function useSorting(initialField = "id", initialOrder = "DESC") {
export default function useSorting(initialField = "created_at", initialOrder = "DESC") {
const [sorting, setSorting] = useState([{ id: initialField, desc: initialOrder === "DESC" }]);

return {
Expand Down
32 changes: 32 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,35 @@ export const omitFields = (schema, fieldsToOmit) => {
}, {});
return z.object(newShape);
};

export function optional<TSchema extends z.AnyZodObject>(schema: TSchema) {
const entries = Object.entries(schema.shape) as [keyof TSchema["shape"], z.ZodTypeAny][];

const newProps = entries.reduce(
(acc, [key, value]) => {
acc[key] = value.optional();
return acc;
},
{} as {
[key in keyof TSchema["shape"]]: z.ZodOptional<TSchema["shape"][key]>;
},
);

return z.object(newProps);
}

export function makeOptionalPropsNullable<Schema extends z.AnyZodObject>(schema: Schema) {
const entries = Object.entries(schema.shape) as [keyof Schema["shape"], z.ZodTypeAny][];
const newProps = entries.reduce(
(acc, [key, value]) => {
acc[key] = value instanceof z.ZodOptional ? value.unwrap().nullable() : value;
return acc;
},
{} as {
[key in keyof Schema["shape"]]: Schema["shape"][key] extends z.ZodOptional<infer T>
? z.ZodNullable<T>
: Schema["shape"][key];
},
);
return z.object(newProps);
}
3 changes: 2 additions & 1 deletion src/services/role-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,13 @@ const editRole = async (role: Role) => {
};

// Define the hook to get all roles
export const useGetRoles = (initialData: Role[], params = defaultParams) => {
export const useGetRoles = (initialData?: Role[], params = defaultParams) => {
return useQuery({
queryKey: [QUERY_KEY, params],
queryFn: () => fetchRoles(params),
placeholderData: keepPreviousData, // The data from the last successful fetch is available while new data is being requested
initialData: () => {
if (!initialData) return undefined;
// first data will be fetched from server side
const isInitialParams = deepEqual(params, defaultParams);
return isInitialParams ? initialData : undefined;
Expand Down
21 changes: 10 additions & 11 deletions src/types/employee.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
import { z } from "zod";
import dayjs from "dayjs";
import { defaultSchema } from "@/types/default-type";

const roleSchema = z
.object({
id: z.preprocess((x) => "" + x, z.string()),
name: z.string().nullable(),
})
.nullable()
.optional();
import { roleSchema } from "@/types/role";
import { omitFields } from "@/lib/utils";

export const employeeSchema = z
.object({
id: z.preprocess((x) => "" + x, z.string()),
name: z.string(),
username: z.string(),
password: z.string().nullable(),
password: z.string().nullable().optional(),
email: z.string().nullable(),
phone: z.string().nullable().optional(),
sex: z.string().nullable().optional(),
birth_date: z.coerce.date().optional(),
last_login_at: z.coerce.date().optional(),
role_id: z.string().optional(),
role: roleSchema, // nested role object
})
.extend(defaultSchema.shape);
// .omit({ password: true });

export type Employee = z.infer<typeof employeeSchema>;

// Define the fields to omit in the create form
const formExcludedFields = ["created_at", "updated_at", "role"];

// Create a schema for the create form
export const EmployeeFormSchema = omitFields(employeeSchema, formExcludedFields);
2 changes: 1 addition & 1 deletion src/types/request-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type PaginationParams = {
export type DefaultRequestParams = PaginationParams & FilterParams;

export const defaultParams: DefaultRequestParams = {
order_by: "id",
order_by: "created_at",
sort: "DESC",
filter: {},
page: 1,
Expand Down
4 changes: 2 additions & 2 deletions src/types/role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const roleSchema = z
id: z.preprocess((x) => "" + x, z.string()),
name: z.string(),
display_name: z.string(),
description: z.string().optional(),
description: z.string().nullable().optional(),
permission_ids: z.any(),
permission_groups: permissionGroupSchema,
// type: z.objec({ id: 1, value: "success" }, { id: 2, value: "pending" }, { id: 3, value: "failed" }),
Expand All @@ -30,7 +30,7 @@ export const roleSchema = z
export type Role = z.infer<typeof roleSchema>;

// Define the fields to omit in the create form
const createFormExcludedFields = ["created_at", "updated_at"];
const createFormExcludedFields = ["created_at", "updated_at", "role"];

// Create a schema for the create form
export const createRoleFormSchema = omitFields(roleSchema, createFormExcludedFields);

0 comments on commit e302931

Please sign in to comment.