diff --git a/apps/backend/src/routes/users/route.ts b/apps/backend/src/routes/users/route.ts index 260afcd..9f2e289 100644 --- a/apps/backend/src/routes/users/route.ts +++ b/apps/backend/src/routes/users/route.ts @@ -41,135 +41,168 @@ 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(10), - 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"); - - // Total count for pagination - const totalCountQuery = includeTrashed - ? sql`(SELECT count(*) FROM ${users})` - : sql`(SELECT count(*) FROM ${users} WHERE ${users.deletedAt} IS NULL)`; - - // Query to get unique user IDs with pagination (Sub Query) - const userIdsQuery = db + const { includeTrashed, page, limit, q } = c.req.valid("query"); + + // Query to count total data without duplicates + const totalCountQuery = db .select({ - id: users.id, - }) - .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 - ) - ) - .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, - }, - fullCount: totalCountQuery, + 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(inArray(users.id, userIdsQuery)) // using ID from subquery - .orderBy(users.createdAt); // sort by createdAt - - - // Group roles for each user to prevent duplication - const userMap = new Map(); - - 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 duplication and roles in array form - const groupedData = Array.from(userMap.values()); - - return c.json({ - data: groupedData.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, - }, - }); + .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) + ) + : 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( @@ -270,7 +303,7 @@ const usersRoute = new Hono() .where(eq(respondents.phoneNumber, userData.phoneNumber)); if (existingUser.length > 0) { - throw notFound({ + throw forbidden({ message: "Email or username has been registered", }) } diff --git a/apps/frontend/src/modules/usersManagement/modals/UserFormModal.tsx b/apps/frontend/src/modules/usersManagement/modals/UserFormModal.tsx index 3d96351..c153550 100644 --- a/apps/frontend/src/modules/usersManagement/modals/UserFormModal.tsx +++ b/apps/frontend/src/modules/usersManagement/modals/UserFormModal.tsx @@ -216,14 +216,6 @@ export default function UserFormModal() { disableAll: mutation.isPending, readonlyAll: formType === "detail", inputs: [ - { - type: "text", - label: "Id Pengguna", - readOnly: true, - variant: "filled", - ...form.getInputProps("id"), - hidden: !form.values.id, - }, { type: "text", label: "Nama", @@ -293,7 +285,7 @@ export default function UserFormModal() { onClick={() => navigate({ search: {} })} disabled={mutation.isPending} > - Close + Tutup {formType !== "detail" && ( )} diff --git a/apps/frontend/src/routes/_dashboardLayout/users/index.lazy.tsx b/apps/frontend/src/routes/_dashboardLayout/users/index.lazy.tsx index 3c5ec15..8b6262d 100644 --- a/apps/frontend/src/routes/_dashboardLayout/users/index.lazy.tsx +++ b/apps/frontend/src/routes/_dashboardLayout/users/index.lazy.tsx @@ -54,7 +54,7 @@ export default function UsersPage() { if (roles && roles.length > 0) { return roles.map(role => role.name).join(", "); } - return
Tidak ada peran yang diberikan
; + return
-
; }, }),