Skip to content
1 change: 1 addition & 0 deletions src/components/Schedule/ScheduleHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@ function ScheduleTemplateAvailabilityItem({
availability,
unavailableExceptions,
date,
{ includeUnavailableSlots: true },
);

const availableSlots = computedSlots.filter(
Expand Down
58 changes: 33 additions & 25 deletions src/pages/Scheduling/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,55 +52,63 @@ type VirtualSlot = {
exceptions: ScheduleException[];
};

// Schedule times may arrive as HH:mm (e.g. the exception form's time input,
// which sets values like "00:00"/"23:59") or HH:mm:ss; parse with the matching
// format so neither variant produces an Invalid Date.
const parseScheduleTime = (value: Time, referenceDate: Date) =>
parse(value, value.length > 5 ? "HH:mm:ss" : "HH:mm", referenceDate);
Comment thread
Valyrian-Code marked this conversation as resolved.
Outdated

interface ComputeAppointmentSlotsOptions {
/**
* When true, slots blocked by an exception are returned with
* `isAvailable: false` and their overlapping exceptions, instead of being
* omitted. Defaults to false (available slots only).
*/
includeUnavailableSlots?: boolean;
}

export function computeAppointmentSlots(
availability: ScheduleAvailability & {
slot_type: AvailabilitySlotType.Appointment;
},
exceptions: ScheduleException[],
referenceDate: Date = new Date(),
{ includeUnavailableSlots = false }: ComputeAppointmentSlotsOptions = {},
) {
const startTime = parse(
const startTime = parseScheduleTime(
availability.availability[0].start_time,
"HH:mm:ss",
referenceDate,
);
const endTime = parse(
const endTime = parseScheduleTime(
availability.availability[0].end_time,
"HH:mm:ss",
referenceDate,
);
const slotSizeInMinutes = availability.slot_size_in_minutes;
const slots: VirtualSlot[] = [];

// Parse each exception's time window once instead of for every slot.
const parsedExceptions = exceptions.map((exception) => ({
exception,
start: parseScheduleTime(exception.start_time, referenceDate),
end: parseScheduleTime(exception.end_time, referenceDate),
}));
Comment thread
Valyrian-Code marked this conversation as resolved.

let time = startTime;
while (time < endTime) {
const slotEndTime = addMinutes(time, slotSizeInMinutes);

let conflicting = false;
for (const exception of exceptions) {
const exceptionStartTime = parse(
exception.start_time,
"HH:mm:ss",
referenceDate,
);
const exceptionEndTime = parse(
exception.end_time,
"HH:mm:ss",
referenceDate,
);

if (exceptionStartTime < slotEndTime && exceptionEndTime > time) {
conflicting = true;
break;
}
}
// Single pass: collect overlapping exceptions, then derive availability.
const slotExceptions = parsedExceptions
.filter(({ start, end }) => start < slotEndTime && end > time)
.map(({ exception }) => exception);
Comment thread
Valyrian-Code marked this conversation as resolved.
Comment thread
Valyrian-Code marked this conversation as resolved.
const isAvailable = slotExceptions.length === 0;

if (!conflicting) {
if (isAvailable || includeUnavailableSlots) {
slots.push({
start_time: format(time, "HH:mm") as Time,
end_time: format(slotEndTime, "HH:mm") as Time,
isAvailable: true,
exceptions: [],
isAvailable,
exceptions: slotExceptions,
});
}

Expand Down
Loading