Files
touchbase/docs/progress/2026-05-02-admin-create-booking.md

5.6 KiB
Raw Permalink Blame History

2026-05-02 — Admin Create-Booking Page

Companion to Initial.md. Predecessor: 2026-05-02-auth-and-admin-shell.md. Closes Step 5b of Initial.md §9.

Milestone

Admin can now take a booking from the browser, end-to-end. The CLI (scripts/book-on-behalf.ts) is no longer the only way to create a booking — there's a /admin/bookings/new page that wraps the same loader + createHold + email pipeline behind a server-rendered form. The practice can now use TouchBase to take phone bookings without leaving the browser.

What landed

Path Role
src/app/admin/bookings/new/page.tsx Server-rendered create-booking page. Form (service + date + customer email) → server action redirects to same URL with query params → page renders slot grid. Each slot is its own form whose server action calls the booking pipeline.
src/app/admin/bookings/page.tsx Added "New booking" link in the header

No new dependencies. No schema changes.

What's verified end-to-end

In the dev-server smoke test (curl + Mailpit, server running on :3000):

  • GET /admin/bookings/new (signed in) → 200, form renders
  • GET /admin/bookings/new?serviceId=…&customerEmail=alex@example.com&date=2026-05-05 → 33 slots rendered (10:00 EDT through 18:00 EDT, every 15 min, for a 60-min service)
  • GET /admin/bookings/new?…&customerEmail=ghost@example.com&… → "No customer registered for ghost@example.com" inline error
  • GET /admin/bookings/new?…&date=2026-05-04 (Monday, not in working hours) → "No available slots on this date"

Plus: 64/64 tests, lint clean, typecheck clean.

The actual bookSlotAction (server action that creates the booking) wasn't exercised via curl because Next.js server actions require a signed POST that's annoying to replay; all of its building blocks (createHold, confirmHold, sendBookingConfirmation, the loader, findSlots) are unit-tested individually plus end-to-end via the existing CLI smoke test.

Decisions ratified

Decision Resolution
Form architecture Multi-step server-rendered. No client state. Form 1 = filter (service+date+customer); slot grid renders on submit; each slot is its own server-action form. Reason: simplest possible thing that works; preserves admin's filter state across page loads via query params.
Customer selection Plain email input (no autocomplete/search-as-you-type yet). Reason: keep client JS out for v1; admin already knows customer emails. Revisit if it gets clunky.
Slot picker UX 46 column grid of buttons, each labeled with local time only (e.g. "10:00 AM"). Date is fixed by the filter above.
Greedy assignment First eligible therapist + first eligible room from the slot's candidate lists. Same as the CLI. Reason: more sophisticated load-balancing waits until we have data on whether it matters.
Error surfacing Errors round-trip through the URL via error=... query param. Reason: server actions in Next.js can't return errors directly to the same page without client state; redirect-with-error is the simplest pattern.
Date input minimum min= set to today (in practice TZ via Intl.DateTimeFormat with en-CA locale to get YYYY-MM-DD).

Gotchas hit

searchParams is async in Next.js 16

The searchParams page prop is now Promise<…> in Next 15+. Must await searchParams before reading. Easy to miss — TS will catch it but only if the type annotation is correct.

Open questions still unresolved

  1. Customer-visible brand name
  2. Currency
  3. Stripe account ownership
  4. NEW: Customer search-as-you-type vs. plain email field — admin UX call. Plain email works; autocomplete is nicer. Defer until requested.

Roadmap status (Initial.md §9)

  1. Spike — done 2026-04-30
  2. Schema + seed — done 2026-05-01
  3. Availability algorithm — done 2026-05-01
  4. First end-to-end story (admin booking + email) — done 2026-05-01
  5. Public self-booking → Stripe → reminders
    • 5a Auth.js + admin shell — done 2026-05-02
    • 5b Admin "create booking" page — done 2026-05-02 (this session)
    • 5c Public booking page (CUSTOMER role, magic-link signup, slot search + checkout)
    • 5d Stripe deposit flow + webhook
    • 5e Email reminders (pg-boss scheduled jobs)

5c — public booking page. Most of the slot-picker UI carries over from /admin/bookings/new; the differences are:

  • CUSTOMER auth flow: magic-link signup if email isn't registered. Auth.js's signIn("nodemailer", { email }) already creates a User row via the adapter; we just need to also create a Customer row + redirect into a booking flow.
  • Public-facing styling: likely a polished landing page with service grid → "Book" → date+slot picker → confirm.
  • Customer can't pick therapist or room directly — algorithm assigns greedy.
  • Hold semantics matter more: customer pays deposit during the hold window; if they bail, hold expires.

That last point pulls Stripe (5d) closer in dependency: deposit-required services need the payment flow before the booking can become CONFIRMED. We may want to do 5c in two phases:

  • 5c.1: public booking with no payment (CONFIRMED on click). Fast to ship; lets the practice test the customer-side UX before Stripe.
  • 5c.2: add the deposit payment flow (Stripe).

Either approach is fine. Recommend 5c.1 first (~35 days), then 5d (~35 days, gates 5c.2).

How to resume

cd /Users/noise/Documents/code/touchbase
docker-compose up -d postgres mailpit
pnpm db:seed
pnpm dev
# Sign in at http://localhost:3000/login as admin@touchbase.local
# Click magic link in http://localhost:8025
# /admin/bookings → "New booking" → fill form → pick slot → done