Skip to content

Commit 5a0611d

Browse files
228/Assessment Template Preview (#243)
* assessment template preview * lint * lint * fix fetch * quick fixes * quick fix * lint --------- Co-authored-by: Laith Taher <lotaher04@gmail.com>
1 parent 31112cf commit 5a0611d

8 files changed

Lines changed: 345 additions & 64 deletions

File tree

src/app/(web)/crm/templates/page.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
DialogTitle,
4141
} from '@/lib/components/ui/Modal';
4242
import { useAuth } from '@/lib/auth/auth-context';
43+
import { AssessmentTemplatePreview } from '@/lib/components/core/AssessmentTemplatePreview';
4344

4445
export default function TemplatesPage() {
4546
const [selectedTaskTemplate, setSelectedTaskTemplate] =
@@ -241,8 +242,10 @@ export default function TemplatesPage() {
241242
index={idx}
242243
taskTemplateId={task.id}
243244
isPreviewSelected={selectedTaskTemplate?.id === task.id}
244-
onPreviewSelect={() => setSelectedTaskTemplate(task)}
245-
maxTags={2}
245+
onPreviewSelect={() => {
246+
setSelectedAssessmentTemplate(null);
247+
setSelectedTaskTemplate(task);
248+
}}
246249
/>
247250
);
248251
})
@@ -352,7 +355,10 @@ export default function TemplatesPage() {
352355
key={assessment.id}
353356
template={assessment}
354357
isSelected={selectedAssessmentTemplate?.id === assessment.id}
355-
onClick={() => setSelectedAssessmentTemplate(assessment)}
358+
onClick={() => {
359+
setSelectedTaskTemplate(null);
360+
setSelectedAssessmentTemplate(assessment);
361+
}}
356362
/>
357363
))
358364
)}
@@ -370,15 +376,17 @@ export default function TemplatesPage() {
370376
</div>
371377
</TabsContent>
372378

373-
<div className="flex w-3/4 flex-col overflow-y-scroll p-[30px] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
379+
<div className="flex w-3/4 flex-col overflow-y-scroll [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
374380
{selectedTaskTemplate ? (
375381
<TaskTemplatePreviewPanel
376382
taskTemplatePreview={selectedTaskTemplate}
377383
onDuplicate={isMutating ? undefined : onDuplicate}
378384
onDelete={isMutating ? undefined : onDelete}
379385
/>
380386
) : selectedAssessmentTemplate ? (
381-
<div className="flex h-full items-center justify-center" />
387+
<AssessmentTemplatePreview
388+
assessmentTemplatePreview={selectedAssessmentTemplate}
389+
/>
382390
) : (
383391
<div className="text-body-m text-muted-foreground flex h-full items-center justify-center">
384392
Select a template to preview

src/app/api/assessment-templates/[id]/tasks/route.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,17 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
2424
return handleError(err);
2525
}
2626
}
27+
28+
export async function GET(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
29+
try {
30+
const session = await getSession();
31+
const { id } = await params;
32+
33+
await AssessmentTemplateService.getAssessmentTemplate(id, session.activeOrganizationId);
34+
const tasks = await AssessmentTemplateService.getAssessmentTemplateTaskOrder(id);
35+
36+
return Response.json({ data: tasks }, { status: 200 });
37+
} catch (err) {
38+
return handleError(err);
39+
}
40+
}

src/lib/api/assessment-templates.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,25 @@ export async function createAssessmentTemplate(
117117

118118
return json.data;
119119
}
120+
121+
export type AssessmentTemplateTaskOrder = {
122+
taskTemplateId: string;
123+
order: number;
124+
};
125+
126+
/**
127+
* GET /api/assessment-templates/:id/tasks
128+
*/
129+
export async function getAssessmentTemplateTaskOrder(
130+
assessmentTemplateId: string
131+
): Promise<AssessmentTemplateTaskOrder[]> {
132+
const res = await fetch(`/api/assessment-templates/${assessmentTemplateId}/tasks`);
133+
134+
const json = await res.json();
135+
136+
if (!res.ok) {
137+
throw new Error(json.message);
138+
}
139+
140+
return json.data;
141+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use client';
2+
3+
import * as React from 'react';
4+
import { AlarmClock } from 'lucide-react';
5+
import type { TaskTemplateListItemDTO } from '@/lib/schemas/task-template.schema';
6+
import { TaskTemplatePreviewPanel } from './TaskTemplatePreviewPanel';
7+
8+
export interface TaskPreviewProps {
9+
taskTemplatePreview: TaskTemplateListItemDTO;
10+
}
11+
12+
export function TaskAssessmentPreview({ taskTemplatePreview }: TaskPreviewProps) {
13+
const { title } = taskTemplatePreview;
14+
return (
15+
<div className="flex h-full w-full flex-col gap-2 overflow-hidden px-6 pt-4">
16+
<div className="inline-flex flex-col items-start gap-1 self-stretch">
17+
<h2 className="text-display-xs justify-start font-['Satoshi_Variable'] font-medium text-neutral-800">
18+
{title}
19+
</h2>
20+
<div className="inline-flex items-center justify-start gap-2 self-stretch">
21+
<AlarmClock className="h-5 w-5 text-neutral-700" />
22+
<p className="text-body-s font-medium text-neutral-800"> WiP </p>
23+
</div>
24+
</div>
25+
<div>
26+
<TaskTemplatePreviewPanel
27+
removeHeader={true}
28+
taskTemplatePreview={taskTemplatePreview}
29+
/>
30+
</div>
31+
</div>
32+
);
33+
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
import type { AssessmentTemplateListItemDTO } from '@/lib/schemas/assessment-template.schema';
5+
import {
6+
getAssessmentTemplateTaskOrder,
7+
type AssessmentTemplateTaskOrder,
8+
} from '@/lib/api/assessment-templates';
9+
import { getTaskTemplate } from '@/lib/api/task-templates';
10+
import { Button } from '@/lib/components/ui/Button';
11+
import {
12+
DropdownMenu,
13+
DropdownMenuContent,
14+
DropdownMenuItem,
15+
DropdownMenuTrigger,
16+
} from '@/lib/components/ui/Dropdown';
17+
import { ChevronLeft, ChevronRight, SquarePen } from 'lucide-react';
18+
import Link from 'next/link';
19+
import { type TaskTemplateListItemDTO } from '@/lib/schemas/task-template.schema';
20+
import { TaskAssessmentPreview } from './AssessmentTaskPreview';
21+
22+
export interface AssessmentTemplatePreviewProps {
23+
assessmentTemplatePreview: AssessmentTemplateListItemDTO;
24+
}
25+
26+
export function AssessmentTemplatePreview({
27+
assessmentTemplatePreview,
28+
}: AssessmentTemplatePreviewProps) {
29+
const [tasks, setTasks] = useState<AssessmentTemplateTaskOrder[]>([]);
30+
const [taskTemplates, setTaskTemplates] = useState<TaskTemplateListItemDTO[]>([]);
31+
const [currentIndex, setCurrentIndex] = useState(0);
32+
const [isLoading, setIsLoading] = useState(false);
33+
34+
useEffect(() => {
35+
let isMounted = true;
36+
setIsLoading(true);
37+
setCurrentIndex(0);
38+
39+
const load = async () => {
40+
const orderedTasks = await getAssessmentTemplateTaskOrder(assessmentTemplatePreview.id);
41+
42+
const templates = await Promise.all(
43+
orderedTasks.map(async (task) => {
44+
const taskTemplate = await getTaskTemplate(task.taskTemplateId);
45+
46+
return taskTemplate;
47+
})
48+
);
49+
50+
if (!isMounted) return;
51+
setTasks(orderedTasks);
52+
setTaskTemplates(templates as TaskTemplateListItemDTO[]);
53+
};
54+
55+
load()
56+
.catch(() => {
57+
if (!isMounted) return;
58+
setTasks([]);
59+
setTaskTemplates([]);
60+
})
61+
.finally(() => {
62+
if (!isMounted) return;
63+
setIsLoading(false);
64+
});
65+
66+
return () => {
67+
isMounted = false;
68+
};
69+
}, [assessmentTemplatePreview.id]);
70+
71+
const currentTask = taskTemplates[currentIndex];
72+
const totalTasks = tasks.length;
73+
74+
if (isLoading) {
75+
return (
76+
<div className="text-body-m text-muted-foreground flex h-full items-center justify-center">
77+
Loading tasks...
78+
</div>
79+
);
80+
}
81+
82+
if (totalTasks === 0) {
83+
return (
84+
<div className="text-body-m text-muted-foreground flex h-full items-center justify-center">
85+
No tasks in this assessment template
86+
</div>
87+
);
88+
}
89+
90+
return (
91+
<div className="flex h-full w-full flex-col">
92+
<div className="border-border flex w-full flex-row items-center justify-between border-b px-6 py-5">
93+
<div className="flex w-full flex-col gap-1">
94+
<h1 className="text-display-xs text-foreground truncate font-medium">
95+
{assessmentTemplatePreview.title}
96+
</h1>
97+
<div className="flex flex-row items-center gap-0.5">
98+
<p className="text-body-s font-medium text-gray-400">Assigned to</p>
99+
<p className="text-label-s text-sarge-primary-600 underline">
100+
{assessmentTemplatePreview.positions.length > 0
101+
? assessmentTemplatePreview.positions
102+
.map((position) => position.title)
103+
.join(', ')
104+
: '0 positions'}
105+
</p>
106+
</div>
107+
</div>
108+
<Button variant="secondary" className="h-fit px-4 py-2" asChild>
109+
<Link
110+
aria-label="Edit assessment template"
111+
href={`/crm/assessment-templates/${assessmentTemplatePreview.id}/edit`}
112+
>
113+
<SquarePen className="size-5" />
114+
Edit details
115+
</Link>
116+
</Button>
117+
</div>
118+
119+
<div className="flex-1 overflow-y-auto">
120+
{currentTask ? (
121+
<TaskAssessmentPreview taskTemplatePreview={currentTask} />
122+
) : (
123+
<div className="text-body-m text-muted-foreground flex h-full items-center justify-center">
124+
Select a task to preview
125+
</div>
126+
)}
127+
</div>
128+
129+
<div className="border-border flex w-full items-center justify-between border-t px-6 py-3">
130+
<div className="flex flex-row items-center gap-0.5">
131+
<p className="text-body-s font-medium text-gray-400">Created by</p>
132+
<p className="text-body-s font-bold text-gray-400">
133+
{assessmentTemplatePreview.author?.name ?? 'Unknown'}
134+
</p>
135+
</div>
136+
137+
<div className="bg-sarge-gray-0 border-sarge-gray-200 flex items-center rounded-lg border-1">
138+
<div className="flex-1">
139+
<DropdownMenu>
140+
<DropdownMenuTrigger asChild>
141+
<div className="bg-sarge-gray-0 hover:bg-sarge-gray-100 border-sarge-gray-200 flex cursor-pointer items-center gap-2 rounded-l-lg border-r-1 px-3 py-2">
142+
<span className="text-sarge-gray-800 text-sm font-medium">
143+
{currentTask?.title ?? 'Select task'}
144+
</span>
145+
</div>
146+
</DropdownMenuTrigger>
147+
<DropdownMenuContent align="start">
148+
{tasks.map((task, index) => (
149+
<DropdownMenuItem
150+
key={`${task.taskTemplateId}-${task.order}`}
151+
onSelect={() => setCurrentIndex(index)}
152+
>
153+
{taskTemplates[index]?.title ?? 'Task'}
154+
</DropdownMenuItem>
155+
))}
156+
</DropdownMenuContent>
157+
</DropdownMenu>
158+
</div>
159+
160+
<div className="bg-sarge-gray-0 flex rounded-r-lg">
161+
<Button
162+
variant="secondary"
163+
onClick={() => setCurrentIndex((prev) => Math.max(prev - 1, 0))}
164+
disabled={currentIndex === 0}
165+
className="bg-sarge-gray-0 size-9 rounded-lg border-none"
166+
>
167+
<ChevronLeft className="size-4 !text-black" />
168+
</Button>
169+
<Button
170+
variant="secondary"
171+
onClick={() =>
172+
setCurrentIndex((prev) => Math.min(prev + 1, totalTasks - 1))
173+
}
174+
disabled={currentIndex === totalTasks - 1}
175+
className="bg-sarge-gray-0 size-9 border-none"
176+
>
177+
<ChevronRight className="size-4 !text-black" />
178+
</Button>
179+
</div>
180+
</div>
181+
</div>
182+
</div>
183+
);
184+
}

0 commit comments

Comments
 (0)