From 2ace6d4d87e027152d439d62711b558c4eb1e502 Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Sat, 24 Jan 2026 22:08:36 +0000 Subject: [PATCH] WIP call in booking --- backend/handlers/bookings/manage.go | 184 +++++++++++++++--- backend/main.go | 3 +- .../admin/BookingCreateModal.svelte | 108 ++++++++++ .../lib/components/admin/CallInBooking.svelte | 35 ++++ .../lib/components/today/TodayCalendar.svelte | 9 +- frontend/src/routes/today/+page.svelte | 32 +-- 6 files changed, 330 insertions(+), 41 deletions(-) create mode 100644 frontend/src/lib/components/admin/BookingCreateModal.svelte create mode 100644 frontend/src/lib/components/admin/CallInBooking.svelte diff --git a/backend/handlers/bookings/manage.go b/backend/handlers/bookings/manage.go index fc0a40f..1ea81fe 100644 --- a/backend/handlers/bookings/manage.go +++ b/backend/handlers/bookings/manage.go @@ -168,57 +168,193 @@ func AdminEditBookingHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } -// AdminCreateBookingForUserHandler creates a booking on behalf of a user. -func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) { - userID := chi.URLParam(r, "user_id") - var req CreateBookingRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) +type AdminCreateBookingForUserRequest struct { + UserID string `json:"user_id" validate:"required"` + StartTime time.Time `json:"start_time" validate:"required"` + ServiceIDs []string `json:"service_ids" validate:"required,min=1"` + ServiceOverrides []ServiceOverride `json:"service_overrides,omitempty"` + Notes *string `json:"notes,omitempty"` // staff notes +} + +func oo(w http.ResponseWriter, r *http.Request) { + // Admin identity (creator) + adminID, ok := r.Context().Value(mw.UserIDKey).(string) + if !ok || adminID == "" { + http.Error(w, "Authentication required", http.StatusUnauthorized) return } - adminID, _ := r.Context().Value(mw.UserIDKey).(string) + var req AdminCreateBookingForUserRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + log.Printf("Failed to decode request: %v", err) + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + + // Basic validation + if req.UserID == "" { + http.Error(w, "User ID is required", http.StatusBadRequest) + return + } + if req.StartTime.IsZero() { + http.Error(w, "Start time is required", http.StatusBadRequest) + return + } + if len(req.ServiceIDs) == 0 { + http.Error(w, "At least one service is required", http.StatusBadRequest) + return + } + + // Validate overrides + for _, override := range req.ServiceOverrides { + if override.ServiceID == "" { + http.Error(w, "Service ID is required for overrides", http.StatusBadRequest) + return + } + if override.OverridePrice != nil && *override.OverridePrice < 0 { + http.Error(w, "Override price cannot be negative", http.StatusBadRequest) + return + } + if override.OverrideDurationMinutes != nil && *override.OverrideDurationMinutes <= 0 { + http.Error(w, "Override duration must be positive", http.StatusBadRequest) + return + } + } tx, err := db.DB.Begin(r.Context()) if err != nil { - log.Printf("Failed to begin transaction: %v", err) + log.Printf("Failed to start transaction: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } defer tx.Rollback(r.Context()) - var bookingID string - err = tx.QueryRow(r.Context(), ` - INSERT INTO bookings (id, user_id, start_time, status, created_by) - VALUES (generate_booking_id(), $1, $2, 'pending', $3) - RETURNING id - `, userID, req.StartTime, adminID).Scan(&bookingID) + // Create booking directly as confirmed + bookingQuery := ` + INSERT INTO bookings ( + user_id, + start_time, + status, + notes, + created_by + ) + VALUES ($1, $2, 'confirmed', $3, $4) + RETURNING id, user_id, start_time, status, notes, created_at, updated_at, created_by + ` + + var booking Booking + booking.User = &UserSummary{} + + err = tx.QueryRow( + r.Context(), + bookingQuery, + req.UserID, + req.StartTime, + req.Notes, + adminID, + ).Scan( + &booking.ID, + &booking.User.ID, + &booking.StartTime, + &booking.Status, + &booking.Notes, + &booking.CreatedAt, + &booking.UpdatedAt, + &booking.CreatedBy, + ) + if err != nil { - log.Printf("Failed to create booking: %v", err) + log.Printf("Failed to create admin booking: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } - for _, svcID := range req.ServiceIDs { - if _, err := tx.Exec(r.Context(), ` - INSERT INTO booking_services (booking_id, service_id) - VALUES ($1, $2) - `, bookingID, svcID); err != nil { - log.Printf("Failed to insert booking service: %v", err) + // Insert booking services + serviceInsertQuery := ` + INSERT INTO booking_services (booking_id, service_id) + VALUES ($1, $2) + ` + + for _, serviceID := range req.ServiceIDs { + _, err := tx.Exec(r.Context(), serviceInsertQuery, booking.ID, serviceID) + if err != nil { + log.Printf("Failed to insert booking service %s: %v", serviceID, err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } } - if err = tx.Commit(r.Context()); err != nil { - log.Printf("Failed to commit transaction: %v", err) + // Apply overrides (optional) + if len(req.ServiceOverrides) > 0 { + // Ensure overrides only reference services in this booking + serviceCheckQuery := ` + SELECT COUNT(*) FROM booking_services + WHERE booking_id = $1 AND service_id = ANY($2) + ` + + overrideServiceIDs := make([]string, len(req.ServiceOverrides)) + for i, o := range req.ServiceOverrides { + overrideServiceIDs[i] = o.ServiceID + } + + var count int + err = tx.QueryRow( + r.Context(), + serviceCheckQuery, + booking.ID, + overrideServiceIDs, + ).Scan(&count) + + if err != nil { + log.Printf("Failed to verify service overrides: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + if count != len(req.ServiceOverrides) { + http.Error(w, "One or more service overrides do not belong to this booking", http.StatusBadRequest) + return + } + + overrideUpdateQuery := ` + UPDATE booking_services + SET override_price = $1, + override_duration_minutes = $2 + WHERE booking_id = $3 AND service_id = $4 + ` + + for _, override := range req.ServiceOverrides { + _, err := tx.Exec( + r.Context(), + overrideUpdateQuery, + override.OverridePrice, + override.OverrideDurationMinutes, + booking.ID, + override.ServiceID, + ) + if err != nil { + log.Printf( + "Failed to apply override (booking %s, service %s): %v", + booking.ID, + override.ServiceID, + err, + ) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + } + } + + if err := tx.Commit(r.Context()); err != nil { + log.Printf("Failed to commit admin booking creation: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) - if err := json.NewEncoder(w).Encode(map[string]string{"id": bookingID}); err != nil { + if err := json.NewEncoder(w).Encode(booking); err != nil { log.Printf("Failed to encode response: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) } } diff --git a/backend/main.go b/backend/main.go index a0cc0fc..579f5dd 100644 --- a/backend/main.go +++ b/backend/main.go @@ -128,12 +128,13 @@ func main() { r.Route("/admin/bookings", func(r chi.Router) { r.Get("/", bookings.GetAllAdminBookingsHandler) + r.Post("/", bookings.AdminCreateBookingForUserHandler) r.Get("/search", bookings.SearchAdminBookingsHandler) r.Get("/user/{user_id}", bookings.GetAllBookingsByUserHandler) r.Get("/{id}", bookings.GetAdminBookingHandler) r.Put("/{id}/progress", bookings.ProgressBookingHandler) r.Post("/{id}/confirm", bookings.ConfirmBookingHandler) - r.Post("/{id}/cancel", bookings.ConfirmBookingHandler) + r.Post("/{id}/cancel", bookings.ConfirmBookingHandler) // todo }) r.Route("/admin/users", func(r chi.Router) { diff --git a/frontend/src/lib/components/admin/BookingCreateModal.svelte b/frontend/src/lib/components/admin/BookingCreateModal.svelte new file mode 100644 index 0000000..402a74d --- /dev/null +++ b/frontend/src/lib/components/admin/BookingCreateModal.svelte @@ -0,0 +1,108 @@ + + + + + + Create Booking + + +
+
+ + +
+ +
+ + +
+ +
+ +
+ {#each services as service} + + {/each} +
+
+ +
+ +