feat(intake): structured per-service intake questions

Services declare up to 8 intake fields (text/yesno, required) stored
as JSONB; shared/intake.go is the single normalization point — slug
keys, label/dup/unknown-key rules, yes/no canonicalization. Booking
create validates answers against the service declaration (missing
required -> 400), unknown keys are dropped before persist.

Answers surface on UpcomingBooking and CustomerBookingView; the
services page gains a compact editor and the public form renders
declared questions under contact details. Verified live end-to-end.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-18 18:04:32 +02:00
1 parent ad60167a31
commit 33b6e2a2a6
20 files changed
+598 -41

No files matched your search

+9 -1
View File
@@ -212,6 +212,14 @@
- dashboard: "Opakující se lekce" card between templates and sessions on the classes tab — rule form (template, weekday, time, optional location) + rule list; sessions expose `scheduleId` so rule-generated sessions are identifiable
- verified live: seeded Wed 10:00 + Sat 09:30 rules generated 8 sessions at correct local times; create → 4 Monday sessions materialized immediately; delete → rule gone, sessions remain; dayOfWeek 9 rejected; public availability converts local→UTC correctly; focused tests cover materialization, idempotency, validation
- remaining: live staging validation on Neon + pgx repository path; no bulk series edit (delete-and-recreate by design)
- `[done/partial]` Milestone 30: Structured intake questions
- services declare up to 8 intake fields (`text` / `yesno`, required flag); `services.intake_fields` JSONB (migration 00019), bookings gain `intake` JSONB for canonical answers
- `shared/intake.go` is the single normalization point: slug keys (≤40), labels ≤80, duplicate/unknown keys rejected on declaration, unknown answer keys silently dropped, answers trimmed ≤500, yes/no normalized
- booking create validates against the service's declared fields — missing required → 400 naming the field label; catalog create/update reuses the same normalizer
- intake surfaces: public catalog (`PublicService.intakeFields`), dashboard upcoming bookings (`UpcomingBooking.intake`), customer manage view (`CustomerBookingView.intake`)
- services page gains a compact intake editor (label + type + required per row, max 8, client-side slug key generation mirroring the backend); public form renders declared questions under contact details with required markers; dashboard booking modal and manage view list answers (yes/no localized, keys shown normalized — labels are not stored with answers by design)
- verified live + in-browser: catalog exposes fields, booking with unknown key stores only declared answers, missing required → 400, UI submit → stored answers visible in dashboard summary and manage view; focused `internal/shared` tests cover normalization edge cases
- remaining: live staging validation on Neon + pgx repository path; labels shown to staff are key-derived (service lookup for original labels is a possible refinement)
## Overview
Bookra is a Czech-first booking SaaS for small local service businesses. It targets salons, massage therapists, small clinics, repair shops, and small studios/clubs that need a simple public booking flow, internal schedule management, reminders, and basic business reporting without becoming a full ERP.
@@ -463,7 +471,7 @@ Shortlist of worthwhile directions once v1 stabilizes — kept deliberately smal
- **Google Calendar sync**: M22 shipped read-only ICS feeds; two-way sync (push bookings into Google, pull busy blocks back as availability exceptions) needs OAuth and conflict rules — larger commitment, defer until a concrete request.
- **Series-aware session edits**: M29's rules generate sessions, but editing a rule means delete-and-recreate; an "apply to future sessions of this rule" update (time shift, capacity change) would remove the workaround.
- **Booking notes intake**: structured intake fields per service (e.g. "first visit?", "injuries?") instead of the single free-text notes field — still schema-light.
- **Intake answer surfacing**: M30 stores answers keyed by slug, so staff see normalized keys rather than original labels; joining the service's declared labels into the booking view (and filtering/reporting on yes-no answers) would complete the loop without changing the schema.
- **Browser push notifications**: M27 persists read state and the feed covers deliveries; Web Push (service worker + VAPID) would notify owners about new bookings without keeping the dashboard open — defer until a concrete request, email already covers off-site reach.
- **Service performance panel**: M28's service-report aggregation could surface in the dashboard (revenue by service, top performers) once the metric set proves stable — the CSV is the probe.
- **Scheduled report delivery**: period-end exports emailed automatically (cron + existing mail transport) instead of manual downloads — pairs naturally with M28's report endpoints.