mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-29 07:54:44 +00:00
feat(bookings): customer self-service rebooking with real slot picker
CustomerBookingView now carries tenantSlug/serviceId/staffId/ classSessionId so the manage page can query live availability; class bookings resolve their session title as the display label. Reschedule rejects waitlisted/completed bookings and session-bound class bookings (400), collapsing a dead branch in the appointment path. The manage page reschedule dialog replaces free-form date/time inputs with a date → availability-slot picker filtered to appointment mode, so customers can only move to a genuinely free slot with the correct duration; 409 surfaces "slot just taken". Actions are status-gated: reschedule for confirmed/pending appointments, cancel for live bookings, explanatory note for class bookings. 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
1cd5b1ba91
commit
796c06ae66
7 files changed
+311
-87
No files matched your search
@@ -73,6 +73,12 @@ func (s *CustomerService) managementURL(booking db.BookingRecord) string {
|
||||
}
|
||||
|
||||
func (s *CustomerService) descriptors(ctx context.Context, booking db.BookingRecord) (serviceName string, locationName string) {
|
||||
if booking.ServiceID == nil && booking.ClassSessionID != nil {
|
||||
// Class bookings carry no service; the session title is the label.
|
||||
if session, err := s.repo.GetClassSessionByID(ctx, *booking.ClassSessionID); err == nil {
|
||||
serviceName = session.Title
|
||||
}
|
||||
}
|
||||
if booking.ServiceID != nil {
|
||||
if services, err := s.repo.ListServicesByTenant(ctx, booking.TenantID); err == nil {
|
||||
for _, service := range services {
|
||||
@@ -119,6 +125,10 @@ func (s *CustomerService) GetBookingByReference(ctx context.Context, reference s
|
||||
CustomerEmail: booking.CustomerEmail,
|
||||
Service: serviceName,
|
||||
BusinessName: tenant.Name,
|
||||
TenantSlug: tenant.Slug,
|
||||
ServiceID: booking.ServiceID,
|
||||
StaffID: booking.StaffID,
|
||||
ClassSessionID: booking.ClassSessionID,
|
||||
StartsAt: booking.StartsAt,
|
||||
EndsAt: booking.EndsAt,
|
||||
Location: locationName,
|
||||
@@ -142,6 +152,18 @@ func (s *CustomerService) RescheduleBooking(ctx context.Context, reference strin
|
||||
return ErrBookingCancelled
|
||||
}
|
||||
|
||||
// Only live bookings may move: waitlisted entries sit in a queue tied to
|
||||
// their session, and completed bookings are history.
|
||||
if booking.Status == "waitlisted" || booking.Status == "completed" {
|
||||
return ErrInvalidReschedule
|
||||
}
|
||||
|
||||
// Class bookings are bound to their session window; moving to another
|
||||
// session means cancelling and booking again.
|
||||
if booking.ClassSessionID != nil {
|
||||
return ErrInvalidReschedule
|
||||
}
|
||||
|
||||
newStartsAt, err := time.Parse(time.RFC3339, req.NewStartsAt)
|
||||
if err != nil {
|
||||
return ErrInvalidReschedule
|
||||
@@ -174,16 +196,14 @@ func (s *CustomerService) RescheduleBooking(ctx context.Context, reference strin
|
||||
}
|
||||
}
|
||||
|
||||
// Appointments must land inside bookable hours and off blocked days;
|
||||
// class bookings keep their session-defined window.
|
||||
if booking.ClassSessionID == nil {
|
||||
tenant, err := s.repo.GetTenantByID(ctx, booking.TenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateAppointmentTarget(ctx, s.repo, tenant, booking.StaffID, booking.LocationID, newStartsAt, newEndsAt); err != nil {
|
||||
return ErrInvalidReschedule
|
||||
}
|
||||
// Appointments must land inside bookable hours and off blocked days.
|
||||
// Class bookings never reach this point — rejected above.
|
||||
tenant, err := s.repo.GetTenantByID(ctx, booking.TenantID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateAppointmentTarget(ctx, s.repo, tenant, booking.StaffID, booking.LocationID, newStartsAt, newEndsAt); err != nil {
|
||||
return ErrInvalidReschedule
|
||||
}
|
||||
|
||||
if err := s.repo.RescheduleBooking(ctx, booking.ID, newStartsAt, newEndsAt); err != nil {
|
||||
|
||||
Reference in new issue
Block a user