From 31cf0a291dc3b6607f4f9402313ba3aaa2c33e59 Mon Sep 17 00:00:00 2001 From: RAJVEER42 Date: Tue, 2 Jun 2026 17:18:18 +0530 Subject: [PATCH 1/5] fix: surface schedule exception reasons in slot tooltip computeAppointmentSlots dropped conflicting slots and always returned isAvailable=true / exceptions=[], so ScheduleHome's hasExceptions was always false and the slot tooltip rendered empty. Attach overlapping exceptions to each slot and mark conflicting ones unavailable; the available-slot count is unchanged (filter(isAvailable)) and the tooltip now shows the exception reasons. See issue #16415. --- src/pages/Scheduling/utils.ts | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/pages/Scheduling/utils.ts b/src/pages/Scheduling/utils.ts index 0476c269291..e9c55126466 100644 --- a/src/pages/Scheduling/utils.ts +++ b/src/pages/Scheduling/utils.ts @@ -76,8 +76,7 @@ export function computeAppointmentSlots( while (time < endTime) { const slotEndTime = addMinutes(time, slotSizeInMinutes); - let conflicting = false; - for (const exception of exceptions) { + const slotExceptions = exceptions.filter((exception) => { const exceptionStartTime = parse( exception.start_time, "HH:mm:ss", @@ -89,20 +88,15 @@ export function computeAppointmentSlots( referenceDate, ); - if (exceptionStartTime < slotEndTime && exceptionEndTime > time) { - conflicting = true; - break; - } - } - - if (!conflicting) { - slots.push({ - start_time: format(time, "HH:mm") as Time, - end_time: format(slotEndTime, "HH:mm") as Time, - isAvailable: true, - exceptions: [], - }); - } + return exceptionStartTime < slotEndTime && exceptionEndTime > time; + }); + + slots.push({ + start_time: format(time, "HH:mm") as Time, + end_time: format(slotEndTime, "HH:mm") as Time, + isAvailable: slotExceptions.length === 0, + exceptions: slotExceptions, + }); time = slotEndTime; } From c7e6bcd9d57004cb7b059bb68916c5ef5ee1ebcb Mon Sep 17 00:00:00 2001 From: RAJVEER42 Date: Wed, 3 Jun 2026 18:51:51 +0530 Subject: [PATCH 2/5] refactor: gate unavailable slots behind a flag; pre-parse exceptions Addresses review feedback on computeAppointmentSlots: - Behavior compatibility: returning unavailable slots is now opt-in via `includeUnavailableSlots` (default false = previous "available slots only" behavior). ScheduleHome passes true so the tooltip can read the overlapping exceptions; any other caller is unaffected. - Performance: parse each exception's time window once outside the loop, determine availability with some() (early-exit on first overlap), and only build the overlapping-exception list for unavailable slots that are actually returned. --- src/components/Schedule/ScheduleHome.tsx | 1 + src/pages/Scheduling/utils.ts | 53 ++++++++++++++---------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/components/Schedule/ScheduleHome.tsx b/src/components/Schedule/ScheduleHome.tsx index 03e082fd748..76693d6bc87 100644 --- a/src/components/Schedule/ScheduleHome.tsx +++ b/src/components/Schedule/ScheduleHome.tsx @@ -437,6 +437,7 @@ function ScheduleTemplateAvailabilityItem({ availability, unavailableExceptions, date, + { includeUnavailableSlots: true }, ); const availableSlots = computedSlots.filter( diff --git a/src/pages/Scheduling/utils.ts b/src/pages/Scheduling/utils.ts index e9c55126466..e2080853be8 100644 --- a/src/pages/Scheduling/utils.ts +++ b/src/pages/Scheduling/utils.ts @@ -58,6 +58,9 @@ export function computeAppointmentSlots( }, exceptions: ScheduleException[], referenceDate: Date = new Date(), + { + includeUnavailableSlots = false, + }: { includeUnavailableSlots?: boolean } = {}, ) { const startTime = parse( availability.availability[0].start_time, @@ -72,31 +75,37 @@ export function computeAppointmentSlots( 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: parse(exception.start_time, "HH:mm:ss", referenceDate), + end: parse(exception.end_time, "HH:mm:ss", referenceDate), + })); + let time = startTime; while (time < endTime) { const slotEndTime = addMinutes(time, slotSizeInMinutes); - - const slotExceptions = exceptions.filter((exception) => { - const exceptionStartTime = parse( - exception.start_time, - "HH:mm:ss", - referenceDate, - ); - const exceptionEndTime = parse( - exception.end_time, - "HH:mm:ss", - referenceDate, - ); - - return exceptionStartTime < slotEndTime && exceptionEndTime > time; - }); - - slots.push({ - start_time: format(time, "HH:mm") as Time, - end_time: format(slotEndTime, "HH:mm") as Time, - isAvailable: slotExceptions.length === 0, - exceptions: slotExceptions, - }); + const start_time = format(time, "HH:mm") as Time; + const end_time = format(slotEndTime, "HH:mm") as Time; + + // some() short-circuits on the first overlapping exception. + const isAvailable = !parsedExceptions.some( + ({ start, end }) => start < slotEndTime && end > time, + ); + + if (isAvailable) { + slots.push({ start_time, end_time, isAvailable: true, exceptions: [] }); + } else if (includeUnavailableSlots) { + // Only build the overlapping list for slots we actually return. + slots.push({ + start_time, + end_time, + isAvailable: false, + exceptions: parsedExceptions + .filter(({ start, end }) => start < slotEndTime && end > time) + .map(({ exception }) => exception), + }); + } time = slotEndTime; } From 846ae6d94c1b810f11878218e44867fb23b05506 Mon Sep 17 00:00:00 2001 From: RAJVEER42 Date: Wed, 3 Jun 2026 19:01:00 +0530 Subject: [PATCH 3/5] refactor: single-pass overlap check + named options type Address review feedback: - Compute each slot's overlapping exceptions in a single filter pass and derive isAvailable from its length, instead of a some() check followed by a separate filter for unavailable slots. - Extract a named ComputeAppointmentSlotsOptions interface for the options parameter instead of an inline anonymous type. --- src/pages/Scheduling/utils.ts | 43 ++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/src/pages/Scheduling/utils.ts b/src/pages/Scheduling/utils.ts index e2080853be8..7b03d95abd0 100644 --- a/src/pages/Scheduling/utils.ts +++ b/src/pages/Scheduling/utils.ts @@ -52,15 +52,22 @@ type VirtualSlot = { exceptions: ScheduleException[]; }; +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, - }: { includeUnavailableSlots?: boolean } = {}, + { includeUnavailableSlots = false }: ComputeAppointmentSlotsOptions = {}, ) { const startTime = parse( availability.availability[0].start_time, @@ -85,25 +92,19 @@ export function computeAppointmentSlots( let time = startTime; while (time < endTime) { const slotEndTime = addMinutes(time, slotSizeInMinutes); - const start_time = format(time, "HH:mm") as Time; - const end_time = format(slotEndTime, "HH:mm") as Time; - - // some() short-circuits on the first overlapping exception. - const isAvailable = !parsedExceptions.some( - ({ start, end }) => start < slotEndTime && end > time, - ); - - if (isAvailable) { - slots.push({ start_time, end_time, isAvailable: true, exceptions: [] }); - } else if (includeUnavailableSlots) { - // Only build the overlapping list for slots we actually return. + + // Single pass: collect overlapping exceptions, then derive availability. + const slotExceptions = parsedExceptions + .filter(({ start, end }) => start < slotEndTime && end > time) + .map(({ exception }) => exception); + const isAvailable = slotExceptions.length === 0; + + if (isAvailable || includeUnavailableSlots) { slots.push({ - start_time, - end_time, - isAvailable: false, - exceptions: parsedExceptions - .filter(({ start, end }) => start < slotEndTime && end > time) - .map(({ exception }) => exception), + start_time: format(time, "HH:mm") as Time, + end_time: format(slotEndTime, "HH:mm") as Time, + isAvailable, + exceptions: slotExceptions, }); } From 48f28a0e22d56927b4ea6d3d3ee2f815439b63b6 Mon Sep 17 00:00:00 2001 From: RAJVEER42 Date: Wed, 3 Jun 2026 19:11:12 +0530 Subject: [PATCH 4/5] fix: parse schedule times tolerant of HH:mm and HH:mm:ss The exception time inputs produce HH:mm values (e.g. "00:00"/"23:59"), so hardcoding parse(..., "HH:mm:ss") would yield Invalid Date and break the overlap checks. Add a parseScheduleTime helper that selects the format by length, and use it for both availability and exception times. (The single-pass overlap from the previous commit already keeps isAvailable and the attached exceptions derived from one computation.) --- src/pages/Scheduling/utils.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/pages/Scheduling/utils.ts b/src/pages/Scheduling/utils.ts index 7b03d95abd0..f414f5526d8 100644 --- a/src/pages/Scheduling/utils.ts +++ b/src/pages/Scheduling/utils.ts @@ -52,6 +52,12 @@ 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); + interface ComputeAppointmentSlotsOptions { /** * When true, slots blocked by an exception are returned with @@ -69,14 +75,12 @@ export function computeAppointmentSlots( 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; @@ -85,8 +89,8 @@ export function computeAppointmentSlots( // Parse each exception's time window once instead of for every slot. const parsedExceptions = exceptions.map((exception) => ({ exception, - start: parse(exception.start_time, "HH:mm:ss", referenceDate), - end: parse(exception.end_time, "HH:mm:ss", referenceDate), + start: parseScheduleTime(exception.start_time, referenceDate), + end: parseScheduleTime(exception.end_time, referenceDate), })); let time = startTime; From 6592ad86d14998406dfe81ffbd0af82a9712fcd7 Mon Sep 17 00:00:00 2001 From: RAJVEER42 Date: Wed, 3 Jun 2026 19:15:23 +0530 Subject: [PATCH 5/5] refactor: detect time format by colon segments, not string length Use value.trim().split(":").length to choose HH:mm vs HH:mm:ss so format selection is structural (and tolerant of surrounding whitespace) rather than relying on the brittle string-length heuristic. --- src/pages/Scheduling/utils.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/pages/Scheduling/utils.ts b/src/pages/Scheduling/utils.ts index f414f5526d8..f3e1515254d 100644 --- a/src/pages/Scheduling/utils.ts +++ b/src/pages/Scheduling/utils.ts @@ -53,10 +53,14 @@ type VirtualSlot = { }; // 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); +// which sets values like "00:00"/"23:59") or HH:mm:ss; pick the format from the +// number of colon-separated segments (after trimming) so neither variant +// produces an Invalid Date. +const parseScheduleTime = (value: Time, referenceDate: Date) => { + const trimmed = value.trim(); + const pattern = trimmed.split(":").length > 2 ? "HH:mm:ss" : "HH:mm"; + return parse(trimmed, pattern, referenceDate); +}; interface ComputeAppointmentSlotsOptions { /**