Commit ·
9185393
1
Parent(s): fdaefd4
Displaying Applications.
Browse files
frontend/app/routes.ts
CHANGED
|
@@ -5,6 +5,7 @@ export default [
|
|
| 5 |
route("jobs", "routes/jobs.tsx"),
|
| 6 |
route("jobs/:id", "routes/jobs.$id.tsx"),
|
| 7 |
route("jobs/:jid/assessments/:id", "routes/jobs.$jid.assessments.$id.tsx"),
|
|
|
|
| 8 |
route("dashboard", "routes/dashboard.tsx"),
|
| 9 |
route("registration", "routes/registration.tsx"),
|
| 10 |
] satisfies RouteConfig;
|
|
|
|
| 5 |
route("jobs", "routes/jobs.tsx"),
|
| 6 |
route("jobs/:id", "routes/jobs.$id.tsx"),
|
| 7 |
route("jobs/:jid/assessments/:id", "routes/jobs.$jid.assessments.$id.tsx"),
|
| 8 |
+
route("jobs/:jid/assessment/:aid/applications", "routes/jobs.$jid.assessment.$aid.applications.tsx"),
|
| 9 |
route("dashboard", "routes/dashboard.tsx"),
|
| 10 |
route("registration", "routes/registration.tsx"),
|
| 11 |
] satisfies RouteConfig;
|
frontend/app/routes/jobs.$jid.assessment.$aid.applications.tsx
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Avatar } from "radix-ui";
|
| 2 |
+
import { useParams } from "react-router";
|
| 3 |
+
import { Loader2Icon } from "lucide-react";
|
| 4 |
+
import { JobCard } from "~/components/job-card";
|
| 5 |
+
import { Paginator } from "~/components/paginator";
|
| 6 |
+
import { useGetJobByID } from "~/services/useGetJobsByID";
|
| 7 |
+
import { AssessmentCard } from "~/components/assessment-card";
|
| 8 |
+
import { useGetJobAssessmentByID } from "~/services/useGetJobAssessmentByID";
|
| 9 |
+
import type { Route } from "./+types/jobs.$jid.assessment.$aid.applications";
|
| 10 |
+
import { useGetJobAssessmentApplications } from "~/services/useGetJobAssessmentApplications";
|
| 11 |
+
|
| 12 |
+
export function meta({}: Route.MetaArgs) {
|
| 13 |
+
return [
|
| 14 |
+
{ title: "Assessments Applications" },
|
| 15 |
+
{
|
| 16 |
+
name: "description",
|
| 17 |
+
content: "View applications for the selected job assessment.",
|
| 18 |
+
},
|
| 19 |
+
];
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
export default function AssessmentDetailRoute() {
|
| 23 |
+
const { jid, aid } = useParams();
|
| 24 |
+
const { data: job, isLoading: isJobLoading, isError: isJobError, refetch: refetchJob } = useGetJobByID({ id: jid || "" });
|
| 25 |
+
const { data: jobAssessment, isLoading: isJobAssessmentLoading, isError: isJobAssessmentError, refetch: refetchJobAssessment } = useGetJobAssessmentByID({ jid: jid || "", id: aid || "" });
|
| 26 |
+
const { data: { data: applications, total } = { data: [] }, isLoading: isApplicationsLoading, isError: isApplicationsError, refetch: refetchApplications } = useGetJobAssessmentApplications({ jid: jid || "", aid: aid || "" });
|
| 27 |
+
|
| 28 |
+
const isError = isJobError || isJobAssessmentError || isApplicationsError;
|
| 29 |
+
const isLoading = isJobLoading || isJobAssessmentLoading || isApplicationsLoading;
|
| 30 |
+
const refetch = () => (refetchJob(), refetchJobAssessment(), refetchApplications());
|
| 31 |
+
|
| 32 |
+
if (isLoading) {
|
| 33 |
+
return (
|
| 34 |
+
<main className="container mx-auto p-4 flex flex-col gap-2 place-items-center">
|
| 35 |
+
<div className="flex flex-col gap-2 place-items-center">
|
| 36 |
+
<Loader2Icon className="animate-spin" />
|
| 37 |
+
<p>Loading Applications...</p>
|
| 38 |
+
</div>
|
| 39 |
+
</main>
|
| 40 |
+
);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
if (isError) {
|
| 44 |
+
return (
|
| 45 |
+
<main className="container mx-auto p-4 flex flex-col gap-2">
|
| 46 |
+
<div className="bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-100 p-4 rounded flex flex-col gap-2 place-items-center">
|
| 47 |
+
<p className="text-center">Failed to load applications<br />Please try again</p>
|
| 48 |
+
<button
|
| 49 |
+
onClick={() => refetch()}
|
| 50 |
+
className="ml-4 px-3 py-1 cursor-pointer bg-red-500 text-white dark:bg-red-200 dark:text-red-700 rounded"
|
| 51 |
+
>
|
| 52 |
+
Retry
|
| 53 |
+
</button>
|
| 54 |
+
</div>
|
| 55 |
+
</main>
|
| 56 |
+
);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
return (
|
| 60 |
+
<main className="container mx-auto p-4 flex flex-col gap-8">
|
| 61 |
+
<JobCard job={job} isStatic />
|
| 62 |
+
<AssessmentCard jid={jid || ""} assessment={jobAssessment} isStatic />
|
| 63 |
+
<section className="flex flex-col gap-4">
|
| 64 |
+
<h3 className="text-xl font-semibold">Assessment's Applications</h3>
|
| 65 |
+
<div className="grid grid-cols-[repeat(auto-fill,minmax(400px,1fr))] gap-4">
|
| 66 |
+
{applications.length === 0 ? (
|
| 67 |
+
<p>No applications found for this assessment.</p>
|
| 68 |
+
) : applications.map(application => (
|
| 69 |
+
<div key={application.id} className="border p-4 rounded bg-indigo-50 dark:bg-gray-700 flex flex-wrap justify-evenly gap-4 place-items-center">
|
| 70 |
+
<div className="group-data-[collapsible=icon]:-mx-4 flex gap-2">
|
| 71 |
+
<Avatar.Avatar className="shrink-0 cursor-pointer" tabIndex={0}>
|
| 72 |
+
<Avatar.AvatarFallback className="rounded-full bg-gray-200 dark:bg-gray-800 size-10 group-data-[collapsible=icon]:size-8 flex items-center justify-center">
|
| 73 |
+
{application.user ? `${application.user.first_name[0]}${application.user.last_name[0]}` : "U"}
|
| 74 |
+
</Avatar.AvatarFallback>
|
| 75 |
+
</Avatar.Avatar>
|
| 76 |
+
<div className="overflow-hidden group-data-[collapsible=icon]:hidden">
|
| 77 |
+
<p className="font-bold whitespace-nowrap text-ellipsis overflow-hidden text-start">
|
| 78 |
+
{application.user.first_name} {application.user.last_name}
|
| 79 |
+
</p>
|
| 80 |
+
<p className="whitespace-nowrap text-ellipsis overflow-hidden">
|
| 81 |
+
{application.user.email}
|
| 82 |
+
</p>
|
| 83 |
+
</div>
|
| 84 |
+
</div>
|
| 85 |
+
<p>Score: {application.score}/{application.passing_score}</p>
|
| 86 |
+
</div>
|
| 87 |
+
))}
|
| 88 |
+
</div>
|
| 89 |
+
{total && <Paginator total={total} />}
|
| 90 |
+
</section>
|
| 91 |
+
</main>
|
| 92 |
+
);
|
| 93 |
+
}
|
frontend/app/routes/jobs.$jid.assessments.$id.tsx
CHANGED
|
@@ -1,17 +1,17 @@
|
|
| 1 |
import { toast } from "react-toastify";
|
| 2 |
-
import { useParams } from "react-router";
|
| 3 |
-
import { Loader2Icon } from "lucide-react";
|
| 4 |
import { useEffect, useState } from "react";
|
| 5 |
import { Label, RadioGroup } from "radix-ui";
|
| 6 |
import { Button } from "~/components/ui/button";
|
| 7 |
import { Textarea } from "~/components/ui/textarea";
|
| 8 |
import { Checkbox } from "~/components/ui/checkbox";
|
|
|
|
| 9 |
import { RadioGroupItem } from "~/components/ui/radio-group";
|
| 10 |
import { AssessmentCard } from "~/components/assessment-card";
|
| 11 |
import type { Route } from "./+types/jobs.$jid.assessments.$id";
|
| 12 |
import { useGetJobAssessmentByID } from "~/services/useGetJobAssessmentByID";
|
| 13 |
import { usePostAssessmentApplication } from "~/services/usePostAssessmentApplication";
|
| 14 |
-
import { useGetMyUser } from "~/services/useGetMyUser";
|
| 15 |
|
| 16 |
export function meta({}: Route.MetaArgs) {
|
| 17 |
return [
|
|
@@ -86,6 +86,10 @@ export default function AssessmentDetailRoute() {
|
|
| 86 |
return (
|
| 87 |
<main className="container mx-auto p-4 flex flex-col gap-8">
|
| 88 |
<AssessmentCard jid={jid || ""} assessment={assessment} isStatic />
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
<section className="flex flex-col gap-4">
|
| 90 |
<h3 className="text-xl font-semibold">Assessment's Questions</h3>
|
| 91 |
<div className="flex flex-col gap-4">
|
|
|
|
| 1 |
import { toast } from "react-toastify";
|
| 2 |
+
import { Link, useParams } from "react-router";
|
| 3 |
+
import { ExternalLinkIcon, Loader2Icon } from "lucide-react";
|
| 4 |
import { useEffect, useState } from "react";
|
| 5 |
import { Label, RadioGroup } from "radix-ui";
|
| 6 |
import { Button } from "~/components/ui/button";
|
| 7 |
import { Textarea } from "~/components/ui/textarea";
|
| 8 |
import { Checkbox } from "~/components/ui/checkbox";
|
| 9 |
+
import { useGetMyUser } from "~/services/useGetMyUser";
|
| 10 |
import { RadioGroupItem } from "~/components/ui/radio-group";
|
| 11 |
import { AssessmentCard } from "~/components/assessment-card";
|
| 12 |
import type { Route } from "./+types/jobs.$jid.assessments.$id";
|
| 13 |
import { useGetJobAssessmentByID } from "~/services/useGetJobAssessmentByID";
|
| 14 |
import { usePostAssessmentApplication } from "~/services/usePostAssessmentApplication";
|
|
|
|
| 15 |
|
| 16 |
export function meta({}: Route.MetaArgs) {
|
| 17 |
return [
|
|
|
|
| 86 |
return (
|
| 87 |
<main className="container mx-auto p-4 flex flex-col gap-8">
|
| 88 |
<AssessmentCard jid={jid || ""} assessment={assessment} isStatic />
|
| 89 |
+
<Link to={`/jobs/${jid}/assessment/${id}/applications`} className="text-indigo-600 hover:underline">
|
| 90 |
+
View Applications for this Assessment
|
| 91 |
+
<ExternalLinkIcon className="inline -translate-y-1 mx-2" />
|
| 92 |
+
</Link>
|
| 93 |
<section className="flex flex-col gap-4">
|
| 94 |
<h3 className="text-xl font-semibold">Assessment's Questions</h3>
|
| 95 |
<div className="flex flex-col gap-4">
|
frontend/app/services/useGetJobAssessmentApplications.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { User } from "./useGetMyUser";
|
| 2 |
+
import { useQuery } from "@tanstack/react-query";
|
| 3 |
+
import { HTTPManager } from "~/managers/HTTPManager";
|
| 4 |
+
import type { Pagination } from "~/types/pagination";
|
| 5 |
+
import { usePagination } from "~/hooks/use-pagination";
|
| 6 |
+
|
| 7 |
+
export const GET_JOB_ASSESSMENT_APPLICATION_KEY = "job-assessments-application";
|
| 8 |
+
|
| 9 |
+
export type Application = {
|
| 10 |
+
id: string;
|
| 11 |
+
user: User;
|
| 12 |
+
score: number;
|
| 13 |
+
passing_score: number;
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
export const useGetJobAssessmentApplications = ({ jid, aid }: { jid: string, aid: string }) => {
|
| 17 |
+
const { page, limit } = usePagination();
|
| 18 |
+
const searchParams = new URLSearchParams({ page: String(page), limit: String(limit) });
|
| 19 |
+
return useQuery({
|
| 20 |
+
queryKey: [GET_JOB_ASSESSMENT_APPLICATION_KEY, page, limit],
|
| 21 |
+
queryFn: async () => HTTPManager.get<Pagination<Application>>(`/applications/jobs/${jid}/assessments/${aid}?` + searchParams.toString()).then(response => response.data),
|
| 22 |
+
});
|
| 23 |
+
}
|
frontend/app/services/useGetMyUser.ts
CHANGED
|
@@ -4,11 +4,11 @@ import { HTTPManager } from "~/managers/HTTPManager";
|
|
| 4 |
export const GET_MY_USER_KEY = "my-user";
|
| 5 |
|
| 6 |
export type User = {
|
| 7 |
-
"id": string
|
| 8 |
-
"email": string
|
| 9 |
-
"last_name": string
|
| 10 |
-
"first_name": string
|
| 11 |
-
"role": "hr" | "applicant"
|
| 12 |
}
|
| 13 |
|
| 14 |
export const useGetMyUser = () => useQuery({
|
|
|
|
| 4 |
export const GET_MY_USER_KEY = "my-user";
|
| 5 |
|
| 6 |
export type User = {
|
| 7 |
+
"id": string;
|
| 8 |
+
"email": string;
|
| 9 |
+
"last_name": string;
|
| 10 |
+
"first_name": string;
|
| 11 |
+
"role": "hr" | "applicant";
|
| 12 |
}
|
| 13 |
|
| 14 |
export const useGetMyUser = () => useQuery({
|