5.6 KiB
2026-05-02 — Admin Create-Booking Page
Companion to
Initial.md. Predecessor:2026-05-02-auth-and-admin-shell.md. Closes Step 5b ofInitial.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 rendersGET /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 errorGET /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 | 4–6 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
- Customer-visible brand name
- Currency
- Stripe account ownership
- 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)
- Spike — done 2026-04-30
- Schema + seed — done 2026-05-01
- Availability algorithm — done 2026-05-01
- First end-to-end story (admin booking + email) — done 2026-05-01
- 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)
Recommended next step
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 aCustomerrow + 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 (~3–5 days), then 5d (~3–5 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