diff --git a/apps/backend/src/data/sidebarMenus.ts b/apps/backend/src/data/sidebarMenus.ts index ec98d7e..9cb2421 100644 --- a/apps/backend/src/data/sidebarMenus.ts +++ b/apps/backend/src/data/sidebarMenus.ts @@ -21,6 +21,13 @@ const sidebarMenus: SidebarMenu[] = [ link: "/questions", color: "green", }, + { + label: "Permohonan Asesmen", + icon: { tb: "TbChecklist" }, + allowedPermissions: ["permissions.read"], + link: "/assessmentRequest", + color: "green", + }, ]; export default sidebarMenus; diff --git a/apps/backend/src/middlewares/authInfo.ts b/apps/backend/src/middlewares/authInfo.ts index 10514c2..957dc24 100644 --- a/apps/backend/src/middlewares/authInfo.ts +++ b/apps/backend/src/middlewares/authInfo.ts @@ -71,6 +71,7 @@ const authInfo = createMiddleware(async (c, next) => { // Setting the currentUser with fetched data c.set("currentUser", { + id: user[0].users.id, // Adding user ID here name: user[0].users.name, // Assuming the first result is the user permissions: Array.from(permissions), roles: Array.from(roles), diff --git a/apps/backend/src/routes/assessmentRequest/route.ts b/apps/backend/src/routes/assessmentRequest/route.ts index 3f189a9..3332c71 100644 --- a/apps/backend/src/routes/assessmentRequest/route.ts +++ b/apps/backend/src/routes/assessmentRequest/route.ts @@ -1,4 +1,4 @@ -import { eq } from "drizzle-orm"; +import { eq, sql, ilike, isNull, and } from "drizzle-orm"; import { Hono } from "hono"; import { z } from "zod"; import db from "../../drizzle"; @@ -20,23 +20,53 @@ const assessmentRequestRoute = new Hono() // Get assessment request by user ID .get( - "/:id", + "/", checkPermission("assessmentRequest.read"), requestValidator( "query", z.object({ - includeTrashed: z.string().default("false"), + page: z.coerce.number().int().min(0).default(0), // Menambahkan pagination page + limit: z.coerce.number().int().min(1).max(1000).default(10), // Menambahkan pagination limit + q: z.string().optional(), // Kata kunci pencarian (search) }) ), async (c) => { - const userId = c.req.param("id"); - + const currentUser = c.get("currentUser"); + const userId = currentUser?.id; // Mengambil userId dari currentUser yang disimpan di context + if (!userId) { + // Handle case where userId is undefined + return c.text("User not authenticated", 401); + } + + const { page, limit, q } = c.req.valid("query"); + + // Query untuk menghitung total data + const totalCountQuery = db + .select({ + count: sql`count(distinct ${assessments.id})`, + }) + .from(assessments) + .leftJoin(respondents, eq(assessments.respondentId, respondents.id)) + .leftJoin(users, eq(respondents.userId, users.id)) + .where( + and( + eq(users.id, userId), + q ? ilike(assessments.status, `%${q}%`) : undefined + ) + ) + + const totalCountResult = await totalCountQuery; + const totalItems = totalCountResult[0]?.count || 0; + + // Query untuk mendapatkan data assessment dengan pagination const queryResult = await db .select({ userId: users.id, createdAt: assessments.createdAt, name: users.name, code: rolesSchema.code, + id: assessments.id, + tanggal: assessments.createdAt, status: assessments.status, }) .from(users) @@ -44,21 +74,34 @@ const assessmentRequestRoute = new Hono() .leftJoin(rolesSchema, eq(rolesToUsers.roleId, rolesSchema.id)) .leftJoin(respondents, eq(users.id, respondents.userId)) .leftJoin(assessments, eq(respondents.id, assessments.respondentId)) - .where(eq(users.id, userId)); - + .where( + and( + eq(users.id, userId), + q ? ilike(assessments.status, `%${q}%`) : undefined + ) + ) + .offset(page * limit) + .limit(limit); + if (!queryResult[0]) throw notFound(); - - const assessmentRequestData = { - ...queryResult, - }; - - return c.json(assessmentRequestData); + + // Mengembalikan data dengan metadata pagination + return c.json({ + data: queryResult, + _metadata: { + currentPage: page, + totalPages: Math.ceil(totalItems / limit), + totalItems, + perPage: limit, + }, + }); } ) + // Post assessment request by user ID .post( - "/:id", + "/", checkPermission("assessmentRequest.create"), requestValidator( "json", @@ -68,11 +111,12 @@ const assessmentRequestRoute = new Hono() ), async (c) => { const { respondentId } = c.req.valid("json"); - const userId = c.req.param("id"); + const currentUser = c.get("currentUser"); + const userId = currentUser?.id; // Mengambil userId dari currentUser yang disimpan di context // Make sure the userId exists if (!userId) { - throw new HTTPException(400, { message: "User ID is required." }); + throw new HTTPException(400, { message: "User not authenticated" }); } // Validate if respondent exists @@ -101,5 +145,5 @@ const assessmentRequestRoute = new Hono() return c.json({ message: "Successfully submitted the assessment request" }, 201); } ); - + export default assessmentRequestRoute; \ No newline at end of file diff --git a/apps/backend/src/types/HonoEnv.d.ts b/apps/backend/src/types/HonoEnv.d.ts index 4cf35db..e9ceb7d 100644 --- a/apps/backend/src/types/HonoEnv.d.ts +++ b/apps/backend/src/types/HonoEnv.d.ts @@ -5,6 +5,7 @@ type HonoEnv = { Variables: { uid?: string; currentUser?: { + id: string; name: string; permissions: SpecificPermissionCode[]; roles: RoleCode[]; diff --git a/apps/frontend/src/modules/assessmentRequestManagement/queries/assessmentRequestQueries.ts b/apps/frontend/src/modules/assessmentRequestManagement/queries/assessmentRequestQueries.ts new file mode 100644 index 0000000..17344f6 --- /dev/null +++ b/apps/frontend/src/modules/assessmentRequestManagement/queries/assessmentRequestQueries.ts @@ -0,0 +1,68 @@ +import client from "@/honoClient"; +import fetchRPC from "@/utils/fetchRPC"; +import { queryOptions } from "@tanstack/react-query"; +import { InferRequestType } from "hono"; + +export const assessmentRequestQueryOptions = (page: number, limit: number, q?: string) => + queryOptions({ + queryKey: ["assesmentRequest", { page, limit, q }], + queryFn: () => + fetchRPC( + client.assessmentRequest.$get({ + query: { + limit: String(limit), + page: String(page), + q, + }, + }) + ), + }); + +export const getUserByIdQueryOptions = (userId: string | undefined) => + queryOptions({ + queryKey: ["user", userId], + queryFn: () => + fetchRPC( + client.users[":id"].$get({ + param: { + id: userId!, + }, + query: {}, + }) + ), + enabled: Boolean(userId), + }); + +export const createUser = async ( + form: InferRequestType["form"] +) => { + return await fetchRPC( + client.users.$post({ + form, + }) + ); +}; + +export const updateUser = async ( + form: InferRequestType<(typeof client.users)[":id"]["$patch"]>["form"] & { + id: string; + } +) => { + return await fetchRPC( + client.users[":id"].$patch({ + param: { + id: form.id, + }, + form, + }) + ); +}; + +export const deleteUser = async (id: string) => { + return await fetchRPC( + client.users[":id"].$delete({ + param: { id }, + form: {}, + }) + ); +}; diff --git a/apps/frontend/src/routes/_dashboardLayout/assessmentRequest/index.lazy.tsx b/apps/frontend/src/routes/_dashboardLayout/assessmentRequest/index.lazy.tsx new file mode 100644 index 0000000..08db5ed --- /dev/null +++ b/apps/frontend/src/routes/_dashboardLayout/assessmentRequest/index.lazy.tsx @@ -0,0 +1,87 @@ +import { assessmentRequestQueryOptions } from "@/modules/assessmentRequestManagement/queries/assessmentRequestQueries"; +import PageTemplate from "@/components/PageTemplate"; +import { createLazyFileRoute } from "@tanstack/react-router"; +import UserFormModal from "@/modules/assessmentRequestManagement/modals/UserFormModal"; +import ExtractQueryDataType from "@/types/ExtractQueryDataType"; +import { createColumnHelper } from "@tanstack/react-table"; +import { Badge, Flex } from "@mantine/core"; +import UserDeleteModal from "@/modules/assessmentRequestManagement/modals/UserDeleteModal"; +import { Button } from "@/shadcn/components/ui/button"; + +export const Route = createLazyFileRoute("/_dashboardLayout/assessmentRequest/")({ + component: UsersPage, +}); + +type DataType = ExtractQueryDataType; + +const columnHelper = createColumnHelper(); + +export default function UsersPage() { + return ( + , ]} + columnDefs={[ + columnHelper.display({ + header: "No", + cell: (props) => props.row.index + 1, + }), + columnHelper.display({ + header: "Tanggal", + cell: (props) => + props.row.original.createdAt + ? new Intl.DateTimeFormat("ID", { + year: "numeric", + month: "long", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + hour12: true, + }).format(new Date(props.row.original.createdAt)) + : 'N/A', + }), + + columnHelper.display({ + header: "Status", + cell: (props) => { + const status = props.row.original.status; + switch (status) { + case "menunggu konfirmasi": + return Menunggu Konfirmasi; + case "diterima": + return Diterima; + case "ditolak": + return Ditolak; + case "selesai": + return Selesai; + default: + return Tidak diketahui; + } + }, + }), + + columnHelper.display({ + header: "Actions", + cell: (props) => { + const status = props.row.original.status; + + return ( + + {status === "selesai" ? ( + + ) : status === "diterima" ? ( + + ) : status === "menunggu konfirmasi" || status === "ditolak" ? ( + + ) : null} + + ); + }, + }), + ]} + /> + ); +} \ No newline at end of file diff --git a/apps/frontend/src/routes/_dashboardLayout/assessmentRequest/index.tsx b/apps/frontend/src/routes/_dashboardLayout/assessmentRequest/index.tsx new file mode 100644 index 0000000..3eb169d --- /dev/null +++ b/apps/frontend/src/routes/_dashboardLayout/assessmentRequest/index.tsx @@ -0,0 +1,18 @@ +import { assessmentRequestQueryOptions } from "@/modules/assessmentRequestManagement/queries/assessmentRequestQueries" +import { createFileRoute } from "@tanstack/react-router"; +import { z } from "zod"; + +const searchParamSchema = z.object({ + create: z.boolean().default(false).optional(), + edit: z.string().default("").optional(), + delete: z.string().default("").optional(), + detail: z.string().default("").optional(), +}); + +export const Route = createFileRoute("/_dashboardLayout/assessmentRequest/")({ + validateSearch: searchParamSchema, + + loader: ({ context: { queryClient } }) => { + queryClient.ensureQueryData(assessmentRequestQueryOptions(0, 10)); + }, +});