Dentolize · Appointment Availability Crash Fix Walkthrough
On this pageBusiness viewTechnical viewA gap this PR does not close

The three guards, in detail

Business view

Think of each fix as a "wait, do I actually have what I need?" check added right before the app tries to use branch data. If the check fails (data isn't there yet), the screen now shows a safe placeholder — an empty list, an "open all day" default, or simply nothing — rather than crashing. As soon as the real data arrives, the screen updates normally. The user experience in the common case (data already cached, which is most of the time) is identical to before; the difference only shows up in that first-load window.

Technical view

1. getStartAndEndTimes — the shared low-level helper

packages/clinic-mobile/src/components/dashboard/calendar/newAppointment/calendarHelpers.js:24-27

export const getStartAndEndTimes = (value, branchData, moment) => {
  if (!branchData?.branchDetails) {
    return { starts: 0, ends: 24, startsMin: 0, endsMin: 0 }
  }
  ...

When branch data isn't available, this now returns a permissive default — "open from hour 0 to hour 24" — rather than reading .opens[dayIndex] off undefined. This function is exported from the mobile package and consumed by three different screens: the web CheckRepeatTimesAvailabilityButton, web MonthlyCalendar, and web MultipleTimesButton, plus the mobile AvailableSlotsScreen and MonthlyCalendarScreen. Fixing it here fixes it everywhere it's called from — the highest-leverage change in this PR.

2. CheckRepeatTimesAvailabilityButton — the "Available Slots" button

packages/clinic-web/src/components/dashboard/appointments/AppointmentForm/CheckRepeatTimesAvailabilityButton.js:99-107

const parsedBranchData = branchData
  ? {
      ...branchData,
      branchDetails: {
        ...branchData.branchDetails,
        breaks: branchData.branchDetails.breaks ? JSON.parse(branchData.branchDetails.breaks) : []
      }
    }
  : {}

Previously this object was built unconditionally by spreading branchData.branchDetails, which crashed as soon as the component rendered with branchData still undefined — independent of whether its modal was even open. The guard falls back to an empty object, matching the pattern already used in the sibling MultipleTimesButton.js:44-56.

3. MonthlyCalendar — the month-view picker

packages/clinic-web/src/components/dashboard/appointments/AppointmentForm/MonthlyCalendar.js:61-101

Two related changes in the same useMemo:

const dataByDay = useMemo(() => {
  if (!data || !branchData) return []   // was: if (!data) return []
  ...
}, [data, doctor, room, interval, branchData])  // branchData added to deps

This is two bugs fixed together, both necessary:

  • The crash: the early-return guard now also checks branchData, not

just data. DATE_RANGE_APPOINTMENTS (data) and BRANCH_DETAILS (branchData) are two independent queries fired in parallel when the month picker opens; without this, a common race — data resolving before branchData — reliably crashed the grid.

  • The staleness bug: branchData was read inside the memo but wasn't

listed as a dependency, so even once it did arrive, React had no reason to recompute dataByDay — the grid would have been stuck showing whatever it computed the first time (previously: a crash; with only the first fix and not this one: permanently empty). Both fixes together are what make the month view self-heal once branch data lands.

A gap this PR does not close

getAvailableSlots and isSlotAvailable, in the same calendarHelpers.js file (lines 77-117 and 119-162), still read branchData.branchDetails.breaks[...] and branchData.branchDetails.openDays unconditionally — no optional chaining, no guard. CheckRepeatTimesAvailabilityButton calls both of these once its "Available Slots" modal is showing results (CheckRepeatTimesAvailabilityButton.js:133-181), passing the same parsedBranchData that this PR taught to fall back to {} rather than crash immediately. If branchData is still unresolved by the time that code path runs — the user clicked "Available Slots" and it doesn't crash, but branchData hasn't caught up yet — parsedBranchData is {}, and getAvailableSlots/isSlotAvailable will still throw on {}.branchDetails.openDays. In practice this is unlikely, because the DATE_RANGE_APPOINTMENTS fetch this button waits on tends to give BRANCH_DETAILS enough time to resolve first — but it is a real, unguarded edge case, not a resolved one. See For Quality for how to probe it.