diff --git a/apps/backend/src/data/sidebarMenus.ts b/apps/backend/src/data/sidebarMenus.ts index ec98d7e..85d98d6 100644 --- a/apps/backend/src/data/sidebarMenus.ts +++ b/apps/backend/src/data/sidebarMenus.ts @@ -8,7 +8,7 @@ const sidebarMenus: SidebarMenu[] = [ link: "/dashboard", }, { - label: "Users", + label: "Manajemen Pengguna", icon: { tb: "TbUsers" }, allowedPermissions: ["permissions.read"], link: "/users", diff --git a/apps/backend/src/routes/users/route.ts b/apps/backend/src/routes/users/route.ts index 26a13e1..00f7cd5 100644 --- a/apps/backend/src/routes/users/route.ts +++ b/apps/backend/src/routes/users/route.ts @@ -13,37 +13,20 @@ import requestValidator from "../../utils/requestValidator"; import authInfo from "../../middlewares/authInfo"; import checkPermission from "../../middlewares/checkPermission"; import { respondents } from "../../drizzle/schema/respondents"; -import { notFound } from "../../errors/DashboardError"; +import { forbidden, notFound } from "../../errors/DashboardError"; export const userFormSchema = z.object({ - name: z.string().min(1).max(255), - username: z.string().min(1).max(255), - email: z.string().email().optional().or(z.literal("")), - password: z.string().min(6), - companyName: z.string().min(1).max(255), - position: z.string().min(1).max(255), - workExperience: z.string().min(1).max(255), - address: z.string().min(1), - phoneNumber: z.string().min(1).max(13), + name: z.string().min(1, "Name is required").max(255), + username: z.string().min(1, "Username is required").max(255), + email: z.string().min(1, "Email is required").email().optional().or(z.literal("")), + password: z.string().min(6, "Password is required"), + companyName: z.string().min(1, "Company name is required").max(255), + position: z.string().min(1, "Position is required").max(255), + workExperience: z.string().min(1, "Work experience is required").max(255), + address: z.string().min(1, "Address is required"), + phoneNumber: z.string().min(1, "Phone number is required").max(13), isEnabled: z.string().default("false"), - // roles: z - // .string() - // .refine( - // (data) => { - // console.log(data); - // try { - // const parsed = JSON.parse(data); - // return Array.isArray(parsed); - // } catch { - // return false; - // } - // }, - // { - // message: "Roles must be an array", - // } - // ) - // .optional(), - roles: z.array(z.string()).optional(), + roles: z.array(z.string().min(1, "Role is required")), }); export const userUpdateSchema = userFormSchema.extend({ @@ -75,7 +58,7 @@ const usersRoute = new Hono() .optional() .transform((v) => v?.toLowerCase() === "true"), page: z.coerce.number().int().min(0).default(0), - limit: z.coerce.number().int().min(1).max(1000).default(100), + limit: z.coerce.number().int().min(1).max(1000).default(1), q: z.string().default(""), }) ), @@ -225,9 +208,9 @@ const usersRoute = new Hono() ); if (!queryResult.length) - throw new HTTPException(404, { - message: "The user does not exists", - }); + throw notFound({ + message : "The user does not exists", + }) const roles = queryResult.reduce((prev, curr) => { if (!curr.role) return prev; @@ -244,6 +227,7 @@ const usersRoute = new Hono() return c.json(userData); } ) + //create user .post( "/", @@ -275,15 +259,15 @@ const usersRoute = new Hono() .where(eq(respondents.phoneNumber, userData.phoneNumber)); if (existingUser.length > 0) { - throw new HTTPException(400, { + throw notFound({ message: "Email or username has been registered", - }); + }) } if (existingRespondent.length > 0) { - throw new HTTPException(400, { + throw forbidden({ message: "Phone number has been registered", - }); + }) } // Hash the password @@ -303,7 +287,9 @@ const usersRoute = new Hono() }) .returning() .catch(() => { - throw new HTTPException(500, { message: "Error creating user" }); + throw forbidden({ + message: "Error creating user", + }) }); // Create respondent @@ -325,7 +311,7 @@ const usersRoute = new Hono() }); // Add other roles if provided - if (userData.roles) { + if (userData.roles && userData.roles.length > 0) { const roles = userData.roles; for (let roleId of roles) { @@ -348,6 +334,10 @@ const usersRoute = new Hono() }); } } + } else { + throw forbidden({ + message: "Harap pilih minimal satu role", + }); } return newUser; @@ -387,9 +377,9 @@ const usersRoute = new Hono() ); if (existingUser.length > 0) { - throw new HTTPException(400, { + throw forbidden({ message: "Email or username has been registered by another user", - }); + }) } } @@ -431,7 +421,7 @@ const usersRoute = new Hono() } // Update roles if provided - if (userData.roles) { + if (userData.roles && userData.roles.length > 0) { const roles = userData.roles; // Remove existing roles for the user @@ -458,7 +448,11 @@ const usersRoute = new Hono() }); } } - } + } else { + throw forbidden({ + message: "Harap pilih minimal satu role", + }); + } }); return c.json({ @@ -497,13 +491,13 @@ const usersRoute = new Hono() // Throw error if the user does not exist if (!user[0]) - throw new HTTPException(404, { + throw notFound ({ message: "The user is not found", }); // Throw error if the user is trying to delete themselves if (user[0].id === currentUserId) { - throw new HTTPException(400, { + throw forbidden ({ message: "You cannot delete yourself", }); } @@ -541,7 +535,7 @@ const usersRoute = new Hono() // Throw error if the user is not deleted if (!user.deletedAt) { - throw new HTTPException(400, { + throw forbidden({ message: "The user is not deleted", }); } diff --git a/apps/frontend/src/modules/usersManagement/modals/UserDeleteModal.tsx b/apps/frontend/src/modules/usersManagement/modals/UserDeleteModal.tsx index 8e5f24f..2e5d89b 100644 --- a/apps/frontend/src/modules/usersManagement/modals/UserDeleteModal.tsx +++ b/apps/frontend/src/modules/usersManagement/modals/UserDeleteModal.tsx @@ -63,14 +63,14 @@ export default function UserDeleteModal() { navigate({ search: {} })} - title={`Delete confirmation`} + title={`Konfirmasi Hapus`} > - Are you sure you want to delete user{" "} + Apakah Anda yakin ingin menghapus pengguna{" "} {userQuery.data?.name} - ? This action is irreversible. + ? Tindakan ini tidak dapat diubah. {/* {errorMessage && {errorMessage}} */} @@ -81,7 +81,7 @@ export default function UserDeleteModal() { onClick={() => navigate({ search: {} })} disabled={mutation.isPending} > - Cancel + Batal diff --git a/apps/frontend/src/modules/usersManagement/modals/UserFormModal.tsx b/apps/frontend/src/modules/usersManagement/modals/UserFormModal.tsx index 8708684..3d96351 100644 --- a/apps/frontend/src/modules/usersManagement/modals/UserFormModal.tsx +++ b/apps/frontend/src/modules/usersManagement/modals/UserFormModal.tsx @@ -42,7 +42,7 @@ export default function UserFormModal() { const detailId = searchParams.detail; const editId = searchParams.edit; - const formType = detailId ? "detail" : editId ? "edit" : "create"; + const formType = detailId ? "detail" : editId ? "ubah" : "tambah"; /** * CHANGE FOLLOWING: @@ -51,7 +51,7 @@ export default function UserFormModal() { const userQuery = useQuery(getUserByIdQueryOptions(dataId)); const modalTitle = - formType.charAt(0).toUpperCase() + formType.slice(1) + " User"; + formType.charAt(0).toUpperCase() + formType.slice(1) + " Pengguna"; const form = useForm({ initialValues: { @@ -101,11 +101,11 @@ export default function UserFormModal() { mutationKey: ["usersMutation"], mutationFn: async ( options: - | { action: "edit"; data: Parameters[0] } - | { action: "create"; data: Parameters[0] } + | { action: "ubah"; data: Parameters[0] } + | { action: "tambah"; data: Parameters[0] } ) => { console.log("called"); - return options.action === "edit" + return options.action === "ubah" ? await updateUser(options.data) : await createUser(options.data); }, @@ -130,7 +130,7 @@ export default function UserFormModal() { if (formType === "detail") return; //TODO: OPtimize this code - if (formType === "create") { + if (formType === "tambah") { await mutation.mutateAsync({ action: formType, data: { @@ -168,7 +168,7 @@ export default function UserFormModal() { } queryClient.invalidateQueries({ queryKey: ["users"] }); notifications.show({ - message: `The ser is ${formType === "create" ? "created" : "edited"}`, + message: `The ser is ${formType === "tambah" ? "created" : "edited"}`, }); navigate({ search: {} }); @@ -212,59 +212,13 @@ export default function UserFormModal() { - {/* {createInputComponents({ - disableAll: mutation.isPending, - readonlyAll: formType === "detail", - inputs: [ - { - type: "text", - readOnly: true, - variant: "filled", - ...form.getInputProps("id"), - hidden: !form.values.id, - }, - { - type: "text", - label: "Name", - ...form.getInputProps("name"), - }, - { - type: "text", - label: "Username", - ...form.getInputProps("username"), - }, - { - type: "text", - label: "Email", - ...form.getInputProps("email"), - }, - { - type: "password", - label: "Password", - hidden: formType !== "create", - ...form.getInputProps("password"), - }, - { - type: "multi-select", - label: "Roles", - value: form.values.roles, - onChange: (values) => - form.setFieldValue("roles", values), - data: rolesQuery.data?.map((role) => ({ - value: role.id, - label: role.name, - })), - error: form.errors.roles, - }, - ], - })} */} - {createInputComponents({ disableAll: mutation.isPending, readonlyAll: formType === "detail", inputs: [ { type: "text", + label: "Id Pengguna", readOnly: true, variant: "filled", ...form.getInputProps("id"), @@ -272,17 +226,17 @@ export default function UserFormModal() { }, { type: "text", - label: "Name", + label: "Nama", ...form.getInputProps("name"), }, { type: "text", - label: "Position", + label: "Jabatan", ...form.getInputProps("position"), }, { type: "text", - label: "Work Experience", + label: "Pengalaman Kerja", ...form.getInputProps("workExperience"), }, { @@ -292,17 +246,17 @@ export default function UserFormModal() { }, { type: "text", - label: "Company Name", + label: "Instansi/Perusahaan", ...form.getInputProps("companyName"), }, { type: "text", - label: "Address", + label: "Alamat", ...form.getInputProps("address"), }, { type: "text", - label: "Phone Number", + label: "Nomor Telepon", ...form.getInputProps("phoneNumber"), }, @@ -326,7 +280,7 @@ export default function UserFormModal() { { type: "password", label: "Password", - hidden: formType !== "create", + hidden: formType !== "tambah", ...form.getInputProps("password"), }, ], diff --git a/apps/frontend/src/routeTree.gen.ts b/apps/frontend/src/routeTree.gen.ts index 8619974..3f1430d 100644 --- a/apps/frontend/src/routeTree.gen.ts +++ b/apps/frontend/src/routeTree.gen.ts @@ -23,6 +23,10 @@ import { Route as DashboardLayoutDashboardIndexImport } from './routes/_dashboar const IndexLazyImport = createFileRoute('/')() const LogoutIndexLazyImport = createFileRoute('/logout/')() const LoginIndexLazyImport = createFileRoute('/login/')() +const ForgotPasswordIndexLazyImport = createFileRoute('/forgot-password/')() +const ForgotPasswordVerifyLazyImport = createFileRoute( + '/forgot-password/verify', +)() // Create/Update Routes @@ -46,6 +50,20 @@ const LoginIndexLazyRoute = LoginIndexLazyImport.update({ getParentRoute: () => rootRoute, } as any).lazy(() => import('./routes/login/index.lazy').then((d) => d.Route)) +const ForgotPasswordIndexLazyRoute = ForgotPasswordIndexLazyImport.update({ + path: '/forgot-password/', + getParentRoute: () => rootRoute, +} as any).lazy(() => + import('./routes/forgot-password/index.lazy').then((d) => d.Route), +) + +const ForgotPasswordVerifyLazyRoute = ForgotPasswordVerifyLazyImport.update({ + path: '/forgot-password/verify', + getParentRoute: () => rootRoute, +} as any).lazy(() => + import('./routes/forgot-password/verify.lazy').then((d) => d.Route), +) + const DashboardLayoutUsersIndexRoute = DashboardLayoutUsersIndexImport.update({ path: '/users/', getParentRoute: () => DashboardLayoutRoute, @@ -83,6 +101,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DashboardLayoutImport parentRoute: typeof rootRoute } + '/forgot-password/verify': { + id: '/forgot-password/verify' + path: '/forgot-password/verify' + fullPath: '/forgot-password/verify' + preLoaderRoute: typeof ForgotPasswordVerifyLazyImport + parentRoute: typeof rootRoute + } + '/forgot-password/': { + id: '/forgot-password/' + path: '/forgot-password' + fullPath: '/forgot-password' + preLoaderRoute: typeof ForgotPasswordIndexLazyImport + parentRoute: typeof rootRoute + } '/login/': { id: '/login/' path: '/login' @@ -130,6 +162,8 @@ export const routeTree = rootRoute.addChildren({ DashboardLayoutTimetableIndexRoute, DashboardLayoutUsersIndexRoute, }), + ForgotPasswordVerifyLazyRoute, + ForgotPasswordIndexLazyRoute, LoginIndexLazyRoute, LogoutIndexLazyRoute, }) @@ -144,6 +178,8 @@ export const routeTree = rootRoute.addChildren({ "children": [ "/", "/_dashboardLayout", + "/forgot-password/verify", + "/forgot-password/", "/login/", "/logout/" ] @@ -159,6 +195,12 @@ export const routeTree = rootRoute.addChildren({ "/_dashboardLayout/users/" ] }, + "/forgot-password/verify": { + "filePath": "forgot-password/verify.lazy.tsx" + }, + "/forgot-password/": { + "filePath": "forgot-password/index.lazy.tsx" + }, "/login/": { "filePath": "login/index.lazy.tsx" }, diff --git a/apps/frontend/src/routes/_dashboardLayout/users/index.lazy.tsx b/apps/frontend/src/routes/_dashboardLayout/users/index.lazy.tsx index e155d41..3c5ec15 100644 --- a/apps/frontend/src/routes/_dashboardLayout/users/index.lazy.tsx +++ b/apps/frontend/src/routes/_dashboardLayout/users/index.lazy.tsx @@ -20,7 +20,7 @@ const columnHelper = createColumnHelper(); export default function UsersPage() { return ( , ]} columnDefs={[ @@ -30,7 +30,7 @@ export default function UsersPage() { }), columnHelper.display({ - header: "Name", + header: "Nama", cell: (props) => props.row.original.name, }), @@ -43,7 +43,7 @@ export default function UsersPage() { cell: (props) => props.row.original.email, }), columnHelper.display({ - header: "Company", + header: "Perusahaan", cell: (props) => props.row.original.company, }), @@ -54,12 +54,12 @@ export default function UsersPage() { if (roles && roles.length > 0) { return roles.map(role => role.name).join(", "); } - return
No roles assigned
; + return
Tidak ada peran yang diberikan
; }, }), columnHelper.display({ - header: "Actions", + header: "Aksi", cell: (props) => ( {createActionButtons([ @@ -71,14 +71,14 @@ export default function UsersPage() { icon: , }, { - label: "Edit", + label: "Ubah", permission: true, action: `?edit=${props.row.original.id}`, color: "orange", icon: , }, { - label: "Delete", + label: "Hapus", permission: true, action: `?delete=${props.row.original.id}`, color: "red",