Files
touchbase/docs/progress/2026-05-02-therapist-self-serve.md

5.4 KiB

2026-05-02 — Therapist Self-Serve (Phase B)

Companion to Initial.md. Predecessor: 2026-05-02-admin-crud.md. Closes Phase B of the post-payments UX plan.

Milestone

Therapists can now sign in and manage their own availability without involving admin. They also see their own schedule (upcoming + recent appointments) with customer contact info. The admin-side per-therapist availability editor and the new therapist self-serve editor share the same component, so any future improvement to the editor benefits both surfaces.

What landed

Path Role
src/components/AvailabilityEditor.tsx Reusable server component (no client state) — weekly working-hours grid + overrides list/add/delete. Caller supplies the data + 3 server-action callbacks + optional hidden form fields.
src/app/admin/therapists/[id]/availability/page.tsx (refactored) Now uses AvailabilityEditor with hiddenFields={{ therapistId: id }}. Same behavior, less code.
src/app/therapist/layout.tsx Auth-required, role-gated to THERAPIST. Friendly redirect/explanation for ADMIN or CUSTOMER hitting the route. Header with "My schedule" + "Availability" + sign-out.
src/app/therapist/page.tsx Redirects to /therapist/bookings (most-useful default).
src/app/therapist/availability/page.tsx Same editor as the admin page, scoped to session.user.id. Server actions read therapistId from the session and ignore any form-supplied therapistId — defense in depth. Delete-override action also re-checks ownership.
src/app/therapist/bookings/page.tsx Read-only schedule. Upcoming table (When, Service+duration, Customer name+email+phone, Room) + recent list with status pills.

What's verified

  • pnpm test — 71/71 (no new tests)
  • pnpm lint — clean
  • pnpm exec tsc --noEmit — clean
  • Live smoke test as mei@touchbase.local (THERAPIST role from seed):
    • Magic-link sign-in with callbackUrl=/therapist → lands on /therapist/bookings
    • /therapist (no trailing path) → 307 → /therapist/bookings
    • /therapist/bookings shows the two seeded-CLI bookings with customer name + email
    • /therapist/availability renders working-hours editor (Tuesday-Saturday from seed) + override form
    • /admin/bookings (as therapist) renders "Not authorized" — correctly gated

Decisions ratified

Decision Resolution
Editor reuse Extracted to src/components/AvailabilityEditor.tsx. Caller passes data + actions + optional hidden fields. Reason: avoid 280 LOC of duplication; one place to fix bugs.
Therapist actions ignore form-supplied therapistId Server actions read therapist id from auth() session, never from FormData. Reason: a malicious THERAPIST could otherwise edit another therapist's schedule by tampering with the form. The component still has a hiddenFields prop for the admin route, but the therapist route doesn't pass any.
Delete-override ownership check Action looks up the override and aborts if therapistId !== session.user.id. Reason: defense in depth; the page only shows own overrides but the action could be replayed with someone else's id.
Therapist landing /therapist redirects to /therapist/bookings (the schedule), not /therapist/availability. Reason: most therapists open the app to see "what's next today" — the schedule is the more useful default.
Therapist sees customer contact info Name + email + phone exposed in the schedule table. Reason: legitimate operational need (running late, customer no-show, etc.). Not HIPAA in our scope, but worth being mindful of.
Cross-role friendly errors ADMIN visiting /therapist sees "Therapists only" with link to /admin/therapists. CUSTOMER sees link to /account/bookings. Reason: mistaken navigation should explain itself.

Gotchas hit

None this session — clean refactor + extension.

Open questions

  1. Customer-visible brand name (still pending)
  2. Currency
  3. Stripe account ownership
  4. NEW: should the therapist schedule include a "mark complete / no-show" button, or do we keep that admin-only? Coming up in Phase C.
  5. NEW: today's-day highlight in the therapist schedule? Defer until requested.

Roadmap status

UX-completeness:

  • A — Admin CRUD: done 2026-05-02
  • B — Therapist self-serve: done 2026-05-02 (this session)
  • C — Customer reschedule + admin "mark complete/no-show" + booking detail pages
  • D — PWA shell
  • E — Polish

Phase C — booking lifecycle UX:

  1. Booking detail page at /admin/bookings/[id] (and maybe /account/bookings/[id] for customers): all fields, history, actions.
  2. Reschedule action for customers (and admin): cancel + rebook in one transaction. Likely add rescheduleBooking() to src/lib/booking.ts that wraps the operation atomically.
  3. Admin "mark complete / no-show" buttons on the admin booking detail (or directly on the row in /admin/bookings). Adds two more state transitions to handle.

Estimate ~1 day. Mostly composition over existing primitives.

How to resume

cd /Users/noise/Documents/code/touchbase
docker-compose up -d postgres mailpit
pnpm db:seed
pnpm tsx scripts/book-on-behalf.ts alex@example.com "60-minute Swedish" 2026-05-05T10:00
pnpm dev
# Sign in as a therapist (e.g. mei@touchbase.local) → lands on /therapist/bookings
# Click "Availability" → edit working hours, add an override
# Sign out, sign in as admin@touchbase.local → /admin/* still works as before