package bookings import ( "crussell/db" "crussell/mw" "database/sql" "encoding/json" "log" "net/http" "time" "github.com/go-chi/chi/v5" ) // UserCancelBookingHandler allows an authenticated user to cancel a booking they own. // The update is performed in a single statement with appropriate conditions. func UserCancelBookingHandler(w http.ResponseWriter, r *http.Request) { bookingID := chi.URLParam(r, "id") userID, ok := r.Context().Value(mw.UserIDKey).(string) if !ok || userID == "" { http.Error(w, "Authentication required", http.StatusUnauthorized) return } res, err := db.DB.Exec(r.Context(), ` UPDATE bookings SET status = 'client_cancelled', updated_at = $1 WHERE id = $2 AND user_id = $3 AND status IN ('pending', 'confirmed', 'in_progress') `, time.Now(), bookingID, userID) if err != nil { log.Printf("Failed to cancel booking %s: %v", bookingID, err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } rowsAffected := res.RowsAffected() if rowsAffected == 0 { http.Error(w, "Booking not cancellable", http.StatusNotFound) return } w.WriteHeader(http.StatusNoContent) } // AdminCancelBookingHandler allows an admin to cancel any booking. // The update uses a status filter and checks RowsAffected for existence. func AdminCancelBookingHandler(w http.ResponseWriter, r *http.Request) { bookingID := chi.URLParam(r, "id") res, err := db.DB.Exec(r.Context(), ` UPDATE bookings SET status = 'we_cancelled', updated_at = $1 WHERE id = $2 AND status IN ('pending', 'confirmed', 'in_progress') `, time.Now(), bookingID) if err != nil { log.Printf("Failed to admin cancel booking %s: %v", bookingID, err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } rowsAffected := res.RowsAffected() if rowsAffected == 0 { http.Error(w, "Booking not cancellable", http.StatusNotFound) return } w.WriteHeader(http.StatusNoContent) } // AdminListPendingBookingsHandler returns all bookings with status `pending` by delegating to the existing admin list handler. func AdminListPendingBookingsHandler(w http.ResponseWriter, r *http.Request) { r = r.Clone(r.Context()) q := r.URL.Query() q.Set("status", "pending") r.URL.RawQuery = q.Encode() GetAllAdminBookingsHandler(w, r) } // AdminGetInProgressBookingHandler returns the booking that is currently in progress. // It joins the bookings table with users to populate the UserSummary in the returned Booking. func AdminGetInProgressBookingHandler(w http.ResponseWriter, r *http.Request) { var b Booking var userID, fullName string err := db.DB.QueryRow(r.Context(), ` SELECT b.id, b.start_time, b.status, b.notes, b.created_at, b.updated_at, b.created_by, u.id, u.fn FROM bookings b LEFT JOIN users u ON b.user_id = u.id WHERE b.status = 'in_progress' ORDER BY b.start_time LIMIT 1 `).Scan( &b.ID, &b.StartTime, &b.Status, &b.Notes, &b.CreatedAt, &b.UpdatedAt, &b.CreatedBy, &userID, &fullName, ) if err != nil { if err == sql.ErrNoRows { http.Error(w, "No in-progress booking found", http.StatusNotFound) return } log.Printf("Failed to fetch in-progress booking: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } // Populate the UserSummary field b.User = &UserSummary{ ID: userID, FullName: fullName, FirstName: "", // not available here LastName: "", // not available here } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(b); err != nil { log.Printf("Failed to encode booking response: %v", err) } } // AdminEditBookingHandler allows an admin to modify the start time of any booking. // It validates the new start time and returns 404 if the booking does not exist. func AdminEditBookingHandler(w http.ResponseWriter, r *http.Request) { bookingID := chi.URLParam(r, "id") var req EditBookingRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } // Basic validation: ensure the new time is not in the past if time.Now().After(req.StartTime) { http.Error(w, "Start time must be in the future", http.StatusBadRequest) return } res, err := db.DB.Exec(r.Context(), ` UPDATE bookings SET start_time = $1, updated_at = $2 WHERE id = $3 `, req.StartTime, time.Now(), bookingID) if err != nil { log.Printf("Failed to edit booking %s: %v", bookingID, err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } rowsAffected := res.RowsAffected() if rowsAffected == 0 { http.Error(w, "Booking not found", http.StatusNotFound) return } 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) return } adminID, _ := r.Context().Value(mw.UserIDKey).(string) tx, err := db.DB.Begin(r.Context()) if err != nil { log.Printf("Failed to begin 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) if err != nil { log.Printf("Failed to create 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) 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) 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 { log.Printf("Failed to encode response: %v", err) } }