diff --git a/apps/Frontend/src/pages/appointments-page.tsx b/apps/Frontend/src/pages/appointments-page.tsx index 0f89af05..aaa5153a 100755 --- a/apps/Frontend/src/pages/appointments-page.tsx +++ b/apps/Frontend/src/pages/appointments-page.tsx @@ -323,15 +323,15 @@ export default function AppointmentsPage() { ); }; - // Generate time slots from 8:00 AM to 6:00 PM in 15-minute increments + // Generate time slots from 8:00 AM to 9:00 PM in 15-minute increments const timeSlots: TimeSlot[] = []; - for (let hour = 8; hour <= 18; hour++) { + for (let hour = 8; hour <= 21; hour++) { for (let minute = 0; minute < 60; minute += 15) { const pad = (n: number) => n.toString().padStart(2, "0"); const timeStr = `${pad(hour)}:${pad(minute)}`; - // Only allow start times up to 18:00 (last start for 30-min appointment) - if (timeStr > "18:00") continue; + // Only allow start times up to 21:00 (9 PM) + if (timeStr > "21:00") continue; const hour12 = hour > 12 ? hour - 12 : hour; const period = hour >= 12 ? "PM" : "AM"; @@ -585,6 +585,35 @@ export default function AppointmentsPage() { return processed; }); + // Compute how many 15-min slots an appointment occupies + const getSlotSpan = (apt: ScheduledAppointment): number => { + const startStr = (typeof apt.startTime === "string" ? apt.startTime : formatLocalTime(apt.startTime)).substring(0, 5); + const endStr = (typeof apt.endTime === "string" ? apt.endTime : formatLocalTime(apt.endTime)).substring(0, 5); + const startParts = startStr.split(":"); + const endParts = endStr.split(":"); + const startH = parseInt(startParts[0] ?? "0", 10); + const startM = parseInt(startParts[1] ?? "0", 10); + const endH = parseInt(endParts[0] ?? "0", 10); + const endM = parseInt(endParts[1] ?? "0", 10); + const diff = (endH * 60 + endM) - (startH * 60 + startM); + return Math.max(1, Math.round(diff / 15)); + }; + + // Slots that are "continued" rows of a multi-slot appointment (should not render a td) + const coveredSlots = new Set(); + (processedAppointments ?? []).forEach((apt) => { + const span = getSlotSpan(apt); + if (span <= 1) return; + const startStr = (typeof apt.startTime === "string" ? apt.startTime : formatLocalTime(apt.startTime)).substring(0, 5); + const [startH, startM] = startStr.split(":").map(Number); + for (let i = 1; i < span; i++) { + const totalMin = (startH ?? 0) * 60 + (startM ?? 0) + i * 15; + const h = Math.floor(totalMin / 60).toString().padStart(2, "0"); + const m = (totalMin % 60).toString().padStart(2, "0"); + coveredSlots.add(`${h}:${m}-${apt.staffId}`); + } + }); + // Check if appointment exists at a specific time slot and staff const getAppointmentAtSlot = (timeSlot: TimeSlot, staffId: number) => { if (!processedAppointments || processedAppointments.length === 0) @@ -800,12 +829,14 @@ export default function AppointmentsPage() { staffIndex, appointment, staff, + rowSpan = 1, }: { timeSlot: TimeSlot; staffId: number; staffIndex: number; appointment: ScheduledAppointment | undefined; staff: Staff; + rowSpan?: number; }) { const blocked = !isWithinOfficeHours(timeSlot.time, staffIndex); @@ -841,6 +872,7 @@ export default function AppointmentsPage() { className={`px-1 py-1 border relative h-14 ${ isOver && canDrop ? "bg-green-100" : blocked ? "bg-gray-100" : "" }`} + rowSpan={rowSpan > 1 ? rowSpan : undefined} title={blocked ? "Outside office hours — click to override" : undefined} > {appointment ? ( @@ -1573,19 +1605,22 @@ export default function AppointmentsPage() { {timeSlot.displayTime} - {staffMembers.map((staff, staffIndex) => ( - - ))} + {staffMembers.map((staff, staffIndex) => { + if (coveredSlots.has(`${timeSlot.time}-${staff.id}`)) return null; + const apt = getAppointmentAtSlot(timeSlot, Number(staff.id)); + const span = apt ? getSlotSpan(apt) : 1; + return ( + + ); + })} ))}