Files
touchbase/docs/progress/2026-05-02-admin-crud.md
2026-05-02 09:25:07 -04:00

103 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 2026-05-02 — Admin CRUD (Phase A)
> Companion to `Initial.md`. Predecessor: `2026-05-02-customer-account.md`. Closes Phase A of the post-payments UX plan.
## Milestone
The practice can now manage all of its data from the browser — no SQL, no `seed.ts` re-runs. Services, rooms, therapists, working hours, overrides, and room blocks all have full CRUD UIs under `/admin`. Together with the bookings list and create-booking page already in place, the admin shell is functionally complete for v1 minus payments.
## What landed
| Area | Pages |
|---|---|
| Services | `/admin/services` (list with active/inactive + counts), `/admin/services/new`, `/admin/services/[id]` |
| Rooms | `/admin/rooms` (list + capability tags + booking/block counts), `/admin/rooms/new`, `/admin/rooms/[id]`, `/admin/rooms/[id]/blocks` (list + add + delete) |
| Therapists | `/admin/therapists` (list + qualifications + service count), `/admin/therapists/new`, `/admin/therapists/[id]`, `/admin/therapists/[id]/availability` (weekly hours editor + overrides list/add/delete) |
Plus:
- `src/app/admin/layout.tsx` — added Services / Rooms / Therapists nav links
- `src/lib/forms.ts` — shared parsers (`parseInt10`, `parseDollarsToCents`, `centsToDollars`, `parseTags`, `tagsToInputValue`, `parseBool`, `parseStringTrim`)
- `src/lib/working-hours.ts``WEEKDAYS` constant + `timeToMin`/`minToTime` for `<input type="time">` interop
- Per-area `_form.tsx` shared form components (Service, Room, Therapist) so create + edit pages reuse the same UI
## What's verified
- `pnpm test` — 71/71 (no new tests; this session is pure UI on already-tested data layer)
- `pnpm lint` — clean
- `pnpm exec tsc --noEmit` — clean
- Live smoke: signed in as admin@touchbase.local, all 11 new admin pages return 200:
```
/admin/services, /admin/services/new, /admin/services/[id]
/admin/rooms, /admin/rooms/new, /admin/rooms/[id], /admin/rooms/[id]/blocks
/admin/therapists, /admin/therapists/new, /admin/therapists/[id], /admin/therapists/[id]/availability
```
The form server actions weren't replayed via curl (Next.js 16 RPC encoding rejects manual replays — same constraint we've documented). Each action is a thin Prisma `create`/`update`/`deleteMany`+`createMany` against tested models; the seed script already exercises the same query shapes successfully.
## Decisions ratified
| Decision | Resolution |
|---|---|
| Form architecture | Shared `_form.tsx` per entity (private `_` directory ignored by App Router routing). `create` and `edit` pages both render the same form with different action props. Editor pages pass `id` so the form renders a hidden input. |
| Tag input | Comma-separated text field. Parsed server-side: lowercased, trimmed, deduped via filter. Reason: zero client JS; users see exactly what they typed. |
| Soft delete | All entities use `active` boolean. **No hard-delete** anywhere (would orphan bookings). Admin sees `active=false` rows greyed-out. |
| Email uniqueness on therapist create | Pre-checked before `db.user.create`; throws a clear error rather than letting Prisma's generic unique-constraint violation bubble up. |
| Therapist edit doesn't change email | Email is the sign-in identity; changing it would silently break Auth.js sessions. Field rendered disabled in edit mode. |
| Working hours model | One shift per day per therapist. Split-shift support via `EXTRA_HOURS` overrides. Reason: 90% of practice schedules are single-shift; split-shift edge cases live in the override table where they belong. |
| Time inputs | HTML `<input type="time">` (HH:mm). Converted to/from minutes-from-midnight via `src/lib/working-hours.ts`. |
| Override datetime inputs | HTML `<input type="datetime-local">`. Server parses with `fromZonedTime` (date-fns-tz) using `APP_TZ`. |
| Working-hours save | Replace-all transaction: `deleteMany` then `createMany` for the therapist's rows. Reason: simpler than diffing; one txn. |
| Tag/service updates on therapist edit | Same replace-all pattern: delete + recreate join rows in a transaction. |
## Gotchas hit
### Nested `<form>` is invalid HTML
First pass had the edit page wrap `ServiceForm` (which is itself a `<form>`) in another `<form>` to add a hidden `id`. Browsers silently break nested forms. Fix: `ServiceForm` accepts an optional `id` prop and renders the hidden input itself.
### Unused `toLocalInputValue` import
Started writing the override editor with prefilled datetime-local values but settled on always-empty (the form is for adding new). Removed the helper to avoid the lint warning.
## Open questions
1. Customer-visible brand name (still pending)
2. Currency (USD assumed; admin price inputs are labeled "USD")
3. Stripe account ownership (not blocking until we get to 5d)
4. **NEW**: bulk operations? E.g. "set TueFri 1019 for all therapists" or "block Room 7 every Sunday." Defer until someone asks for it.
5. **NEW**: audit logging for admin actions. We have an `AuditLog` table but the new CRUD actions don't write to it. Worth wiring up before launch.
## Roadmap status
UX-completeness phases (the user's "complete the rest of the UX" pivot before payments):
- **A — Admin CRUD: done 2026-05-02 (this session)**
- B — Therapist self-serve at `/therapist` — own working hours + time-off
- C — Customer reschedule + admin "mark complete/no-show" + booking detail pages
- D — PWA shell (manifest + service worker) per Initial.md §11
- E — Polish — 404, empty states, mobile-first review, dark-mode glance
Backend roadmap unchanged: 5d Stripe → 5e reminders, after UX is rounded out.
## Recommended next step
**Phase B — therapist self-serve**. The smallest remaining UX chunk that's actually load-bearing. Per `Initial.md` decision log, therapists set their own availability; admin is the override path, not the primary path. Implementation:
- `/therapist` layout that requires `role=THERAPIST`
- `/therapist/availability` — same page as admin's, scoped to the signed-in therapist
- `/therapist/bookings` — read-only list of upcoming bookings on this therapist
- Reuse the existing form helpers; no new infrastructure
~half-day. After B, Phase C (~1 day) → D (~1 day) → E (~half-day) and we're UX-complete for v1 minus payments.
## How to resume
```bash
cd /Users/noise/Documents/code/touchbase
docker-compose up -d postgres mailpit
pnpm db:seed
pnpm dev
# Sign in as admin@touchbase.local at /login
# Click around: /admin/services, /admin/rooms, /admin/therapists, etc.
# Try: create a new service, add a tag, deactivate it, edit a therapist's working hours, add an override, add a room block.
```