Skip to main content
Synced from the repo — do not edit here

Canonical source: docs/claude/client-reschedule.md. This page is generated by docs/scripts/sync-handbook.mjs. Edit the source file in the repo; changes appear here on the next build.

Client-App Rescheduling

Lets clients reschedule their own bookings from basishybrid (and basisweb later) instead of calling the clinic. Configurable per-clinic, with per-service overrides.

Where basishybrid reads the configuration

All reschedule configuration lives in Firestore, written by basisflow-web's settings UI. basishybrid is a read-only consumer. Do not duplicate the rules in the Flutter codebase.

Two reads on the clinic the user is currently scoped to:

  1. clinicsv2/{clinicId} — root doc. Read field clientAppReschedule (object with enabled, cutoffMinutesBefore, sameDayOnly). Use a snapshot listener so changes from staff in basisflow-web reflect live.
  2. clinicsv2/{clinicId}/preferences/clientApp — subdoc. Read field serviceReschedulePolicies (map of serviceId'allow' | 'disallow').

If clientAppReschedule.enabled is missing/false and there is no per-service override of 'allow' for the event's service, show "Call the clinic to reschedule" instead of the Reschedule button (no in-app reschedule action).

If enabled === true (or the service has 'allow' override), show the Reschedule button and apply the cutoff + same-day rules from the same doc.

If the service has 'disallow' override, show "Call the clinic to reschedule" regardless of the org-wide toggle.

Effective-rule resolver (paste into Dart as the source of truth)

bool canClientReschedule({
required bool orgEnabled,
required Map<String, String> perServiceOverrides, // serviceId -> 'allow'|'disallow'
required String serviceId,
}) {
final override = perServiceOverrides[serviceId];
if (override == 'allow') return true;
if (override == 'disallow') return false;
return orgEnabled; // inherit
}

Reschedule UI gating

enabledper-service overrideWhat basishybrid shows
any value'disallow'"Call the clinic to reschedule"
false / missingnone"Call the clinic to reschedule"
false / missing'allow'Reschedule button (with the org's cutoff/same-day rules — these always apply when allowed)
truenoneReschedule button
true'allow'Reschedule button (same as inherit-true)

In all "Call the clinic" cases, basishybrid should show the clinic's phone number / contact info inline if available, not just a dead-end message.

Status as of writing

PieceStatus
Settings UI in basisflow-webShipped
Per-service override in marketplace service editorShipped
Backend reschedule_appointment handlerNot built
basishybrid UINot built
basisweb (client portal) UINot built

The settings UI saves to Firestore and the data model is locked in. What's left is the backend handler and the client UIs that call it.

Data model

Org-wide settings — clinicsv2/{clinicId}.clientAppReschedule

{
enabled: boolean; // master switch
cutoffMinutesBefore: number; // how close to original start time clients
// can still act. 0 = up to start time.
sameDayOnly: boolean; // destination slot must be same calendar
// day as the original appointment
}

Stored as a nested field on the clinic doc using dot-notation updateDoc. Read with data.clientAppReschedule ?? {}. Defaults if missing: enabled=false, cutoffMinutesBefore=60, sameDayOnly=false.

The "cutoff" and "same-day" are independent: cutoff restricts when the client can act; same-day restricts where they can move to.

Per-service override — clinicsv2/{clinicId}/preferences/clientApp.serviceReschedulePolicies

{
[serviceId: string]: 'allow' | 'disallow';
}

Three logical states per service:

  • Inherit (default) — no entry in the map; service follows the org-wide enabled flag
  • Allow'allow'; service is reschedulable even when org-wide is off
  • Disallow'disallow'; service is locked even when org-wide is on

Stored in preferences/clientApp subcollection (same doc that holds visible metrics/labs). When the dropdown is set to "Inherit," the entry is deleted from the map (not stored as 'inherit').

Effective rule for a given event

See the resolver in the "Where basishybrid reads the configuration" section above. Apply this before showing a Reschedule button in basishybrid. Backend reschedule_appointment re-validates it server-side using the same logic against the same Firestore docs — basishybrid never sends rules to the backend, the backend reads them directly.

Backend contract (to be built)

Request type

request_type: 'reschedule_appointment'

Request shape

{
clinic_id: string;
event_id: string;
new_start: string; // ISO datetime
new_staff_id?: string; // optional, lets client pick a different coach
// from the available slot's offer set
}

Same service only — no new_service_id in v1.

Server-side validation (must run on the server, not just frontend)

  1. Load the event. Confirm event.uid === caller.uid (client can only reschedule their own bookings). Staff use update_event_details directly.
  2. Load org clientAppReschedule settings + serviceReschedulePolicies from prefs.
  3. Compute the effective policy for event.service. If not allowed → return permission-denied with a clear message.
  4. Cutoff check: now > event.start - cutoffMinutesBefore * 60000failed-precondition "Too close to appointment time."
  5. Same-day check (if enabled): new_start.date() !== event.start.date() (in the clinic's timezone) → failed-precondition "Reschedule must be on the same day."
  6. Slot validity: re-run availability check for event.service at new_start for the event's location. If the slot isn't currently bookable → failed-precondition "Slot not available."
  7. Coach pick: if new_staff_id given, verify they're one of the coaches available for that slot.

If all pass, delegate to update_event_details with new_start, optional new_staff_id, and a note flagging the change for the activity log.

Response

Return the updated event (same shape as update_event_details).

Implementation hint

Don't write a parallel reschedule engine. Call update_event_details under the hood after validation — it already handles attendee shuffling, time change, activity-log entry, and notifications.

Notifications

Should match the existing reschedule path's behavior — staff receives a calendar update; client receives confirmation. The notification toggle that staff have in their reschedule modal doesn't apply here (the client is the one doing it).

basishybrid UI requirements

Add a "Reschedule" button on the appointment detail screen. Visible only when canClientReschedule(org, perService, event.service) === true.

When tapped:

  1. Cutoff guard upfront: if now > event.start - cutoffMinutesBefore * 60000, show "It's too close to your appointment to reschedule. Please contact the clinic." Don't open the picker.
  2. Date picker — initial date = event's date. If sameDayOnly, lock to that day.
  3. Time/coach picker — call getAvailableServiceTimes(clinic, event.service, event.location, dayStart, dayEnd). Show only slots where available === true (use the availableSlots array, not slots). For each slot, surface the coach if known.
  4. Confirm screen — show "From {old time} → To {new time}" with the new coach name.
  5. Submit — call reschedule_appointment. On success, navigate back and refresh. On failed-precondition (cutoff race / slot taken since rendered), show the error message verbatim — the backend's messages are user-facing.

Disabled / hidden states

  • Org toggle off + service is "inherit" → hide button entirely.
  • Service has override disallow → hide button entirely.
  • Past cutoff → show button but tapping shows the "too close" message.

What clients can change

  • Time (new start)
  • Coach (only among coaches available for that slot)

What they can't change in v1:

  • Service (cross-service reschedule is a future feature, would need allowed-targets matrix)
  • Location
  • Duration

Edge cases

  • Client books and immediately reschedules: cutoff and slot-validity checks still apply.
  • Slot becomes unavailable between picker render and submit: backend re-validates and returns clear error; surface verbatim.
  • Group services: same flow. The slot's capacity is checked server-side; if full when they submit, return failed-precondition. If hasWaitlist, the reschedule path should NOT silently waitlist — fail with "Slot was just filled."
  • Membership credit / payment: the original booking's payment carries to the new event. update_event_details already preserves the event's payment attachments since it's an in-place update. No refund/recharge.
  • Recurring memberships with bookable windows: the new slot must also be inside the member's allowed booking window. Reuse the existing booking eligibility check.

Where the existing settings UI lives

  • app/(main)/settings/page.tsx → ClientAppTab → "Booking" section.
  • app/(main)/marketplace/page.tsx → ServiceModal → "Client app rescheduling" dropdown.

Open questions

  • Should the cutoff respect the clinic's timezone or always client-local? Currently the field is just a duration; recommend interpreting event.start - cutoff strictly in UTC since event.start is UTC.
  • Should rescheduled events get a distinct status / activity-log entry? Recommend yes — adds an Activity History row "Rescheduled by client" with old/new times.

Cross-references

  • Existing reschedule mechanics: basis-functions/functions/src/functions_clinic.py update_event_details at line ~11448
  • Existing client-app payment toggles for reference pattern: data.clientAppPayments on the clinic doc
  • Existing settings model in basisflow-web: app/(main)/settings/page.tsx ClientAppTab