# 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 | 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 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) ## 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 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 (~3–5 days), then 5d (~3–5 days, gates 5c.2). ## How to resume ```bash 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 ```