Pull Request branch dev-clone to main #1
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ const authInfo = createMiddleware<HonoEnv>(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),
|
||||
|
|
|
|||
|
|
@ -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<HonoEnv>()
|
|||
|
||||
// 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<number>`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<HonoEnv>()
|
|||
.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<HonoEnv>()
|
|||
),
|
||||
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
|
||||
|
|
|
|||
1
apps/backend/src/types/HonoEnv.d.ts
vendored
1
apps/backend/src/types/HonoEnv.d.ts
vendored
|
|
@ -5,6 +5,7 @@ type HonoEnv = {
|
|||
Variables: {
|
||||
uid?: string;
|
||||
currentUser?: {
|
||||
id: string;
|
||||
name: string;
|
||||
permissions: SpecificPermissionCode[];
|
||||
roles: RoleCode[];
|
||||
|
|
|
|||
|
|
@ -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<typeof client.users.$post>["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: {},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
@ -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<typeof assessmentRequestQueryOptions>;
|
||||
|
||||
const columnHelper = createColumnHelper<DataType>();
|
||||
|
||||
export default function UsersPage() {
|
||||
return (
|
||||
<PageTemplate
|
||||
title="Users"
|
||||
queryOptions={assessmentRequestQueryOptions}
|
||||
modals={[<UserFormModal />, <UserDeleteModal />]}
|
||||
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 <Badge color="yellow">Menunggu Konfirmasi</Badge>;
|
||||
case "diterima":
|
||||
return <Badge color="green">Diterima</Badge>;
|
||||
case "ditolak":
|
||||
return <Badge color="red">Ditolak</Badge>;
|
||||
case "selesai":
|
||||
return <Badge color="blue">Selesai</Badge>;
|
||||
default:
|
||||
return <Badge color="gray">Tidak diketahui</Badge>;
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
columnHelper.display({
|
||||
header: "Actions",
|
||||
cell: (props) => {
|
||||
const status = props.row.original.status;
|
||||
|
||||
return (
|
||||
<Flex gap="xs">
|
||||
{status === "selesai" ? (
|
||||
<Button onClick={() => alert('Lihat Hasil')}>Lihat Hasil</Button>
|
||||
) : status === "diterima" ? (
|
||||
<Button onClick={() => alert('Mulai Asesmen')}>Mulai Asesmen</Button>
|
||||
) : status === "menunggu konfirmasi" || status === "ditolak" ? (
|
||||
<Button disabled>
|
||||
Mulai Asesmen
|
||||
</Button>
|
||||
) : null}
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -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));
|
||||
},
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user