initial ux
This commit is contained in:
102
docs/progress/2026-05-02-admin-crud.md
Normal file
102
docs/progress/2026-05-02-admin-crud.md
Normal 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 Tue–Fri 10–19 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.
|
||||
```
|
||||
Reference in New Issue
Block a user