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

6.5 KiB
Raw Permalink Blame History

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.tsWEEKDAYS 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.

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

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.