phase B therapist self-serve: /therapist (schedule + availability)
This commit is contained in:
86
docs/progress/2026-05-02-therapist-self-serve.md
Normal file
86
docs/progress/2026-05-02-therapist-self-serve.md
Normal 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
|
||||
```
|
||||
Reference in New Issue
Block a user