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

94 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)
## 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 (~35 days), then 5d (~35 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
```