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/drizzle/schema/respondents.ts b/apps/backend/src/drizzle/schema/respondents.ts index 744ec4f..98d8805 100644 --- a/apps/backend/src/drizzle/schema/respondents.ts +++ b/apps/backend/src/drizzle/schema/respondents.ts @@ -15,7 +15,7 @@ export const respondents = pgTable("respondents", { phoneNumber: varchar("phoneNumber", { length: 13 }).notNull(), createdAt: timestamp("createdAt", { mode: "date" }).defaultNow(), updatedAt: timestamp("updatedAt", { mode: "date" }).defaultNow(), - deletedAt: timestamp("deletetAt", { mode: "date" }), + deletedAt: timestamp("deletedAt", { mode: "date" }), }); export const respondentsRelations = relations(respondents, ({ one }) => ({ diff --git a/apps/backend/src/routes/users/route.ts b/apps/backend/src/routes/users/route.ts index 3069960..9f2e289 100644 --- a/apps/backend/src/routes/users/route.ts +++ b/apps/backend/src/routes/users/route.ts @@ -1,4 +1,4 @@ -import { and, eq, ilike, isNull, or, sql } from "drizzle-orm"; +import { and, eq, ilike, isNull, or, sql, not, inArray } from "drizzle-orm"; import { Hono } from "hono"; import { z } from "zod"; @@ -12,30 +12,21 @@ import HonoEnv from "../../types/HonoEnv"; import requestValidator from "../../utils/requestValidator"; import authInfo from "../../middlewares/authInfo"; import checkPermission from "../../middlewares/checkPermission"; +import { respondents } from "../../drizzle/schema/respondents"; +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), + 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().min(1, "Role is required")), }); export const userUpdateSchema = userFormSchema.extend({ @@ -50,75 +41,169 @@ const usersRoute = new Hono() * Query params: * - includeTrashed: boolean (default: false)\ * - withMetadata: boolean - */ + */ + + // Get all users with search .get( "/", checkPermission("users.readAll"), requestValidator( - "query", - z.object({ - includeTrashed: z - .string() - .optional() - .transform((v) => v?.toLowerCase() === "true"), - withMetadata: z - .string() - .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(1), - q: z.string().default(""), - }) + "query", + z.object({ + includeTrashed: z + .string() + .optional() + .transform((v) => v?.toLowerCase() === "true"), + withMetadata: z + .string() + .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(10), + q: z.string().default(""), // Keyword search + }) ), async (c) => { - const { includeTrashed, page, limit, q } = c.req.valid("query"); - - const totalCountQuery = includeTrashed - ? sql`(SELECT count(*) FROM ${users})` - : sql`(SELECT count(*) FROM ${users} WHERE ${users.deletedAt} IS NULL)`; - - const result = await db - .select({ - id: users.id, - name: users.name, - email: users.email, - username: users.username, - isEnabled: users.isEnabled, - createdAt: users.createdAt, - updatedAt: users.updatedAt, - ...(includeTrashed ? { deletedAt: users.deletedAt } : {}), - fullCount: totalCountQuery, - }) - .from(users) - .where( - and( - includeTrashed ? undefined : isNull(users.deletedAt), - q - ? or( - ilike(users.name, q), - ilike(users.username, q), - ilike(users.email, q), - eq(users.id, q) - ) - : undefined + const { includeTrashed, page, limit, q } = c.req.valid("query"); + + // Query to count total data without duplicates + const totalCountQuery = db + .select({ + count: sql`count(distinct ${users.id})`, + }) + .from(users) + .leftJoin(respondents, eq(users.id, respondents.userId)) + .leftJoin(rolesToUsers, eq(users.id, rolesToUsers.userId)) + .leftJoin(rolesSchema, eq(rolesToUsers.roleId, rolesSchema.id)) + .where( + and( + includeTrashed ? undefined : isNull(users.deletedAt), + q + ? or( + ilike(users.name, `%${q}%`), // Search by name + ilike(users.username, `%${q}%`), // Search by username + ilike(users.email, `%${q}%`), // Search by email + ilike(respondents.companyName, `%${q}%`), // Search by companyName (from respondents) + ilike(rolesSchema.name, `%${q}%`) // Search by role name (from rolesSchema) ) - ) - .offset(page * limit) - .limit(limit); - - return c.json({ - data: result.map((d) => ({ ...d, fullCount: undefined })), - _metadata: { - currentPage: page, - totalPages: Math.ceil( - (Number(result[0]?.fullCount) ?? 0) / limit - ), - totalItems: Number(result[0]?.fullCount) ?? 0, - perPage: limit, - }, - }); + : undefined + ) + ); + + // Get the total count result from the query + const totalCountResult = await totalCountQuery; + const totalCount = totalCountResult[0]?.count || 0; + + // Query to get unique user IDs based on pagination (Sub Query) + const userIdsQuery = db + .select({ + id: users.id, + }) + .from(users) + .leftJoin(respondents, eq(users.id, respondents.userId)) + .leftJoin(rolesToUsers, eq(users.id, rolesToUsers.userId)) + .leftJoin(rolesSchema, eq(rolesToUsers.roleId, rolesSchema.id)) + .where( + and( + includeTrashed ? undefined : isNull(users.deletedAt), + q + ? or( + ilike(users.name, `%${q}%`), // Search by name + ilike(users.username, `%${q}%`), // Search by username + ilike(users.email, `%${q}%`), + ilike(respondents.companyName, `%${q}%`), + ilike(rolesSchema.name, `%${q}%`) + ) + : undefined + ) + ) + .groupBy(users.id) // Group by user ID to avoid the effect of duplicate data + .offset(page * limit) + .limit(limit); + + // Main Query + const result = await db + .select({ + id: users.id, + name: users.name, + email: users.email, + username: users.username, + isEnabled: users.isEnabled, + createdAt: users.createdAt, + updatedAt: users.updatedAt, + ...(includeTrashed ? { deletedAt: users.deletedAt } : {}), + company: respondents.companyName, + role: { + name: rolesSchema.name, + id: rolesSchema.id, + }, + }) + .from(users) + .leftJoin(respondents, eq(users.id, respondents.userId)) + .leftJoin(rolesToUsers, eq(users.id, rolesToUsers.userId)) + .leftJoin(rolesSchema, eq(rolesToUsers.roleId, rolesSchema.id)) + .where(inArray(users.id, userIdsQuery)) // Only take data based on IDs from subquery + .orderBy(users.createdAt); + + // Group roles for each user to avoid duplication + const userMap = new Map< + string, + { + id: string; + name: string; + email: string | null; + username: string; + isEnabled: boolean; + createdAt: Date; + updatedAt: Date; + deletedAt?: Date; + company: string | null; + roles: { id: string; name: string }[]; + } + >(); + + result.forEach((item) => { + if (!userMap.has(item.id)) { + userMap.set(item.id, { + id: item.id, + name: item.name, + email: item.email ?? null, + username: item.username, + isEnabled: item.isEnabled ?? false, + createdAt: item.createdAt ?? new Date(), + updatedAt: item.updatedAt ?? new Date(), + deletedAt: item.deletedAt ?? undefined, + company: item.company, + roles: item.role + ? [{ id: item.role.id, name: item.role.name }] + : [], + }); + } else { + const existingUser = userMap.get(item.id); + if (item.role) { + existingUser?.roles.push({ + id: item.role.id, + name: item.role.name, + }); + } + } + }); + + // Return user data without duplicates, with roles array + const groupedData = Array.from(userMap.values()); + + return c.json({ + data: groupedData, + _metadata: { + currentPage: page, + totalPages: Math.ceil(totalCount / limit), + totalItems: totalCount, + perPage: limit, + }, + }); } - ) + ) + //get user by id .get( "/:id", @@ -139,7 +224,12 @@ const usersRoute = new Hono() .select({ id: users.id, name: users.name, + position: respondents.position, + workExperience: respondents.workExperience, email: users.email, + companyName: respondents.companyName, + address: respondents.address, + phoneNumber: respondents.phoneNumber, username: users.username, isEnabled: users.isEnabled, createdAt: users.createdAt, @@ -151,6 +241,7 @@ const usersRoute = new Hono() }, }) .from(users) + .leftJoin(respondents, eq(users.id, respondents.userId)) .leftJoin(rolesToUsers, eq(users.id, rolesToUsers.userId)) .leftJoin(rolesSchema, eq(rolesToUsers.roleId, rolesSchema.id)) .where( @@ -161,9 +252,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; @@ -180,38 +271,121 @@ const usersRoute = new Hono() return c.json(userData); } ) + //create user .post( "/", checkPermission("users.create"), - requestValidator("form", userFormSchema), + requestValidator("json", userFormSchema), async (c) => { - const userData = c.req.valid("form"); + const userData = c.req.valid("json"); - const user = await db - .insert(users) - .values({ - name: userData.name, - username: userData.username, - email: userData.email, - password: await hashPassword(userData.password), - isEnabled: userData.isEnabled.toLowerCase() === "true", - }) - .returning(); - - if (userData.roles) { - const roles = JSON.parse(userData.roles) as string[]; - console.log(roles); - - if (roles.length) { - await db.insert(rolesToUsers).values( - roles.map((role) => ({ - userId: user[0].id, - roleId: role, - })) - ); - } + // Check if the provided email or username is already exists in database + const conditions = []; + if (userData.email) { + conditions.push(eq(users.email, userData.email)); } + conditions.push(eq(users.username, userData.username)); + + const existingUser = await db + .select() + .from(users) + .where( + or( + eq(users.email, userData.email), + eq(users.username, userData.username) + ) + ); + + const existingRespondent = await db + .select() + .from(respondents) + .where(eq(respondents.phoneNumber, userData.phoneNumber)); + + if (existingUser.length > 0) { + throw forbidden({ + message: "Email or username has been registered", + }) + } + + if (existingRespondent.length > 0) { + throw forbidden({ + message: "Phone number has been registered", + }) + } + + // Hash the password + const hashedPassword = await hashPassword(userData.password); + + // Start a transaction + const result = await db.transaction(async (trx) => { + // Create user + const [newUser] = await trx + .insert(users) + .values({ + name: userData.name, + username: userData.username, + email: userData.email, + password: hashedPassword, + isEnabled: userData.isEnabled?.toLowerCase() === "true" || true, + }) + .returning() + .catch(() => { + throw forbidden({ + message: "Error creating user", + }) + }); + + // Create respondent + const [newRespondent] = await trx + .insert(respondents) + .values({ + companyName: userData.companyName, + position: userData.position, + workExperience: userData.workExperience, + address: userData.address, + phoneNumber: userData.phoneNumber, + userId: newUser.id, + }) + .returning() + .catch((err) => { + throw new HTTPException(500, { + message: "Error creating respondent: " + err.message, + }); + }); + + // Add other roles if provided + if (userData.roles && userData.roles.length > 0) { + const roles = userData.roles; + + for (let roleId of roles) { + const role = ( + await trx + .select() + .from(rolesSchema) + .where(eq(rolesSchema.id, roleId)) + .limit(1) + )[0]; + + if (role) { + await trx.insert(rolesToUsers).values({ + userId: newUser.id, + roleId: role.id, + }); + } else { + throw new HTTPException(404, { + message: `Role ${roleId} does not exists`, + }); + } + } + } else { + throw forbidden({ + message: "Harap pilih minimal satu role", + }); + } + + return newUser; + }); return c.json( { @@ -226,10 +400,32 @@ const usersRoute = new Hono() .patch( "/:id", checkPermission("users.update"), - requestValidator("form", userUpdateSchema), + requestValidator("json", userUpdateSchema), async (c) => { const userId = c.req.param("id"); - const userData = c.req.valid("form"); + const userData = c.req.valid("json"); + + // Check if the provided email or username is already exists in the database (excluding the current user) + if (userData.email || userData.username) { + const existingUser = await db + .select() + .from(users) + .where( + and( + or( + eq(users.email, userData.email), + eq(users.username, userData.username) + ), + not(eq(users.id, userId)) + ) + ); + + if (existingUser.length > 0) { + throw forbidden({ + message: "Email or username has been registered by another user", + }) + } + } const user = await db .select() @@ -238,18 +434,71 @@ const usersRoute = new Hono() if (!user[0]) return c.notFound(); - await db - .update(users) - .set({ - ...userData, - ...(userData.password - ? { password: await hashPassword(userData.password) } - : {}), - updatedAt: new Date(), - isEnabled: userData.isEnabled.toLowerCase() === "true", - }) - .where(eq(users.id, userId)); + // Start transaction to update both user and respondent + await db.transaction(async (trx) => { + // Update user + await trx + .update(users) + .set({ + ...userData, + ...(userData.password + ? { password: await hashPassword(userData.password) } + : {}), + updatedAt: new Date(), + isEnabled: userData.isEnabled.toLowerCase() === "true", + }) + .where(eq(users.id, userId)); + // Update respondent data if provided + if (userData.companyName || userData.position || userData.workExperience || userData.address || userData.phoneNumber) { + await trx + .update(respondents) + .set({ + ...(userData.companyName ? {companyName: userData.companyName} : {}), + ...(userData.position ? {position: userData.position} : {}), + ...(userData.workExperience ? {workExperience: userData.workExperience} : {}), + ...(userData.address ? {address: userData.address} : {}), + ...(userData.phoneNumber ? {phoneNumber: userData.phoneNumber} : {}), + updatedAt: new Date(), + }) + .where(eq(respondents.userId, userId)); + } + + // Update roles if provided + if (userData.roles && userData.roles.length > 0) { + const roles = userData.roles; + + // Remove existing roles for the user + await trx.delete(rolesToUsers).where(eq(rolesToUsers.userId, userId)); + + // Assign new roles + for (let roleId of roles) { + const role = ( + await trx + .select() + .from(rolesSchema) + .where(eq(rolesSchema.id, roleId)) + .limit(1) + )[0]; + + if (role) { + await trx.insert(rolesToUsers).values({ + userId: userId, + roleId: role.id, + }); + } else { + throw new HTTPException(404, { + message: `Role ${roleId} does not exist`, + }); + } + } + } else { + throw forbidden({ + message: "Harap pilih minimal satu role", + }); + } + }); + return c.json({ message: "User updated successfully", }); @@ -273,6 +522,7 @@ const usersRoute = new Hono() const skipTrash = c.req.valid("form").skipTrash.toLowerCase() === "true"; + // Check if the user exists const user = await db .select() .from(users) @@ -283,17 +533,20 @@ 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", }); } + // Delete or soft delete user if (skipTrash) { await db.delete(users).where(eq(users.id, userId)); } else { @@ -311,28 +564,34 @@ const usersRoute = new Hono() ) //undo delete - .patch("/restore/:id", checkPermission("users.restore"), async (c) => { - const userId = c.req.param("id"); + .patch( + "/restore/:id", + checkPermission("users.restore"), + async (c) => { + const userId = c.req.param("id"); - const user = ( - await db.select().from(users).where(eq(users.id, userId)) - )[0]; + // Check if the user exists + const user = ( + await db.select().from(users).where(eq(users.id, userId)) + )[0]; - if (!user) return c.notFound(); + if (!user) return c.notFound(); - if (!user.deletedAt) { - throw new HTTPException(400, { - message: "The user is not deleted", + // Throw error if the user is not deleted + if (!user.deletedAt) { + throw forbidden({ + message: "The user is not deleted", + }); + } + + // Restore user + await db + .update(users) + .set({ deletedAt: null }) + .where(eq(users.id, userId)); + + return c.json({ + message: "User restored successfully", }); - } - - await db - .update(users) - .set({ deletedAt: null }) - .where(eq(users.id, userId)); - - return c.json({ - message: "User restored successfully", - }); }); export default usersRoute; 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 05531b1..c153550 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: { @@ -62,6 +62,11 @@ export default function UserFormModal() { photoProfileUrl: "", password: "", roles: [] as string[], + companyName: "", + position: "", + workExperience: "", + address: "", + phoneNumber: "", }, }); @@ -81,6 +86,11 @@ export default function UserFormModal() { username: data.username, password: "", roles: data.roles.map((v) => v.id), //only extract the id + companyName: data.companyName ?? "", + position: data.position ?? "", + workExperience: data.workExperience ?? "", + address: data.address ?? "", + phoneNumber: data.phoneNumber ?? "", }); form.setErrors({}); @@ -91,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); }, @@ -120,16 +130,21 @@ export default function UserFormModal() { if (formType === "detail") return; //TODO: OPtimize this code - if (formType === "create") { + if (formType === "tambah") { await mutation.mutateAsync({ action: formType, data: { email: values.email, name: values.name, password: values.password, - roles: JSON.stringify(values.roles), + roles: values.roles, isEnabled: "true", username: values.username, + companyName: values.email, + position: values.position, + workExperience: values.workExperience, + address: values.address, + phoneNumber: values.phoneNumber, }, }); } else { @@ -140,15 +155,20 @@ export default function UserFormModal() { email: values.email, name: values.name, password: values.password, - roles: JSON.stringify(values.roles), + roles: values.roles, isEnabled: "true", username: values.username, + companyName: values.companyName, + position: values.position, + workExperience: values.workExperience, + address: values.address, + phoneNumber: values.phoneNumber, }, }); } queryClient.invalidateQueries({ queryKey: ["users"] }); notifications.show({ - message: `The ser is ${formType === "create" ? "created" : "edited"}`, + message: `The ser is ${formType === "tambah" ? "created" : "edited"}`, }); navigate({ search: {} }); @@ -198,20 +218,18 @@ export default function UserFormModal() { inputs: [ { type: "text", - readOnly: true, - variant: "filled", - ...form.getInputProps("id"), - hidden: !form.values.id, - }, - { - type: "text", - label: "Name", + label: "Nama", ...form.getInputProps("name"), }, { type: "text", - label: "Username", - ...form.getInputProps("username"), + label: "Jabatan", + ...form.getInputProps("position"), + }, + { + type: "text", + label: "Pengalaman Kerja", + ...form.getInputProps("workExperience"), }, { type: "text", @@ -219,11 +237,21 @@ export default function UserFormModal() { ...form.getInputProps("email"), }, { - type: "password", - label: "Password", - hidden: formType !== "create", - ...form.getInputProps("password"), + type: "text", + label: "Instansi/Perusahaan", + ...form.getInputProps("companyName"), }, + { + type: "text", + label: "Alamat", + ...form.getInputProps("address"), + }, + { + type: "text", + label: "Nomor Telepon", + ...form.getInputProps("phoneNumber"), + }, + { type: "multi-select", label: "Roles", @@ -236,6 +264,17 @@ export default function UserFormModal() { })), error: form.errors.roles, }, + { + type: "text", + label: "Username", + ...form.getInputProps("username"), + }, + { + type: "password", + label: "Password", + hidden: formType !== "tambah", + ...form.getInputProps("password"), + }, ], })} @@ -246,7 +285,7 @@ export default function UserFormModal() { onClick={() => navigate({ search: {} })} disabled={mutation.isPending} > - Close + Tutup {formType !== "detail" && ( )} diff --git a/apps/frontend/src/modules/usersManagement/queries/userQueries.ts b/apps/frontend/src/modules/usersManagement/queries/userQueries.ts index 02da504..2093488 100644 --- a/apps/frontend/src/modules/usersManagement/queries/userQueries.ts +++ b/apps/frontend/src/modules/usersManagement/queries/userQueries.ts @@ -34,26 +34,26 @@ export const getUserByIdQueryOptions = (userId: string | undefined) => }); export const createUser = async ( - form: InferRequestType["form"] + json: InferRequestType["json"] ) => { return await fetchRPC( client.users.$post({ - form, + json, }) ); }; export const updateUser = async ( - form: InferRequestType<(typeof client.users)[":id"]["$patch"]>["form"] & { + json: InferRequestType<(typeof client.users)[":id"]["$patch"]>["json"] & { id: string; } ) => { return await fetchRPC( client.users[":id"].$patch({ param: { - id: form.id, + id: json.id, }, - form, + json, }) ); }; 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 85cb609..8b6262d 100644 --- a/apps/frontend/src/routes/_dashboardLayout/users/index.lazy.tsx +++ b/apps/frontend/src/routes/_dashboardLayout/users/index.lazy.tsx @@ -20,17 +20,17 @@ const columnHelper = createColumnHelper(); export default function UsersPage() { return ( , ]} columnDefs={[ columnHelper.display({ - header: "#", + header: "No", cell: (props) => props.row.index + 1, }), columnHelper.display({ - header: "Name", + header: "Nama", cell: (props) => props.row.original.name, }), @@ -38,19 +38,28 @@ export default function UsersPage() { header: "Username", cell: (props) => props.row.original.username, }), - columnHelper.display({ - header: "Status", - cell: (props) => - props.row.original.isEnabled ? ( - Active - ) : ( - Inactive - ), + header: "Email", + cell: (props) => props.row.original.email, + }), + columnHelper.display({ + header: "Perusahaan", + cell: (props) => props.row.original.company, }), columnHelper.display({ - header: "Actions", + header: "Roles", + cell: (props) => { + const roles = props.row.original.roles; // Get array of roles from data + if (roles && roles.length > 0) { + return roles.map(role => role.name).join(", "); + } + return
-
; + }, + }), + + columnHelper.display({ + header: "Aksi", cell: (props) => ( {createActionButtons([ @@ -62,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", diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts index 2d6bad5..6c9655b 100644 --- a/apps/frontend/vite.config.ts +++ b/apps/frontend/vite.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ plugins: [react(), TanStackRouterVite()], resolve: { alias: { - "@": path.resolve(__dirname,"/src"), + "@": path.resolve(__dirname,"src"), }, }, });