From d2eb0d425d70f320ee75579d40b401e2011665cf Mon Sep 17 00:00:00 2001 From: Felicia Paulus Date: Mon, 21 Jul 2025 19:23:19 +0800 Subject: [PATCH 1/2] Enhance ExamTable component with conflict detection and tooltip support for overlapping exams --- .../timetable/components/ExamTable.tsx | 129 ++++++++++++------ src/components/timetable/utils/examUtils.ts | 46 +++++++ 2 files changed, 133 insertions(+), 42 deletions(-) create mode 100644 src/components/timetable/utils/examUtils.ts diff --git a/src/components/timetable/components/ExamTable.tsx b/src/components/timetable/components/ExamTable.tsx index 6f06758..4fca21f 100644 --- a/src/components/timetable/components/ExamTable.tsx +++ b/src/components/timetable/components/ExamTable.tsx @@ -1,4 +1,5 @@ import { format } from "date-fns"; +import { AlertTriangle } from "lucide-react"; import type { Module } from "@/types/primitives/module"; import type { TimetableThemeName } from "@/utils/timetable/colours"; @@ -10,8 +11,18 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; import { TIMETABLE_THEMES } from "@/utils/timetable/colours"; +import type { ModuleWithExam } from "../utils/examUtils"; +import { findExamConflicts } from "../utils/examUtils"; + interface ExamTableProps { modules: (Module & { colorIndex: number; visible: boolean })[]; timetableTheme: TimetableThemeName; @@ -20,13 +31,22 @@ interface ExamTableProps { export function ExamTable({ modules, timetableTheme }: ExamTableProps) { // Filter modules that have exams and sort by date const modulesWithExams = modules - .filter((mod) => mod.exam?.dateTime && mod.visible) + .filter( + (mod): mod is ModuleWithExam & typeof mod => + !!mod.exam?.dateTime && mod.visible, + ) .sort((a, b) => { const dateA = new Date(a.exam!.dateTime); const dateB = new Date(b.exam!.dateTime); return dateA.getTime() - dateB.getTime(); }); + const conflictingModules = findExamConflicts(modulesWithExams); + + const moduleCodeToNameMap = new Map( + modulesWithExams.map((mod) => [mod.moduleCode, mod.name]), + ); + if (modulesWithExams.length === 0) { return (
@@ -36,47 +56,72 @@ export function ExamTable({ modules, timetableTheme }: ExamTableProps) { } return ( -
- - - - Module Code - Module Name - Exam Date & Time - Duration - - - - {modulesWithExams.map((mod) => ( - - -
-
- {mod.moduleCode} -
- - {mod.name} - - {format( - new Date(mod.exam!.dateTime), - "MMM dd, yyyy 'at' h:mm a", - )} - - - {mod.exam!.durationInHour} hour - {mod.exam!.durationInHour !== 1 ? "s" : ""} - + +
+
+ + + Module Code + Module Name + Exam Date & Time + Duration - ))} - -
-
+ + + {modulesWithExams.map((mod) => { + const conflicts = conflictingModules.get(mod.moduleCode); + const isConflicting = !!conflicts; + + return ( + + +
+
+ {mod.moduleCode} + {isConflicting && ( + + + + + +

+ Clashes with:{" "} + {conflicts + ?.map((code) => moduleCodeToNameMap.get(code)) + .join(", ")} +

+
+
+ )} +
+ + {mod.name} + + {format( + new Date(mod.exam!.dateTime), + "MMM dd, yyyy 'at' h:mm a", + )} + + + {mod.exam!.durationInHour} hour + {mod.exam!.durationInHour !== 1 ? "s" : ""} + + + ); + })} + + +
+ ); } diff --git a/src/components/timetable/utils/examUtils.ts b/src/components/timetable/utils/examUtils.ts new file mode 100644 index 0000000..a8cf051 --- /dev/null +++ b/src/components/timetable/utils/examUtils.ts @@ -0,0 +1,46 @@ +import type { Module } from "@/types/primitives/module"; + +export type ModuleWithExam = Omit & { + exam: NonNullable; +}; + +export function findExamConflicts( + modulesWithExams: ModuleWithExam[], +): Map { + const conflictingModules = new Map(); + + if (modulesWithExams.length < 2) { + return conflictingModules; + } + + // Assuming modulesWithExams is sorted by exam start time. + for (let i = 0; i < modulesWithExams.length; i++) { + const moduleA = modulesWithExams[i]; + const startA = new Date(moduleA.exam.dateTime); + const endA = new Date( + startA.getTime() + moduleA.exam.durationInHour * 60 * 60 * 1000, + ); + + for (let j = i + 1; j < modulesWithExams.length; j++) { + const moduleB = modulesWithExams[j]; + const startB = new Date(moduleB.exam.dateTime); + + // Check for overlap + if (startB < endA) { + const conflictsForA = conflictingModules.get(moduleA.moduleCode) || []; + if (!conflictsForA.includes(moduleB.moduleCode)) { + conflictsForA.push(moduleB.moduleCode); + } + conflictingModules.set(moduleA.moduleCode, conflictsForA); + + const conflictsForB = conflictingModules.get(moduleB.moduleCode) || []; + if (!conflictsForB.includes(moduleA.moduleCode)) { + conflictsForB.push(moduleA.moduleCode); + } + conflictingModules.set(moduleB.moduleCode, conflictsForB); + } + } + } + + return conflictingModules; +} \ No newline at end of file From 7f8e244d563a7dd97e393543202744899317ec26 Mon Sep 17 00:00:00 2001 From: Felicia Paulus Date: Mon, 21 Jul 2025 19:43:09 +0800 Subject: [PATCH 2/2] prettier formatting --- src/components/timetable/components/ExamTable.tsx | 10 +++++----- src/components/timetable/utils/examUtils.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/timetable/components/ExamTable.tsx b/src/components/timetable/components/ExamTable.tsx index 4fca21f..438acb2 100644 --- a/src/components/timetable/components/ExamTable.tsx +++ b/src/components/timetable/components/ExamTable.tsx @@ -36,8 +36,8 @@ export function ExamTable({ modules, timetableTheme }: ExamTableProps) { !!mod.exam?.dateTime && mod.visible, ) .sort((a, b) => { - const dateA = new Date(a.exam!.dateTime); - const dateB = new Date(b.exam!.dateTime); + const dateA = new Date(a.exam.dateTime); + const dateB = new Date(b.exam.dateTime); return dateA.getTime() - dateB.getTime(); }); @@ -108,13 +108,13 @@ export function ExamTable({ modules, timetableTheme }: ExamTableProps) { {mod.name} {format( - new Date(mod.exam!.dateTime), + new Date(mod.exam.dateTime), "MMM dd, yyyy 'at' h:mm a", )} - {mod.exam!.durationInHour} hour - {mod.exam!.durationInHour !== 1 ? "s" : ""} + {mod.exam.durationInHour} hour + {mod.exam.durationInHour !== 1 ? "s" : ""}
); diff --git a/src/components/timetable/utils/examUtils.ts b/src/components/timetable/utils/examUtils.ts index a8cf051..c55c91f 100644 --- a/src/components/timetable/utils/examUtils.ts +++ b/src/components/timetable/utils/examUtils.ts @@ -43,4 +43,4 @@ export function findExamConflicts( } return conflictingModules; -} \ No newline at end of file +}