The calendar feature lets a trainee pick a free slot from the coach’s availability and cancel an upcoming meeting. It is one screen, ScheduleMeetingScreen, at the route /calendar.

Files

Entry points

The calendar is reachable only when user.calendarEnabled !== false on the signed-in user in useAuthStore.
  • Classic home: the calendar button in HomeHero pushes /calendar.
  • Themed home screens: useHomeModel() exposes calendarEnabled and openCalendar(), and each theme draws its own button.
The route itself does not check the flag. Only the entry points do.

Endpoints

getAvailableSlots always asks for a fixed window. from is now and to is now plus SLOT_RANGE_DAYS = 14 days. Both are sent as full ISO timestamps from Date.toISOString(), URL-encoded.
The booking body is built entirely from the chosen slot. The trainee enters nothing.

Hooks

Invalidating both keys after either mutation is what removes a booked slot from the list and returns a cancelled one to it. The app does no optimistic update.

Screen layout

ScheduleMeetingScreen uses Screen with scroll and pull to refresh, which refetches slots and meetings together. The header is the shared TrackHeader from the tracking feature with a back button. While either query is loading, the screen shows skeletons. After that it has two sections.

Upcoming meetings

The list is filtered on the device:
Past meetings and cancelled meetings are never shown. The section is hidden when the filtered list is empty. MeetingCard shows the type label (training, conversation, or a generic meeting label for OTHER), the day heading and the time range. title and location are on the type but the card does not render them. A cancel button appears when the meeting is in the future and its status is CONFIRMED.

Available slots

groupSlotsByDay(slots) groups slots under the trainee’s local calendar day:
  • The group key is localIsoDate(new Date(slot.startsAt)), so a slot is grouped by the device’s date, not the server’s.
  • Groups keep the order in which their first slot arrives. Slots are not re-sorted on the device.
  • The heading is formatDayHeading(startsAt): long weekday, day number and long month in the app language (getLanguageTag()).
SlotGroup renders one tonal Button per slot labelled with formatTime(startsAt) (two-digit hour and minute in the app language). The duration and slot type are not shown on the button. With no slots at all, the section shows an empty-state line.

Booking flow

1

Pick a slot

Tapping a slot button calls confirmBook(slot).
2

Confirm

A native Alert shows the day heading and start time with cancel and confirm actions.
3

Book

On confirm, the screen stores slotKey(slot) in pendingSlot, which disables that one button, then awaits book.mutateAsync(...) with coachId, slotType, startsAt and endsAt copied from the slot.
4

Result

Success shows a toast with the booked string. Any error shows the booking-failed string. pendingSlot is cleared in both cases. The invalidated queries then refetch, so the new meeting appears under upcoming meetings and the slot leaves the list.
slotKey is startsAt joined with slotType. It is used as the React key and as the pending marker. Two coaches offering the same start time and slot type would share a key.

Cancel flow

The cancel button opens a destructive confirmation Alert. On confirm, the screen stores the meeting id in pendingCancel, which disables the button, and awaits cancel.mutateAsync(meeting.id). Success shows the cancelled toast. An error shows the generic error string. After the refetch the meeting drops out of the upcoming list because its status is now CANCELLED.

Things to know

  • Error handling is generic. The screen does not read the server error code, so a slot taken by someone else in the meantime shows the same booking-failed toast as a network failure.
  • ScheduleMeetingScreen and MeetingCard import the static colors object from @/theme instead of calling useColors(), unlike most newer screens. See Theming and branding before changing colours here.
  • There is no reschedule action. The trainee cancels and books again.