mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 15:51:15 +00:00
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:
1 parent
ad60167a31
commit
33b6e2a2a6
20 files changed
+598
-41
No files matched your search
@@ -26,6 +26,7 @@ type UpcomingBooking struct {
|
||||
Location string `json:"location,omitempty"`
|
||||
StaffName string `json:"staffName,omitempty"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
Intake map[string]string `json:"intake,omitempty"`
|
||||
StartsAt time.Time `json:"startsAt"`
|
||||
EndsAt time.Time `json:"endsAt"`
|
||||
Status string `json:"status"`
|
||||
@@ -135,10 +136,11 @@ type PublicAvailability struct {
|
||||
}
|
||||
|
||||
type PublicService struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
DurationMinutes int `json:"durationMinutes"`
|
||||
PriceCents int `json:"priceCents"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
DurationMinutes int `json:"durationMinutes"`
|
||||
PriceCents int `json:"priceCents"`
|
||||
IntakeFields []IntakeField `json:"intakeFields,omitempty"`
|
||||
}
|
||||
|
||||
type PublicStaff struct {
|
||||
@@ -163,6 +165,9 @@ type CreateBookingRequest struct {
|
||||
CustomerEmail string `json:"customerEmail"`
|
||||
CustomerPhone string `json:"customerPhone,omitempty"`
|
||||
Notes string `json:"notes"`
|
||||
// Intake carries structured intake answers keyed by the service's
|
||||
// declared IntakeField keys; only declared keys are stored.
|
||||
Intake map[string]string `json:"intake,omitempty"`
|
||||
StartsAt string `json:"startsAt"`
|
||||
EndsAt string `json:"endsAt"`
|
||||
}
|
||||
@@ -295,31 +300,44 @@ type UpdateLocationRequest struct {
|
||||
// SERVICE MODELS
|
||||
// ============================================
|
||||
|
||||
// IntakeField is one structured intake question a service asks at booking
|
||||
// time. Key is a slug (lowercase alnum + underscore) used as the JSON key in
|
||||
// booking intake answers; Type is "text" or "yesno".
|
||||
type IntakeField struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Type string `json:"type"`
|
||||
Required bool `json:"required,omitempty"`
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
ID string `json:"id"`
|
||||
TenantID string `json:"tenantId"`
|
||||
Name string `json:"name"`
|
||||
DurationMinutes int `json:"durationMinutes"`
|
||||
BufferBeforeMinutes int `json:"bufferBeforeMinutes"`
|
||||
BufferAfterMinutes int `json:"bufferAfterMinutes"`
|
||||
PriceCents int `json:"priceCents"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
ID string `json:"id"`
|
||||
TenantID string `json:"tenantId"`
|
||||
Name string `json:"name"`
|
||||
DurationMinutes int `json:"durationMinutes"`
|
||||
BufferBeforeMinutes int `json:"bufferBeforeMinutes"`
|
||||
BufferAfterMinutes int `json:"bufferAfterMinutes"`
|
||||
PriceCents int `json:"priceCents"`
|
||||
IntakeFields []IntakeField `json:"intakeFields,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type CreateServiceRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
DurationMinutes int `json:"durationMinutes" binding:"required"`
|
||||
BufferBeforeMinutes int `json:"bufferBeforeMinutes,omitempty"`
|
||||
BufferAfterMinutes int `json:"bufferAfterMinutes,omitempty"`
|
||||
PriceCents int `json:"priceCents,omitempty"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
DurationMinutes int `json:"durationMinutes" binding:"required"`
|
||||
BufferBeforeMinutes int `json:"bufferBeforeMinutes,omitempty"`
|
||||
BufferAfterMinutes int `json:"bufferAfterMinutes,omitempty"`
|
||||
PriceCents int `json:"priceCents,omitempty"`
|
||||
IntakeFields []IntakeField `json:"intakeFields,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateServiceRequest struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
DurationMinutes *int `json:"durationMinutes,omitempty"`
|
||||
BufferBeforeMinutes *int `json:"bufferBeforeMinutes,omitempty"`
|
||||
BufferAfterMinutes *int `json:"bufferAfterMinutes,omitempty"`
|
||||
PriceCents *int `json:"priceCents,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
DurationMinutes *int `json:"durationMinutes,omitempty"`
|
||||
BufferBeforeMinutes *int `json:"bufferBeforeMinutes,omitempty"`
|
||||
BufferAfterMinutes *int `json:"bufferAfterMinutes,omitempty"`
|
||||
PriceCents *int `json:"priceCents,omitempty"`
|
||||
IntakeFields *[]IntakeField `json:"intakeFields,omitempty"`
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -526,6 +544,7 @@ type CustomerBookingView struct {
|
||||
Location string `json:"location"`
|
||||
Status string `json:"status"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
Intake map[string]string `json:"intake,omitempty"`
|
||||
// WaitlistPosition is the 1-based queue spot for waitlisted class
|
||||
// bookings; zero for any other status.
|
||||
WaitlistPosition int `json:"waitlistPosition,omitempty"`
|
||||
|
||||
Reference in new issue
Block a user