package handlers import ( "encoding/json" "fmt" "net/http" "strconv" "git.kirlllll.ru/volontery/backend/internal/api/middleware" "git.kirlllll.ru/volontery/backend/internal/database" "git.kirlllll.ru/volontery/backend/internal/service" "github.com/go-chi/chi/v5" ) // RequestHandler обрабатывает запросы заявок type RequestHandler struct { requestService *service.RequestService } // NewRequestHandler создает новый RequestHandler func NewRequestHandler(requestService *service.RequestService) *RequestHandler { return &RequestHandler{ requestService: requestService, } } // CreateRequest создает новую заявку // POST /api/v1/requests func (h *RequestHandler) CreateRequest(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserIDFromContext(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "unauthorized") return } var input service.CreateRequestInput if err := json.NewDecoder(r.Body).Decode(&input); err != nil { respondError(w, http.StatusBadRequest, "invalid request body") return } input.RequesterID = userID request, err := h.requestService.CreateRequest(r.Context(), input) if err != nil { respondError(w, http.StatusBadRequest, err.Error()) return } respondJSON(w, http.StatusCreated, request) } // GetRequest получает заявку по ID // GET /api/v1/requests/{id} func (h *RequestHandler) GetRequest(w http.ResponseWriter, r *http.Request) { idStr := chi.URLParam(r, "id") requestID, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "invalid request id") return } request, err := h.requestService.GetRequest(r.Context(), requestID) if err != nil { respondError(w, http.StatusNotFound, "request not found") return } respondJSON(w, http.StatusOK, request) } // GetMyRequests получает заявки текущего пользователя // GET /api/v1/requests/my func (h *RequestHandler) GetMyRequests(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserIDFromContext(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "unauthorized") return } limit, offset := parsePagination(r) requests, err := h.requestService.GetUserRequests(r.Context(), userID, limit, offset) if err != nil { respondError(w, http.StatusInternalServerError, "failed to get requests") return } respondJSON(w, http.StatusOK, requests) } // FindNearbyRequests ищет заявки рядом с точкой // GET /api/v1/requests/nearby func (h *RequestHandler) FindNearbyRequests(w http.ResponseWriter, r *http.Request) { latStr := r.URL.Query().Get("lat") lonStr := r.URL.Query().Get("lon") radiusStr := r.URL.Query().Get("radius") if latStr == "" || lonStr == "" { respondError(w, http.StatusBadRequest, "lat and lon are required") return } lat, err := strconv.ParseFloat(latStr, 64) if err != nil { respondError(w, http.StatusBadRequest, "invalid latitude") return } lon, err := strconv.ParseFloat(lonStr, 64) if err != nil { respondError(w, http.StatusBadRequest, "invalid longitude") return } radius := 5000.0 // default 5km if radiusStr != "" { radius, err = strconv.ParseFloat(radiusStr, 64) if err != nil { respondError(w, http.StatusBadRequest, "invalid radius") return } } limit, offset := parsePagination(r) // По умолчанию ищем только активные заявки statuses := []database.RequestStatus{ database.RequestStatusPendingModeration, database.RequestStatusApproved, database.RequestStatusInProgress, } requests, err := h.requestService.FindNearbyRequests(r.Context(), lat, lon, radius, statuses, limit, offset) if err != nil { respondError(w, http.StatusInternalServerError, fmt.Sprintf("failed to find requests: %v", err)) return } respondJSON(w, http.StatusOK, requests) } // FindRequestsInBounds ищет заявки в прямоугольной области // GET /api/v1/requests/bounds func (h *RequestHandler) FindRequestsInBounds(w http.ResponseWriter, r *http.Request) { minLonStr := r.URL.Query().Get("min_lon") minLatStr := r.URL.Query().Get("min_lat") maxLonStr := r.URL.Query().Get("max_lon") maxLatStr := r.URL.Query().Get("max_lat") if minLonStr == "" || minLatStr == "" || maxLonStr == "" || maxLatStr == "" { respondError(w, http.StatusBadRequest, "min_lon, min_lat, max_lon, max_lat are required") return } minLon, err := strconv.ParseFloat(minLonStr, 64) if err != nil { respondError(w, http.StatusBadRequest, "invalid min_lon") return } minLat, err := strconv.ParseFloat(minLatStr, 64) if err != nil { respondError(w, http.StatusBadRequest, "invalid min_lat") return } maxLon, err := strconv.ParseFloat(maxLonStr, 64) if err != nil { respondError(w, http.StatusBadRequest, "invalid max_lon") return } maxLat, err := strconv.ParseFloat(maxLatStr, 64) if err != nil { respondError(w, http.StatusBadRequest, "invalid max_lat") return } // По умолчанию ищем только активные заявки statuses := []database.RequestStatus{ database.RequestStatusPendingModeration, database.RequestStatusApproved, database.RequestStatusInProgress, } requests, err := h.requestService.FindRequestsInBounds(r.Context(), statuses, minLon, minLat, maxLon, maxLat) if err != nil { respondError(w, http.StatusInternalServerError, fmt.Sprintf("failed to find requests: %v", err)) return } respondJSON(w, http.StatusOK, requests) } // CreateVolunteerResponse создает отклик волонтера на заявку // POST /api/v1/requests/{id}/responses func (h *RequestHandler) CreateVolunteerResponse(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserIDFromContext(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "unauthorized") return } idStr := chi.URLParam(r, "id") requestID, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "invalid request id") return } var input struct { Message string `json:"message"` } if err := json.NewDecoder(r.Body).Decode(&input); err != nil { respondError(w, http.StatusBadRequest, "invalid request body") return } response, err := h.requestService.CreateVolunteerResponse(r.Context(), requestID, userID, input.Message) if err != nil { respondError(w, http.StatusBadRequest, err.Error()) return } respondJSON(w, http.StatusCreated, response) } // GetRequestResponses получает отклики на заявку // GET /api/v1/requests/{id}/responses func (h *RequestHandler) GetRequestResponses(w http.ResponseWriter, r *http.Request) { idStr := chi.URLParam(r, "id") requestID, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "invalid request id") return } responses, err := h.requestService.GetRequestResponses(r.Context(), requestID) if err != nil { respondError(w, http.StatusInternalServerError, "failed to get responses") return } respondJSON(w, http.StatusOK, responses) } // ListRequestTypes получает список типов заявок // GET /api/v1/request-types func (h *RequestHandler) ListRequestTypes(w http.ResponseWriter, r *http.Request) { types, err := h.requestService.ListRequestTypes(r.Context()) if err != nil { respondError(w, http.StatusInternalServerError, "failed to get request types") return } respondJSON(w, http.StatusOK, types) } // GetPendingModerationRequests получает заявки на модерации // GET /api/v1/moderation/requests/pending func (h *RequestHandler) GetPendingModerationRequests(w http.ResponseWriter, r *http.Request) { limit, offset := parsePagination(r) requests, err := h.requestService.GetPendingModerationRequests(r.Context(), limit, offset) if err != nil { respondError(w, http.StatusInternalServerError, "failed to get pending requests") return } respondJSON(w, http.StatusOK, requests) } // ApproveRequest одобряет заявку // POST /api/v1/moderation/requests/{id}/approve func (h *RequestHandler) ApproveRequest(w http.ResponseWriter, r *http.Request) { moderatorID, ok := middleware.GetUserIDFromContext(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "unauthorized") return } idStr := chi.URLParam(r, "id") requestID, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "invalid request id") return } var input struct { Comment *string `json:"comment"` } if err := json.NewDecoder(r.Body).Decode(&input); err != nil { respondError(w, http.StatusBadRequest, "invalid request body") return } if err := h.requestService.ApproveRequest(r.Context(), requestID, moderatorID, input.Comment); err != nil { respondError(w, http.StatusBadRequest, err.Error()) return } respondJSON(w, http.StatusOK, map[string]string{"status": "approved"}) } // RejectRequest отклоняет заявку // POST /api/v1/moderation/requests/{id}/reject func (h *RequestHandler) RejectRequest(w http.ResponseWriter, r *http.Request) { moderatorID, ok := middleware.GetUserIDFromContext(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "unauthorized") return } idStr := chi.URLParam(r, "id") requestID, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "invalid request id") return } var input struct { Comment string `json:"comment"` } if err := json.NewDecoder(r.Body).Decode(&input); err != nil { respondError(w, http.StatusBadRequest, "invalid request body") return } if err := h.requestService.RejectRequest(r.Context(), requestID, moderatorID, input.Comment); err != nil { respondError(w, http.StatusBadRequest, err.Error()) return } respondJSON(w, http.StatusOK, map[string]string{"status": "rejected"}) } // GetMyModeratedRequests получает заявки, модерированные текущим модератором // GET /api/v1/moderation/requests/my func (h *RequestHandler) GetMyModeratedRequests(w http.ResponseWriter, r *http.Request) { moderatorID, ok := middleware.GetUserIDFromContext(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "unauthorized") return } limit, offset := parsePagination(r) requests, err := h.requestService.GetModeratedRequests(r.Context(), moderatorID, limit, offset) if err != nil { respondError(w, http.StatusInternalServerError, "failed to get moderated requests") return } respondJSON(w, http.StatusOK, requests) } // AcceptVolunteerResponseHandler принимает отклик волонтера // POST /api/v1/requests/{id}/responses/{response_id}/accept func (h *RequestHandler) AcceptVolunteerResponseHandler(w http.ResponseWriter, r *http.Request) { requesterID, ok := middleware.GetUserIDFromContext(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "unauthorized") return } responseIDStr := chi.URLParam(r, "response_id") responseID, err := strconv.ParseInt(responseIDStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "invalid response id") return } result, err := h.requestService.AcceptVolunteerResponse(r.Context(), responseID, requesterID) if err != nil { respondError(w, http.StatusBadRequest, err.Error()) return } if !result.Success { respondError(w, http.StatusBadRequest, result.Message) return } respondJSON(w, http.StatusOK, map[string]interface{}{ "success": result.Success, "message": result.Message, "request_id": result.RequestID, "volunteer_id": result.VolunteerID, }) } // CompleteRequestWithRatingHandler завершает заявку с рейтингом // POST /api/v1/requests/{id}/complete func (h *RequestHandler) CompleteRequestWithRatingHandler(w http.ResponseWriter, r *http.Request) { requesterID, ok := middleware.GetUserIDFromContext(r.Context()) if !ok { respondError(w, http.StatusUnauthorized, "unauthorized") return } idStr := chi.URLParam(r, "id") requestID, err := strconv.ParseInt(idStr, 10, 64) if err != nil { respondError(w, http.StatusBadRequest, "invalid request id") return } var input struct { Rating int32 `json:"rating"` Comment *string `json:"comment"` } if err := json.NewDecoder(r.Body).Decode(&input); err != nil { respondError(w, http.StatusBadRequest, "invalid request body") return } result, err := h.requestService.CompleteRequestWithRating(r.Context(), requestID, requesterID, input.Rating, input.Comment) if err != nil { respondError(w, http.StatusBadRequest, err.Error()) return } if !result.Success { respondError(w, http.StatusBadRequest, result.Message) return } respondJSON(w, http.StatusOK, map[string]interface{}{ "success": result.Success, "message": result.Message, "rating_id": result.RatingID, }) } // parsePagination парсит параметры пагинации из запроса func parsePagination(r *http.Request) (limit, offset int32) { limitStr := r.URL.Query().Get("limit") offsetStr := r.URL.Query().Get("offset") limit = 20 // default if limitStr != "" { if l, err := strconv.ParseInt(limitStr, 10, 32); err == nil && l > 0 && l <= 100 { limit = int32(l) } } offset = 0 if offsetStr != "" { if o, err := strconv.ParseInt(offsetStr, 10, 32); err == nil && o >= 0 { offset = int32(o) } } return limit, offset }