# 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 ## Recommended next step **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 ```bash 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 ```