phase B therapist self-serve: /therapist (schedule + availability)

This commit is contained in:
2026-05-02 09:35:31 -04:00
parent 3dfc84aa43
commit a270d83c1a
7 changed files with 687 additions and 186 deletions

View File

@@ -0,0 +1,86 @@
# 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
```