package services import ( "crussell/db" "crussell/mw" "database/sql" "encoding/json" "net/http" "time" "github.com/go-chi/chi/v5" ) // Service represents a service in the system type Service struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` Price float64 `json:"price"` DurationMinutes int `json:"duration_minutes"` IsActive bool `json:"is_active"` PatchTestDurationHours int `json:"patch_test_duration_hours"` MinimumAgeRequired int `json:"minimum_age_required"` CreatedAt time.Time `json:"created_at"` CreatedBy *string `json:"created_by,omitempty"` } type ServiceResponse struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` Price float64 `json:"price"` DurationMinutes int `json:"duration_minutes"` PatchTestDurationHours int `json:"patch_test_duration_hours"` MinimumAgeRequired int `json:"minimum_age_required"` } // CreateServiceRequest represents the request payload for creating a new service type CreateServiceRequest struct { Name string `json:"name" validate:"required,min=1,max=100"` Description *string `json:"description,omitempty"` Price float64 `json:"price" validate:"required,gt=0"` DurationMinutes int `json:"duration_minutes" validate:"required,gt=0"` PatchTestDurationHours int `json:"patch_test_duration_hours" validate:"gte=0"` MinimumAgeRequired int `json:"minimum_age_required" validate:"gte=0,lte=100"` } // ToggleServiceHandler handles toggling a service's active status func ToggleService(w http.ResponseWriter, r *http.Request) { serviceID := chi.URLParam(r, "id") if serviceID == "" { http.Error(w, "Service ID is required", http.StatusBadRequest) return } query := "UPDATE services SET is_active = NOT is_active WHERE id = $1" result, err := db.DB.Exec(r.Context(), query, serviceID) if err != nil { http.Error(w, "Failed to toggle service: "+err.Error(), http.StatusInternalServerError) return } if result.RowsAffected() == 0 { http.Error(w, "Service not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Service toggled successfully", "id": serviceID, }) } // POST /api/admin/services func CreateServiceHandler(w http.ResponseWriter, r *http.Request) { // Parse and validate request var req CreateServiceRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest) return } // Basic validation if req.Name == "" { http.Error(w, "Name is required", http.StatusBadRequest) return } if len(req.Name) > 100 { http.Error(w, "Name must be 100 characters or less", http.StatusBadRequest) return } if req.Price <= 0 { http.Error(w, "Price must be greater than 0", http.StatusBadRequest) return } if req.DurationMinutes <= 0 || req.DurationMinutes > 480 { http.Error(w, "Duration must be between 1 and 480 minutes", http.StatusBadRequest) return } if req.PatchTestDurationHours < 0 || req.PatchTestDurationHours > 168 { http.Error(w, "Patch test duration must be between 0 and 168 hours", http.StatusBadRequest) return } if req.MinimumAgeRequired < 0 || req.MinimumAgeRequired > 100 { http.Error(w, "Minimum age must be between 0 and 100", http.StatusBadRequest) return } // Get user ID from context (if authentication is added later) var createdBy *string if userID, ok := r.Context().Value(mw.UserIDKey).(string); ok { createdBy = &userID } // Insert new service query := ` INSERT INTO services ( name, description, price, duration_minutes, patch_test_duration_hours, minimum_age_required, created_by ) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required, created_at, created_by ` var service Service var createdByDB sql.NullString err := db.DB.QueryRow(r.Context(), query, req.Name, req.Description, req.Price, req.DurationMinutes, req.PatchTestDurationHours, req.MinimumAgeRequired, createdBy, ).Scan( &service.ID, &service.Name, &service.Description, &service.Price, &service.DurationMinutes, &service.IsActive, &service.PatchTestDurationHours, &service.MinimumAgeRequired, &service.CreatedAt, &createdByDB, ) if err != nil { // Check for duplicate name or other constraints if err.Error() == "pq: duplicate key value violates unique constraint" { http.Error(w, "A service with this name already exists", http.StatusConflict) return } http.Error(w, "Failed to create service: "+err.Error(), http.StatusInternalServerError) return } // Convert nullable fields to pointers if createdByDB.Valid { service.CreatedBy = &createdByDB.String } // Return created service w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) if err := json.NewEncoder(w).Encode(service); err != nil { http.Error(w, "Failed to encode response: "+err.Error(), http.StatusInternalServerError) return } } // DeleteServiceHandler handles soft deleting a service (setting is_active to false) func DeleteServiceHandler(w http.ResponseWriter, r *http.Request) { serviceID := chi.URLParam(r, "id") if serviceID == "" { http.Error(w, "Service ID is required", http.StatusBadRequest) return } query := "DELETE FROM services WHERE id = $1" result, err := db.DB.Exec(r.Context(), query, serviceID) if err != nil { http.Error(w, "Failed to delete service: "+err.Error(), http.StatusInternalServerError) return } if result.RowsAffected() == 0 { http.Error(w, "Service not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Service deleted successfully", "id": serviceID, }) } // ServicesHandler returns all services from the database func ServicesHandler(w http.ResponseWriter, r *http.Request) { // Query all active services query := ` SELECT id, name, description, price, duration_minutes, patch_test_duration_hours, minimum_age_required FROM services WHERE is_active = TRUE ORDER BY name ` rows, err := db.DB.Query(r.Context(), query) if err != nil { http.Error(w, "Failed to fetch services: "+err.Error(), http.StatusInternalServerError) return } defer rows.Close() var services []ServiceResponse for rows.Next() { var service ServiceResponse err := rows.Scan( &service.ID, &service.Name, &service.Description, &service.Price, &service.DurationMinutes, &service.PatchTestDurationHours, &service.MinimumAgeRequired, ) if err != nil { http.Error(w, "Failed to read service data: "+err.Error(), http.StatusInternalServerError) return } services = append(services, service) } if err = rows.Err(); err != nil { http.Error(w, "Error iterating over services: "+err.Error(), http.StatusInternalServerError) return } // Set response headers w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) // Return empty array instead of null if no services found if services == nil { services = []ServiceResponse{} } // Encode response if err := json.NewEncoder(w).Encode(services); err != nil { http.Error(w, "Failed to encode response: "+err.Error(), http.StatusInternalServerError) return } } // AllServicesHandler returns all services including inactive ones (useful for admin) func AllServicesHandler(w http.ResponseWriter, r *http.Request) { // Query all services including inactive ones query := ` SELECT id, name, description, price, duration_minutes, is_active, patch_test_duration_hours, minimum_age_required, created_at, created_by FROM services ORDER BY is_active DESC, name ` rows, err := db.DB.Query(r.Context(), query) if err != nil { http.Error(w, "Failed to fetch services: "+err.Error(), http.StatusInternalServerError) return } defer rows.Close() var services []Service for rows.Next() { var service Service var createdBy sql.NullString err := rows.Scan( &service.ID, &service.Name, &service.Description, &service.Price, &service.DurationMinutes, &service.IsActive, &service.PatchTestDurationHours, &service.MinimumAgeRequired, &service.CreatedAt, &createdBy, ) if err != nil { http.Error(w, "Failed to read service data: "+err.Error(), http.StatusInternalServerError) return } // Convert nullable fields to pointers if createdBy.Valid { service.CreatedBy = &createdBy.String } services = append(services, service) } if err = rows.Err(); err != nil { http.Error(w, "Error iterating over services: "+err.Error(), http.StatusInternalServerError) return } // Set response headers w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) // Return empty array instead of null if no services found if services == nil { services = []Service{} } // Encode response if err := json.NewEncoder(w).Encode(services); err != nil { http.Error(w, "Failed to encode response: "+err.Error(), http.StatusInternalServerError) return } }