feat: extend schedule to 9 PM and make appointments span multiple rows by duration
- Time slots now run 8:00 AM – 9:00 PM (was 8:00 AM – 6:00 PM) - Appointments visually span the correct number of 15-min rows based on startTime/endTime using HTML rowSpan - Covered rows are skipped so the grid layout stays consistent Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string>();
|
||||
(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() {
|
||||
<td className="border px-2 py-1 text-xs text-gray-600 font-medium">
|
||||
{timeSlot.displayTime}
|
||||
</td>
|
||||
{staffMembers.map((staff, staffIndex) => (
|
||||
<DroppableTimeSlot
|
||||
key={`${timeSlot.time}-${staff.id}`}
|
||||
timeSlot={timeSlot}
|
||||
staffId={Number(staff.id)}
|
||||
staffIndex={staffIndex}
|
||||
appointment={getAppointmentAtSlot(
|
||||
timeSlot,
|
||||
Number(staff.id)
|
||||
)}
|
||||
staff={staff}
|
||||
/>
|
||||
))}
|
||||
{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 (
|
||||
<DroppableTimeSlot
|
||||
key={`${timeSlot.time}-${staff.id}`}
|
||||
timeSlot={timeSlot}
|
||||
staffId={Number(staff.id)}
|
||||
staffIndex={staffIndex}
|
||||
appointment={apt}
|
||||
staff={staff}
|
||||
rowSpan={span}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user