Merge pull request #25 from digitalsolutiongroup/assessment-request-frontend
Assessment request frontend
This commit is contained in:
commit
d8c95229fd
|
|
@ -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,15 +1,13 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import { eq, sql, ilike, and, desc} from "drizzle-orm";
|
||||
import { Hono } from "hono";
|
||||
import { z } from "zod";
|
||||
import db from "../../drizzle";
|
||||
import { respondents } from "../../drizzle/schema/respondents";
|
||||
import { assessments } from "../../drizzle/schema/assessments";
|
||||
import { users } from "../../drizzle/schema/users";
|
||||
import { rolesToUsers } from "../../drizzle/schema/rolesToUsers";
|
||||
import { rolesSchema } from "../../drizzle/schema/roles";
|
||||
import HonoEnv from "../../types/HonoEnv";
|
||||
import authInfo from "../../middlewares/authInfo";
|
||||
import { notFound } from "../../errors/DashboardError";
|
||||
import { forbidden, notFound } from "../../errors/DashboardError";
|
||||
import checkPermission from "../../middlewares/checkPermission";
|
||||
import requestValidator from "../../utils/requestValidator";
|
||||
import { HTTPException } from "hono/http-exception";
|
||||
|
|
@ -20,85 +18,131 @@ 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),
|
||||
limit: z.coerce.number().int().min(1).max(1000).default(10),
|
||||
q: z.string().optional(),
|
||||
})
|
||||
),
|
||||
async (c) => {
|
||||
const userId = c.req.param("id");
|
||||
const currentUser = c.get("currentUser");
|
||||
const userId = currentUser?.id; // Get user ID of the currently logged in currentUser
|
||||
|
||||
if (!userId) {
|
||||
throw forbidden({
|
||||
message: "User not authenticated"
|
||||
});
|
||||
}
|
||||
const { page, limit, q } = c.req.valid("query");
|
||||
|
||||
// Query to count 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 && q.trim() !== ""
|
||||
? ilike(sql`${assessments.status}::text`, `%${q}%`) // Cast status to text for ilike
|
||||
: undefined
|
||||
)
|
||||
)
|
||||
|
||||
const totalCountResult = await totalCountQuery;
|
||||
const totalItems = totalCountResult[0]?.count || 0;
|
||||
|
||||
// Query to get assessment data with pagination
|
||||
const queryResult = await db
|
||||
.select({
|
||||
userId: users.id,
|
||||
createdAt: assessments.createdAt,
|
||||
name: users.name,
|
||||
code: rolesSchema.code,
|
||||
assessmentId: assessments.id,
|
||||
tanggal: assessments.createdAt,
|
||||
status: assessments.status,
|
||||
respondentId: respondents.id,
|
||||
})
|
||||
.from(users)
|
||||
.leftJoin(rolesToUsers, eq(users.id, rolesToUsers.userId))
|
||||
.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 && q.trim() !== ""
|
||||
? ilike(sql`${assessments.status}::text`, `%${q}%`) // Cast status to text for ilike
|
||||
: undefined
|
||||
)
|
||||
)
|
||||
.orderBy(desc(assessments.createdAt))
|
||||
.offset(page * limit)
|
||||
.limit(limit);
|
||||
|
||||
|
||||
if (!queryResult[0]) throw notFound();
|
||||
|
||||
const assessmentRequestData = {
|
||||
...queryResult,
|
||||
};
|
||||
|
||||
return c.json(assessmentRequestData);
|
||||
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",
|
||||
z.object({
|
||||
respondentId: z.string().min(1),
|
||||
respondentId: z.string().min(1), // Memastikan respondentId minimal ada
|
||||
})
|
||||
),
|
||||
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
|
||||
// Memastikan user sudah terautentikasi
|
||||
if (!userId) {
|
||||
throw new HTTPException(400, { message: "User ID is required." });
|
||||
return c.text("User not authenticated", 401);
|
||||
}
|
||||
|
||||
// Validate if respondent exists
|
||||
// Validasi apakah respondent dengan respondentId tersebut ada
|
||||
const respondent = await db
|
||||
.select()
|
||||
.from(respondents)
|
||||
.where(eq(respondents.id, respondentId));
|
||||
.where(and(eq(respondents.id, respondentId), eq(respondents.userId, userId)));
|
||||
|
||||
if (!respondent.length) {
|
||||
throw new HTTPException(404, { message: "Respondent not found." });
|
||||
throw new HTTPException(404, { message: "Respondent not found or unauthorized." });
|
||||
}
|
||||
|
||||
// Create the assessment request
|
||||
// Membuat permohonan asesmen baru
|
||||
const newAssessment = await db
|
||||
.insert(assessments)
|
||||
.values({
|
||||
id: createId(),
|
||||
respondentId,
|
||||
status: "menunggu konfirmasi",
|
||||
status: "menunggu konfirmasi", // Status awal permohonan
|
||||
validatedBy: null,
|
||||
validatedAt: null,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
.returning();
|
||||
|
||||
return c.json({ message: "Successfully submitted the assessment request" }, 201);
|
||||
return c.json({ message: "Successfully submitted the assessment request", data: newAssessment }, 201);
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
|||
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,34 @@
|
|||
import { Modal, Text, Flex } from "@mantine/core";
|
||||
import { Button } from "@/shadcn/components/ui/button";
|
||||
|
||||
interface StartAssessmentModalProps {
|
||||
assessmentId: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: (assessmentId: string) => void;
|
||||
}
|
||||
|
||||
export default function StartAssessmentModal({
|
||||
assessmentId,
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}: StartAssessmentModalProps) {
|
||||
return (
|
||||
<Modal opened={isOpen} onClose={onClose} title="Konfirmasi Mulai Asesmen">
|
||||
<Text>Apakah Anda yakin ingin memulai asesmen ini?</Text>
|
||||
<Flex gap="sm" justify="flex-end" mt="md">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onConfirm(assessmentId); // Use assessmentId when confirming
|
||||
}}
|
||||
>
|
||||
Mulai Asesmen
|
||||
</Button>
|
||||
</Flex>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
import {
|
||||
Flex,
|
||||
Modal,
|
||||
Text
|
||||
} from "@mantine/core";
|
||||
import { Button } from "@/shadcn/components/ui/button";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { getRouteApi } from "@tanstack/react-router";
|
||||
import { useEffect } from "react";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import FormResponseError from "@/errors/FormResponseError";
|
||||
import createInputComponents from "@/utils/createInputComponents";
|
||||
import { assessmentRequestQueryOptions, createAssessmentRequest } from "../queries/assessmentRequestQueries";
|
||||
|
||||
/**
|
||||
* Change this
|
||||
*/
|
||||
const routeApi = getRouteApi("/_dashboardLayout/assessmentRequest/");
|
||||
|
||||
export default function UserFormModal() {
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const navigate = routeApi.useNavigate();
|
||||
|
||||
const searchParams = routeApi.useSearch();
|
||||
|
||||
const isModalOpen = Boolean(searchParams.create);
|
||||
|
||||
const formType = "create";
|
||||
|
||||
const userQuery = useQuery(assessmentRequestQueryOptions(0, 10));
|
||||
|
||||
const modalTitle = <b>Konfirmasi</b>
|
||||
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
respondentsId: "",
|
||||
name: "",
|
||||
},
|
||||
});
|
||||
|
||||
// used to get the respondentId of the currently logged in user
|
||||
// and then set respondentsId in the form to create an assessment request
|
||||
useEffect(() => {
|
||||
const data = userQuery.data;
|
||||
|
||||
if (!data) {
|
||||
form.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
form.setValues({
|
||||
respondentsId: data.data[0].respondentId ?? "",
|
||||
name: data.data[0].name ?? "",
|
||||
});
|
||||
|
||||
form.setErrors({});
|
||||
}, [userQuery.data]);
|
||||
|
||||
// Mutation function to create a new assessment request and refresh query after success
|
||||
const mutation = useMutation({
|
||||
mutationKey: ["usersMutation"],
|
||||
mutationFn: async (options: { action: "create"; data: { respondentsId: string } }) => {
|
||||
console.log("called");
|
||||
if (options.action === "create") {
|
||||
return await createAssessmentRequest(options.data);
|
||||
}
|
||||
},
|
||||
// auto refresh after mutation
|
||||
onSuccess: () => {
|
||||
// force a query-reaction to retrieve the latest data
|
||||
queryClient.invalidateQueries({ queryKey: ["assessmentRequest"] });
|
||||
|
||||
notifications.show({
|
||||
message: "Permohonan Asesmen berhasil dibuat!",
|
||||
color: "green",
|
||||
});
|
||||
|
||||
// close modal
|
||||
navigate({ search: {} });
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
console.log(error);
|
||||
|
||||
if (error instanceof FormResponseError) {
|
||||
form.setErrors(error.formErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
notifications.show({
|
||||
message: error.message,
|
||||
color: "red",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Handle submit form, mutate data to server and close modal after success
|
||||
const handleSubmit = async (values: typeof form.values) => {
|
||||
|
||||
if (formType === "create") {
|
||||
try {
|
||||
await mutation.mutateAsync({
|
||||
action: "create",
|
||||
data: {
|
||||
respondentsId: values.respondentsId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["users"] });
|
||||
navigate({ search: {} });
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={isModalOpen}
|
||||
onClose={() => navigate({ search: {} })}
|
||||
title= {modalTitle}
|
||||
size="md"
|
||||
>
|
||||
<form onSubmit={form.onSubmit((values) => handleSubmit(values))}>
|
||||
|
||||
<Text>Apakah anda yakin ingin membuat Permohonan Asesmen Baru?</Text>
|
||||
|
||||
{/* Fields to display data will be sent but only respondentId */}
|
||||
{createInputComponents({
|
||||
disableAll: mutation.isPending,
|
||||
readonlyAll: formType === "create",
|
||||
inputs: [
|
||||
{
|
||||
type: "text",
|
||||
label: "Respondent ID",
|
||||
...form.getInputProps("respondentsId"),
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
label: "Name",
|
||||
...form.getInputProps("name"),
|
||||
hidden: true,
|
||||
},
|
||||
],
|
||||
})}
|
||||
|
||||
{/* Buttons */}
|
||||
<Flex justify="flex-end" align="center" gap="sm" mt="lg">
|
||||
<Button
|
||||
variant="outline"
|
||||
type="button"
|
||||
onClick={() => navigate({ search: {} })}
|
||||
disabled={mutation.isPending}
|
||||
>
|
||||
Tidak
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={mutation.isPending}
|
||||
>
|
||||
Buat Permohonan
|
||||
</Button>
|
||||
</Flex>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import client from "@/honoClient";
|
||||
import fetchRPC from "@/utils/fetchRPC";
|
||||
import { queryOptions } from "@tanstack/react-query";
|
||||
|
||||
export const assessmentRequestQueryOptions = (page: number, limit: number, q?: string) =>
|
||||
queryOptions({
|
||||
queryKey: ["assessmentRequest", { page, limit, q }],
|
||||
queryFn: () =>
|
||||
fetchRPC(
|
||||
client.assessmentRequest.$get({
|
||||
query: {
|
||||
limit: String(limit),
|
||||
page: String(page),
|
||||
q,
|
||||
},
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
export const createAssessmentRequest = async ({ respondentsId }: { respondentsId: string }) => {
|
||||
const response = await client.assessmentRequest.$post({
|
||||
json: { respondentId: respondentsId },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to create assessment request");
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
};
|
||||
|
|
@ -17,6 +17,7 @@ import { Route as DashboardLayoutImport } from './routes/_dashboardLayout'
|
|||
import { Route as DashboardLayoutUsersIndexImport } from './routes/_dashboardLayout/users/index'
|
||||
import { Route as DashboardLayoutTimetableIndexImport } from './routes/_dashboardLayout/timetable/index'
|
||||
import { Route as DashboardLayoutDashboardIndexImport } from './routes/_dashboardLayout/dashboard/index'
|
||||
import { Route as DashboardLayoutAssessmentRequestIndexImport } from './routes/_dashboardLayout/assessmentRequest/index'
|
||||
|
||||
// Create Virtual Routes
|
||||
|
||||
|
|
@ -83,6 +84,16 @@ const DashboardLayoutDashboardIndexRoute =
|
|||
getParentRoute: () => DashboardLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const DashboardLayoutAssessmentRequestIndexRoute =
|
||||
DashboardLayoutAssessmentRequestIndexImport.update({
|
||||
path: '/assessmentRequest/',
|
||||
getParentRoute: () => DashboardLayoutRoute,
|
||||
} as any).lazy(() =>
|
||||
import('./routes/_dashboardLayout/assessmentRequest/index.lazy').then(
|
||||
(d) => d.Route,
|
||||
),
|
||||
)
|
||||
|
||||
// Populate the FileRoutesByPath interface
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
|
|
@ -129,6 +140,13 @@ declare module '@tanstack/react-router' {
|
|||
preLoaderRoute: typeof LogoutIndexLazyImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/_dashboardLayout/assessmentRequest/': {
|
||||
id: '/_dashboardLayout/assessmentRequest/'
|
||||
path: '/assessmentRequest'
|
||||
fullPath: '/assessmentRequest'
|
||||
preLoaderRoute: typeof DashboardLayoutAssessmentRequestIndexImport
|
||||
parentRoute: typeof DashboardLayoutImport
|
||||
}
|
||||
'/_dashboardLayout/dashboard/': {
|
||||
id: '/_dashboardLayout/dashboard/'
|
||||
path: '/dashboard'
|
||||
|
|
@ -158,6 +176,7 @@ declare module '@tanstack/react-router' {
|
|||
export const routeTree = rootRoute.addChildren({
|
||||
IndexLazyRoute,
|
||||
DashboardLayoutRoute: DashboardLayoutRoute.addChildren({
|
||||
DashboardLayoutAssessmentRequestIndexRoute,
|
||||
DashboardLayoutDashboardIndexRoute,
|
||||
DashboardLayoutTimetableIndexRoute,
|
||||
DashboardLayoutUsersIndexRoute,
|
||||
|
|
@ -190,6 +209,7 @@ export const routeTree = rootRoute.addChildren({
|
|||
"/_dashboardLayout": {
|
||||
"filePath": "_dashboardLayout.tsx",
|
||||
"children": [
|
||||
"/_dashboardLayout/assessmentRequest/",
|
||||
"/_dashboardLayout/dashboard/",
|
||||
"/_dashboardLayout/timetable/",
|
||||
"/_dashboardLayout/users/"
|
||||
|
|
@ -207,6 +227,10 @@ export const routeTree = rootRoute.addChildren({
|
|||
"/logout/": {
|
||||
"filePath": "logout/index.lazy.tsx"
|
||||
},
|
||||
"/_dashboardLayout/assessmentRequest/": {
|
||||
"filePath": "_dashboardLayout/assessmentRequest/index.tsx",
|
||||
"parent": "/_dashboardLayout"
|
||||
},
|
||||
"/_dashboardLayout/dashboard/": {
|
||||
"filePath": "_dashboardLayout/dashboard/index.tsx",
|
||||
"parent": "/_dashboardLayout"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
import { useState } from "react";
|
||||
import { assessmentRequestQueryOptions } from "@/modules/assessmentRequestManagement/queries/assessmentRequestQueries";
|
||||
import PageTemplate from "@/components/PageTemplate";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import FormModal from "@/modules/assessmentRequestManagement/modals/CreateAssessmentRequestModal";
|
||||
import ExtractQueryDataType from "@/types/ExtractQueryDataType";
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
import { Badge } from "@/shadcn/components/ui/badge";
|
||||
import { Button } from "@/shadcn/components/ui/button";
|
||||
import StartAssessmentModal from "@/modules/assessmentRequestManagement/modals/ConfirmModal";
|
||||
|
||||
export const Route = createLazyFileRoute("/_dashboardLayout/assessmentRequest/")({
|
||||
component: UsersPage,
|
||||
});
|
||||
|
||||
type DataType = ExtractQueryDataType<typeof assessmentRequestQueryOptions>;
|
||||
|
||||
const columnHelper = createColumnHelper<DataType>();
|
||||
|
||||
export default function UsersPage() {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [selectedAssessmentId, setSelectedAssessmentId] = useState<string | null>(null);
|
||||
|
||||
/**
|
||||
* Function to open confirmation modal to start assessment
|
||||
* @param {string} assessmentId ID of the assessment to be started
|
||||
*/
|
||||
const handleOpenModal = (assessmentId: string) => {
|
||||
if (!assessmentId) {
|
||||
console.error("Assessment ID is missing");
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedAssessmentId(assessmentId);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to open assessment page in new tab
|
||||
* @param {string} assessmentId ID of the assessment to be opened
|
||||
*/
|
||||
const handleStartAssessment = (assessmentId: string) => {
|
||||
// Redirect to new URL in new tab
|
||||
const assessmentUrl = `/assessment?id=${assessmentId}`;
|
||||
window.open(assessmentUrl, "_blank");
|
||||
setModalOpen(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to open assessment result page based on valid ID
|
||||
* Used when "View Result" button is clicked
|
||||
* @param {string} assessmentId ID of the assessment to be opened
|
||||
*/
|
||||
const handleViewResult = (assessmentId: string) => {
|
||||
// to make sure assessmentId is valid and not null
|
||||
if (!assessmentId) {
|
||||
console.error("Assessment ID is missing");
|
||||
return;
|
||||
}
|
||||
const resultUrl = `/assessmentResult/${assessmentId}`;
|
||||
window.location.href = resultUrl;
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTemplate
|
||||
title="Permohonan Asesmen"
|
||||
queryOptions={assessmentRequestQueryOptions}
|
||||
modals={[<FormModal />]}
|
||||
columnDefs={[
|
||||
columnHelper.display({
|
||||
header: "No",
|
||||
cell: (props) => props.row.index + 1,
|
||||
}),
|
||||
columnHelper.display({
|
||||
header: "Tanggal",
|
||||
cell: (props) =>
|
||||
props.row.original.tanggal
|
||||
? 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.tanggal))
|
||||
: 'N/A',
|
||||
}),
|
||||
columnHelper.display({
|
||||
header: "Status",
|
||||
cell: (props) => {
|
||||
const status = props.row.original.status;
|
||||
switch (status) {
|
||||
case "menunggu konfirmasi":
|
||||
return <Badge variant={"waiting"}>Menunggu Konfirmasi</Badge>;
|
||||
case "diterima":
|
||||
return <Badge variant={"accepted"}>Diterima</Badge>;
|
||||
case "ditolak":
|
||||
return <Badge variant={"rejected"}>Ditolak</Badge>;
|
||||
case "selesai":
|
||||
return <Badge variant={"completed"}>Selesai</Badge>;
|
||||
default:
|
||||
return <Badge variant={"outline"}>Tidak diketahui</Badge>;
|
||||
}
|
||||
},
|
||||
}),
|
||||
columnHelper.display({
|
||||
header: "Actions",
|
||||
cell: (props) => {
|
||||
const status = props.row.original.status;
|
||||
const assessmentId = props.row.original.assessmentId; // Retrieve the assessmentId from the data row
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
{/* Button Create Assessment */}
|
||||
{status === "selesai" ? (
|
||||
<Button variant={"secondary"} disabled>Mulai Asesmen</Button>
|
||||
) : status === "diterima" ? (
|
||||
<Button
|
||||
onClick={() => handleOpenModal(assessmentId ?? '')}
|
||||
>
|
||||
Mulai Asesmen
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant={"secondary"} disabled>Mulai Asesmen</Button>
|
||||
)}
|
||||
|
||||
{/* Button View Result */}
|
||||
{status === "selesai" ? (
|
||||
<Button variant={"outline"} onClick={()=>handleViewResult(assessmentId ?? '')}>Lihat Hasil</Button>
|
||||
) : (
|
||||
<Button variant={"outline"} disabled>Lihat Hasil</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Confirmation Modal to Start Assessment */}
|
||||
{selectedAssessmentId && (
|
||||
<StartAssessmentModal
|
||||
assessmentId={selectedAssessmentId}
|
||||
isOpen={modalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
onConfirm={handleStartAssessment}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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));
|
||||
},
|
||||
});
|
||||
|
|
@ -15,6 +15,12 @@ const badgeVariants = cva(
|
|||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
|
||||
// Custom variants for status
|
||||
waiting: "border-transparent bg-yellow-500 text-white hover:bg-yellow-600",
|
||||
accepted: "border-transparent bg-green-500 text-white hover:bg-green-600",
|
||||
rejected: "border-transparent bg-red-500 text-white hover:bg-red-600",
|
||||
completed: "border-transparent bg-blue-500 text-white hover:bg-blue-600",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { TbLoader2 } from "react-icons/tb"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
|
|
@ -37,17 +38,25 @@ export interface ButtonProps
|
|||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
({ className, variant, size, asChild = false, isLoading, children, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
disabled={isLoading || props.disabled} // Disable button if loading
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{isLoading ? (
|
||||
<TbLoader2 className="mr-2 h-4 w-4 animate-spin" /> // Show spinner when loading
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</Comp>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user