initial ux

This commit is contained in:
2026-05-02 09:25:07 -04:00
parent dbca91e06b
commit 3dfc84aa43
18 changed files with 1723 additions and 0 deletions

View File

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