From de1802c5972911db8bc6b8fac8223251fbbd5269 Mon Sep 17 00:00:00 2001 From: Ardeman Date: Tue, 25 Mar 2025 00:56:14 +0800 Subject: [PATCH] feat: implement staff management features including staff creation and dashboard layout --- app/apis/admin/create-staffs.ts | 28 ++++ app/layouts/admin/dialog-profile.tsx | 8 +- app/pages/dashboard-staffs/index.tsx | 89 ++++++++++++ app/pages/form-staff/index.tsx | 134 ++++++++++++++++++ ...dmin.lg-admin._dashboard.staffs._index.tsx | 44 ++++++ ...dmin.lg-admin._dashboard.staffs.create.tsx | 4 + app/routes/actions.admin.staffs.create.ts | 64 +++++++++ 7 files changed, 367 insertions(+), 4 deletions(-) create mode 100644 app/apis/admin/create-staffs.ts create mode 100644 app/pages/dashboard-staffs/index.tsx create mode 100644 app/pages/form-staff/index.tsx create mode 100644 app/routes/_admin.lg-admin._dashboard.staffs._index.tsx create mode 100644 app/routes/_admin.lg-admin._dashboard.staffs.create.tsx create mode 100644 app/routes/actions.admin.staffs.create.ts diff --git a/app/apis/admin/create-staffs.ts b/app/apis/admin/create-staffs.ts new file mode 100644 index 0000000..22327ac --- /dev/null +++ b/app/apis/admin/create-staffs.ts @@ -0,0 +1,28 @@ +import { z } from 'zod' + +import { HttpServer, type THttpServer } from '~/libs/http-server' +import type { TStaffSchema } from '~/pages/form-staff' + +const createStaffResponseSchema = z.object({ + data: z.object({ + Message: z.string(), + }), +}) + +type TParameter = { + payload: TStaffSchema +} & THttpServer + +export const createStaffsRequest = async (parameters: TParameter) => { + const { payload, ...restParameters } = parameters + try { + const { data } = await HttpServer(restParameters).post( + '/api/staff/register', + payload, + ) + return createStaffResponseSchema.parse(data) + } catch (error) { + // eslint-disable-next-line unicorn/no-useless-promise-resolve-reject + return Promise.reject(error) + } +} diff --git a/app/layouts/admin/dialog-profile.tsx b/app/layouts/admin/dialog-profile.tsx index 5cb9024..57a3240 100644 --- a/app/layouts/admin/dialog-profile.tsx +++ b/app/layouts/admin/dialog-profile.tsx @@ -21,7 +21,7 @@ export const profileSchema = z.object({ name: z.string().min(1, 'Name is required'), email: z.string().email('Email is invalid'), profile_picture: z.string().url({ - message: 'URL must be valid', + message: 'Gambar profil must be valid', }), }) @@ -93,7 +93,7 @@ export const DialogProfile = () => { { + + + {/* */} + + ) +} diff --git a/app/pages/form-staff/index.tsx b/app/pages/form-staff/index.tsx new file mode 100644 index 0000000..b0ddbfe --- /dev/null +++ b/app/pages/form-staff/index.tsx @@ -0,0 +1,134 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { useEffect } from 'react' +import toast from 'react-hot-toast' +import { useFetcher, useNavigate } from 'react-router' +import { RemixFormProvider, useRemixForm } from 'remix-hook-form' +import { z } from 'zod' + +import { Button } from '~/components/ui/button' +import { Input } from '~/components/ui/input' +import { InputFile } from '~/components/ui/input-file' +import { TitleDashboard } from '~/components/ui/title-dashboard' + +export const staffSchema = z + .object({ + profile_picture: z + .string() + .url({ + message: 'Profile picture must be a valid URL', + }) + .or(z.literal('')), + name: z.string().min(1, { + message: 'Nama staff is required', + }), + password: z.string().min(6, 'Kata sandi minimal 6 karakter'), + rePassword: z.string().min(6, 'Kata sandi minimal 6 karakter'), + email: z.string().email('Email tidak valid'), + }) + .refine((field) => field.password === field.rePassword, { + message: 'Kata sandi tidak sama', + path: ['rePassword'], + }) +export type TStaffSchema = z.infer + +export const FormStaffPage = () => { + const fetcher = useFetcher() + const navigate = useNavigate() + const formMethods = useRemixForm({ + mode: 'onSubmit', + fetcher, + resolver: zodResolver(staffSchema), + }) + + const { handleSubmit } = formMethods + + useEffect(() => { + if (!fetcher.data?.success && fetcher.data?.message) { + toast.error(fetcher.data.message) + } + + if (fetcher.data?.success) { + toast.success(`Staff berhasil dibuat!`) + navigate('/lg-admin/staffs') + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fetcher.data]) + + return ( +
+ +
+ + +
+ + + +
+
+ + + +
+
+
+
+
+ ) +} diff --git a/app/routes/_admin.lg-admin._dashboard.staffs._index.tsx b/app/routes/_admin.lg-admin._dashboard.staffs._index.tsx new file mode 100644 index 0000000..20a6e6c --- /dev/null +++ b/app/routes/_admin.lg-admin._dashboard.staffs._index.tsx @@ -0,0 +1,44 @@ +import { isRouteErrorResponse } from 'react-router' + +import { StaffsPage } from '~/pages/dashboard-staffs' + +import type { Route } from './+types/_admin.lg-admin._dashboard.staffs._index' + +// export const loader = async ({ request }: Route.LoaderArgs) => { +// const { staffToken: accessToken } = await handleCookie(request) +// const { data: usersData } = await getUsers({ accessToken }) + +// return { usersData } +// } + +export const ErrorBoundary = ({ error }: Route.ErrorBoundaryProps) => { + let message = 'Oops!' + let details = 'An unexpected error occurred.' + let stack: string | undefined + + if (isRouteErrorResponse(error)) { + message = error.status === 404 ? '404' : 'Error' + details = + error.status === 404 + ? 'The requested page could not be found.' + : error.statusText || details + } else if (import.meta.env.DEV && error && error instanceof Error) { + details = error.message + stack = error.stack + } + + return ( +
+

{message}

+

{details}

+ {stack && ( +
+          {stack}
+        
+ )} +
+ ) +} + +const DashboardStaffsLayout = () => +export default DashboardStaffsLayout diff --git a/app/routes/_admin.lg-admin._dashboard.staffs.create.tsx b/app/routes/_admin.lg-admin._dashboard.staffs.create.tsx new file mode 100644 index 0000000..ab33fe2 --- /dev/null +++ b/app/routes/_admin.lg-admin._dashboard.staffs.create.tsx @@ -0,0 +1,4 @@ +import { FormStaffPage } from '~/pages/form-staff' + +const DashboardStaffsCreateLayout = () => +export default DashboardStaffsCreateLayout diff --git a/app/routes/actions.admin.staffs.create.ts b/app/routes/actions.admin.staffs.create.ts new file mode 100644 index 0000000..776be62 --- /dev/null +++ b/app/routes/actions.admin.staffs.create.ts @@ -0,0 +1,64 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { data } from 'react-router' +import { getValidatedFormData } from 'remix-hook-form' +import { XiorError } from 'xior' + +import { createStaffsRequest } from '~/apis/admin/create-staffs' +import { handleCookie } from '~/libs/cookies' +import { staffSchema, type TStaffSchema } from '~/pages/form-staff' + +import type { Route } from './+types/actions.admin.staffs.create' + +export const action = async ({ request }: Route.ActionArgs) => { + const { staffToken: accessToken } = await handleCookie(request) + try { + const { + errors, + data: payload, + receivedValues: defaultValues, + } = await getValidatedFormData( + request, + zodResolver(staffSchema), + false, + ) + + if (errors) { + return data({ success: false, errors, defaultValues }, { status: 400 }) + } + + const { data: staffData } = await createStaffsRequest({ + accessToken, + payload, + }) + + return data( + { + success: true, + staffData, + }, + { + status: 200, + statusText: 'OK', + }, + ) + } catch (error) { + if (error instanceof XiorError) { + return data( + { + success: false, + message: error?.response?.data?.error?.message || error.message, + }, + { + status: error?.response?.status || 500, + }, + ) + } + return data( + { + success: false, + message: 'Internal server error', + }, + { status: 500 }, + ) + } +}