feat: series-aware schedule edits + waitlist ledger removal (M32)

PATCH /v1/catalog/class-schedules/:id patches a recurring rule and,
with applyToFuture, retimes materialized future sessions to the new
local start time on their existing dates — moving confirmed and
waitlisted bookings with them and re-arming reminders. Zones-page
schedule rows gain an inline editor (CS/EN).

Also drops the vestigial waitlist_entries table (migration 00020):
positions derive from bookings.created_at, so the ledger was written
but never read.

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-22 20:59:34 +02:00
1 parent 55fc0d06e3
commit 65a693cf38
17 files changed
+840 -230

No files matched your search

+62 -49
View File
@@ -18,23 +18,23 @@ type DashboardKPI struct {
}
type UpcomingBooking struct {
ID string `json:"id"`
Reference string `json:"reference"`
CustomerName string `json:"customerName"`
CustomerEmail string `json:"customerEmail"`
Service string `json:"service,omitempty"`
Location string `json:"location,omitempty"`
StaffName string `json:"staffName,omitempty"`
Notes string `json:"notes,omitempty"`
Intake map[string]string `json:"intake,omitempty"`
ID string `json:"id"`
Reference string `json:"reference"`
CustomerName string `json:"customerName"`
CustomerEmail string `json:"customerEmail"`
Service string `json:"service,omitempty"`
Location string `json:"location,omitempty"`
StaffName string `json:"staffName,omitempty"`
Notes string `json:"notes,omitempty"`
Intake map[string]string `json:"intake,omitempty"`
// IntakeLabels maps Intake keys to the service's declared field labels.
IntakeLabels map[string]string `json:"intakeLabels,omitempty"`
StartsAt time.Time `json:"startsAt"`
EndsAt time.Time `json:"endsAt"`
Status string `json:"status"`
Label string `json:"label,omitempty"`
ClassSessionID *string `json:"classSessionId,omitempty"`
WaitlistPosition int `json:"waitlistPosition,omitempty"`
StartsAt time.Time `json:"startsAt"`
EndsAt time.Time `json:"endsAt"`
Status string `json:"status"`
Label string `json:"label,omitempty"`
ClassSessionID *string `json:"classSessionId,omitempty"`
WaitlistPosition int `json:"waitlistPosition,omitempty"`
}
type WidgetSnippet struct {
@@ -169,9 +169,9 @@ type CreateBookingRequest struct {
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"`
Intake map[string]string `json:"intake,omitempty"`
StartsAt string `json:"startsAt"`
EndsAt string `json:"endsAt"`
}
type CreateBookingResponse struct {
@@ -334,11 +334,11 @@ type CreateServiceRequest struct {
}
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"`
}
@@ -413,16 +413,16 @@ type UpsertClassTemplateRequest struct {
}
type ClassSession struct {
ID string `json:"id"`
TenantID string `json:"tenantId"`
TemplateID string `json:"templateId"`
TemplateTitle string `json:"templateTitle"`
LocationID *string `json:"locationId,omitempty"`
LocationName string `json:"locationName,omitempty"`
StartsAt time.Time `json:"startsAt"`
EndsAt time.Time `json:"endsAt"`
Capacity int `json:"capacity"`
BookedCount int `json:"bookedCount"`
ID string `json:"id"`
TenantID string `json:"tenantId"`
TemplateID string `json:"templateId"`
TemplateTitle string `json:"templateTitle"`
LocationID *string `json:"locationId,omitempty"`
LocationName string `json:"locationName,omitempty"`
StartsAt time.Time `json:"startsAt"`
EndsAt time.Time `json:"endsAt"`
Capacity int `json:"capacity"`
BookedCount int `json:"bookedCount"`
// WaitlistCount is the number of customers queued for a free seat.
WaitlistCount int `json:"waitlistCount"`
// ScheduleID links the session back to the recurring rule that generated
@@ -492,6 +492,19 @@ type CreateClassScheduleRequest struct {
StartsLocal string `json:"startsLocal" binding:"required"` // "HH:MM"
}
// UpdateClassScheduleRequest patches a recurring rule; nil fields keep their
// current values. A LocationID of "" clears the location. ApplyToFuture
// additionally retimes already-materialized future sessions (and their live
// bookings) to the new start time and location; a day-of-week change only
// affects sessions materialized afterwards.
type UpdateClassScheduleRequest struct {
DayOfWeek *int `json:"dayOfWeek"`
StartsLocal *string `json:"startsLocal"` // "HH:MM"
LocationID *string `json:"locationId"`
Active *bool `json:"active"`
ApplyToFuture bool `json:"applyToFuture"`
}
// ============================================
// CUSTOMER MODELS
// ============================================
@@ -530,25 +543,25 @@ type UpdateCustomerRequest struct {
// ============================================
type CustomerBookingView struct {
Reference string `json:"reference"`
CustomerName string `json:"customerName"`
CustomerEmail string `json:"customerEmail"`
Service string `json:"service"`
BusinessName string `json:"businessName"`
TenantSlug string `json:"tenantSlug,omitempty"`
ServiceID *string `json:"serviceId,omitempty"`
StaffID *string `json:"staffId,omitempty"`
Reference string `json:"reference"`
CustomerName string `json:"customerName"`
CustomerEmail string `json:"customerEmail"`
Service string `json:"service"`
BusinessName string `json:"businessName"`
TenantSlug string `json:"tenantSlug,omitempty"`
ServiceID *string `json:"serviceId,omitempty"`
StaffID *string `json:"staffId,omitempty"`
// ClassSessionID identifies session-bound bookings; they cannot be
// rescheduled to arbitrary times (see RescheduleBooking).
ClassSessionID *string `json:"classSessionId,omitempty"`
StartsAt time.Time `json:"startsAt"`
EndsAt time.Time `json:"endsAt"`
Location string `json:"location"`
Status string `json:"status"`
Notes string `json:"notes,omitempty"`
Intake map[string]string `json:"intake,omitempty"`
ClassSessionID *string `json:"classSessionId,omitempty"`
StartsAt time.Time `json:"startsAt"`
EndsAt time.Time `json:"endsAt"`
Location string `json:"location"`
Status string `json:"status"`
Notes string `json:"notes,omitempty"`
Intake map[string]string `json:"intake,omitempty"`
// IntakeLabels maps Intake keys to the service's declared field labels.
IntakeLabels map[string]string `json:"intakeLabels,omitempty"`
IntakeLabels map[string]string `json:"intakeLabels,omitempty"`
// WaitlistPosition is the 1-based queue spot for waitlisted class
// bookings; zero for any other status.
WaitlistPosition int `json:"waitlistPosition,omitempty"`