package appeal import ( "encoding/json" "net/http" "strconv" "strings" "api_yal/internal/logger" "api_yal/internal/middleware" "api_yal/internal/models" "github.com/go-chi/chi/v5" "go.uber.org/zap" ) // Handler HTTP обработчик для обращений type Handler struct { service AppealService logger *zap.Logger } // NewHandler создает новый экземпляр обработчика func NewHandler(service AppealService) *Handler { return &Handler{ service: service, logger: logger.Get(), } } // CreateAppeal создает новое обращение // @Summary Создать обращение // @Description Создает новое обращение (жалобу, предложение и т.д.) // @Tags appeals // @Accept json // @Produce json // @Param request body CreateAppealRequest true "Данные обращения" // @Success 201 {object} AppealResponse // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Router /appeals [post] func (h *Handler) CreateAppeal(w http.ResponseWriter, r *http.Request) { var req CreateAppealRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.logger.Error("Failed to decode request", zap.Error(err)) respondWithError(w, http.StatusBadRequest, "Invalid request body") return } // Получаем ID автора из контекста (если авторизован) authorID, isAuth := middleware.GetUserID(r.Context()) var authorIDPtr *uint if isAuth { authorIDPtr = &authorID } // Получаем IP и User-Agent ipAddress := r.RemoteAddr if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" { ipAddress = strings.Split(forwarded, ",")[0] } userAgent := r.Header.Get("User-Agent") appeal, err := h.service.Create(&req, authorIDPtr, ipAddress, userAgent) if err != nil { h.logger.Error("Failed to create appeal", zap.Error(err)) respondWithError(w, http.StatusBadRequest, err.Error()) return } respondWithJSON(w, http.StatusCreated, NewAppealResponse(appeal)) } // GetAppeal возвращает обращение по ID // @Summary Получить обращение // @Description Возвращает обращение по ID // @Tags appeals // @Produce json // @Param id path int true "ID обращения" // @Success 200 {object} AppealResponse // @Failure 404 {object} map[string]string // @Router /appeals/{id} [get] func (h *Handler) GetAppeal(w http.ResponseWriter, r *http.Request) { id, err := parseIDParam(r, "id") if err != nil { respondWithError(w, http.StatusBadRequest, "Invalid appeal ID") return } appeal, err := h.service.GetByID(id) if err != nil { h.logger.Error("Failed to get appeal", zap.Error(err), zap.Uint("id", id)) respondWithError(w, http.StatusNotFound, "Appeal not found") return } // Проверка прав доступа userID, _ := middleware.GetUserID(r.Context()) userRole, _ := middleware.GetUserRole(r.Context()) if !canAccessAppeal(appeal, userID, userRole) { respondWithError(w, http.StatusForbidden, "Access denied") return } respondWithJSON(w, http.StatusOK, NewAppealResponse(appeal)) } // UpdateAppeal обновляет обращение // @Summary Обновить обращение // @Description Обновляет существующее обращение // @Tags appeals // @Accept json // @Produce json // @Param id path int true "ID обращения" // @Param request body UpdateAppealRequest true "Данные для обновления" // @Success 200 {object} AppealResponse // @Failure 400 {object} map[string]string // @Failure 403 {object} map[string]string // @Router /appeals/{id} [put] func (h *Handler) UpdateAppeal(w http.ResponseWriter, r *http.Request) { id, err := parseIDParam(r, "id") if err != nil { respondWithError(w, http.StatusBadRequest, "Invalid appeal ID") return } var req UpdateAppealRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondWithError(w, http.StatusBadRequest, "Invalid request body") return } userID, _ := middleware.GetUserID(r.Context()) userRole, _ := middleware.GetUserRole(r.Context()) isAdmin := userRole == "admin" appeal, err := h.service.Update(id, &req, userID, isAdmin) if err != nil { h.logger.Error("Failed to update appeal", zap.Error(err), zap.Uint("id", id)) if err.Error() == "permission denied" { respondWithError(w, http.StatusForbidden, err.Error()) return } respondWithError(w, http.StatusBadRequest, err.Error()) return } respondWithJSON(w, http.StatusOK, NewAppealResponse(appeal)) } // DeleteAppeal удаляет обращение // @Summary Удалить обращение // @Description Мягкое удаление обращения // @Tags appeals // @Param id path int true "ID обращения" // @Success 204 "No Content" // @Failure 403 {object} map[string]string // @Router /appeals/{id} [delete] func (h *Handler) DeleteAppeal(w http.ResponseWriter, r *http.Request) { id, err := parseIDParam(r, "id") if err != nil { respondWithError(w, http.StatusBadRequest, "Invalid appeal ID") return } userID, _ := middleware.GetUserID(r.Context()) userRole, _ := middleware.GetUserRole(r.Context()) isAdmin := userRole == "admin" if err := h.service.Delete(id, userID, isAdmin); err != nil { h.logger.Error("Failed to delete appeal", zap.Error(err), zap.Uint("id", id)) if err.Error() == "permission denied" { respondWithError(w, http.StatusForbidden, err.Error()) return } respondWithError(w, http.StatusNotFound, err.Error()) return } w.WriteHeader(http.StatusNoContent) } // ListAppeals возвращает список обращений // @Summary Список обращений // @Description Возвращает список обращений с пагинацией и фильтрацией // @Tags appeals // @Produce json // @Param offset query int false "Смещение" default(0) // @Param limit query int false "Лимит" default(20) // @Param status query string false "Статус" Enums(new, in_progress, resolved, rejected, closed) // @Param type query string false "Тип" Enums(complaint, suggestion, wish, question, other) // @Param priority query string false "Приоритет" Enums(low, medium, high, critical) // @Param search query string false "Поиск по заголовку/сообщению" // @Success 200 {object} ListAppealsResponse // @Router /appeals [get] func (h *Handler) ListAppeals(w http.ResponseWriter, r *http.Request) { offset, limit := parsePagination(r) // Собираем фильтры filters := make(map[string]interface{}) if status := r.URL.Query().Get("status"); status != "" { filters["status"] = status } if appealType := r.URL.Query().Get("type"); appealType != "" { filters["type"] = appealType } if priority := r.URL.Query().Get("priority"); priority != "" { filters["priority"] = priority } if search := r.URL.Query().Get("search"); search != "" { filters["search"] = search } appeals, total, err := h.service.List(offset, limit, filters) if err != nil { h.logger.Error("Failed to list appeals", zap.Error(err)) respondWithError(w, http.StatusInternalServerError, "Failed to retrieve appeals") return } data := make([]AppealResponse, len(appeals)) for i, appeal := range appeals { data[i] = NewAppealResponse(&appeal) } totalPages := int(total) / limit if int(total)%limit > 0 { totalPages++ } respondWithJSON(w, http.StatusOK, ListAppealsResponse{ Data: data, Total: total, Offset: offset, Limit: limit, TotalPages: totalPages, }) } // UpdateAppealStatus обновляет статус обращения // @Summary Обновить статус // @Description Обновляет статус обращения (требует прав администратора или модератора) // @Tags appeals // @Accept json // @Produce json // @Param id path int true "ID обращения" // @Param request body UpdateStatusRequest true "Новый статус" // @Success 200 {object} map[string]string // @Failure 400 {object} map[string]string // @Router /appeals/{id}/status [patch] func (h *Handler) UpdateAppealStatus(w http.ResponseWriter, r *http.Request) { id, err := parseIDParam(r, "id") if err != nil { respondWithError(w, http.StatusBadRequest, "Invalid appeal ID") return } var req UpdateStatusRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondWithError(w, http.StatusBadRequest, "Invalid request body") return } userID, _ := middleware.GetUserID(r.Context()) userRole, _ := middleware.GetUserRole(r.Context()) // Только администраторы и модераторы могут менять статус if userRole != "admin" && userRole != "moderator" { respondWithError(w, http.StatusForbidden, "Permission denied") return } if err := h.service.UpdateStatus(id, &req, userID); err != nil { h.logger.Error("Failed to update status", zap.Error(err), zap.Uint("id", id)) respondWithError(w, http.StatusBadRequest, err.Error()) return } respondWithJSON(w, http.StatusOK, map[string]string{"message": "Status updated successfully"}) } // AssignAppeal назначает ответственного // @Summary Назначить ответственного // @Description Назначает ответственного за обработку обращения // @Tags appeals // @Accept json // @Produce json // @Param id path int true "ID обращения" // @Param request body AssignRequest true "ID ответственного" // @Success 200 {object} map[string]string // @Router /appeals/{id}/assign [post] func (h *Handler) AssignAppeal(w http.ResponseWriter, r *http.Request) { id, err := parseIDParam(r, "id") if err != nil { respondWithError(w, http.StatusBadRequest, "Invalid appeal ID") return } var req AssignRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondWithError(w, http.StatusBadRequest, "Invalid request body") return } userID, _ := middleware.GetUserID(r.Context()) userRole, _ := middleware.GetUserRole(r.Context()) if userRole != "admin" && userRole != "moderator" { respondWithError(w, http.StatusForbidden, "Permission denied") return } if err := h.service.AssignTo(id, &req, userID); err != nil { h.logger.Error("Failed to assign appeal", zap.Error(err), zap.Uint("id", id)) respondWithError(w, http.StatusBadRequest, err.Error()) return } respondWithJSON(w, http.StatusOK, map[string]string{"message": "Assigned successfully"}) } // ResolveAppeal решает обращение // @Summary Решить обращение // @Description Отмечает обращение как решенное с указанием решения // @Tags appeals // @Accept json // @Produce json // @Param id path int true "ID обращения" // @Param request body ResolveRequest true "Решение" // @Success 200 {object} map[string]string // @Router /appeals/{id}/resolve [post] func (h *Handler) ResolveAppeal(w http.ResponseWriter, r *http.Request) { id, err := parseIDParam(r, "id") if err != nil { respondWithError(w, http.StatusBadRequest, "Invalid appeal ID") return } var req ResolveRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { respondWithError(w, http.StatusBadRequest, "Invalid request body") return } userID, _ := middleware.GetUserID(r.Context()) userRole, _ := middleware.GetUserRole(r.Context()) if userRole != "admin" && userRole != "moderator" { respondWithError(w, http.StatusForbidden, "Permission denied") return } if err := h.service.Resolve(id, &req, userID); err != nil { h.logger.Error("Failed to resolve appeal", zap.Error(err), zap.Uint("id", id)) respondWithError(w, http.StatusBadRequest, err.Error()) return } respondWithJSON(w, http.StatusOK, map[string]string{"message": "Appeal resolved successfully"}) } // GetAppealHistory возвращает историю изменений обращения // @Summary История изменений // @Description Возвращает историю изменений статуса обращения // @Tags appeals // @Produce json // @Param id path int true "ID обращения" // @Param offset query int false "Смещение" default(0) // @Param limit query int false "Лимит" default(20) // @Success 200 {object} ListHistoryResponse // @Router /appeals/{id}/history [get] func (h *Handler) GetAppealHistory(w http.ResponseWriter, r *http.Request) { id, err := parseIDParam(r, "id") if err != nil { respondWithError(w, http.StatusBadRequest, "Invalid appeal ID") return } offset, limit := parsePagination(r) histories, total, err := h.service.GetHistory(id, offset, limit) if err != nil { h.logger.Error("Failed to get history", zap.Error(err), zap.Uint("id", id)) respondWithError(w, http.StatusInternalServerError, "Failed to retrieve history") return } data := make([]AppealHistoryResponse, len(histories)) for i, history := range histories { data[i] = NewAppealHistoryResponse(&history) } totalPages := int(total) / limit if int(total)%limit > 0 { totalPages++ } respondWithJSON(w, http.StatusOK, ListHistoryResponse{ Data: data, Total: total, Offset: offset, Limit: limit, TotalPages: totalPages, }) } // GetAppealStatistics возвращает статистику по обращениям // @Summary Статистика обращений // @Description Возвращает статистику по обращениям (требует прав администратора) // @Tags appeals // @Produce json // @Success 200 {object} AppealStatisticsResponse // @Router /appeals/statistics [get] func (h *Handler) GetAppealStatistics(w http.ResponseWriter, r *http.Request) { userRole, _ := middleware.GetUserRole(r.Context()) if userRole != "admin" { respondWithError(w, http.StatusForbidden, "Admin access required") return } stats, err := h.service.GetStatistics() if err != nil { h.logger.Error("Failed to get statistics", zap.Error(err)) respondWithError(w, http.StatusInternalServerError, "Failed to retrieve statistics") return } respondWithJSON(w, http.StatusOK, stats) } // GetMyAppeals возвращает обращения текущего пользователя // @Summary Мои обращения // @Description Возвращает обращения текущего авторизованного пользователя // @Tags appeals // @Produce json // @Param offset query int false "Смещение" default(0) // @Param limit query int false "Лимит" default(20) // @Success 200 {object} ListAppealsResponse // @Router /appeals/me [get] func (h *Handler) GetMyAppeals(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserID(r.Context()) if !ok { respondWithError(w, http.StatusUnauthorized, "User not authenticated") return } offset, limit := parsePagination(r) appeals, total, err := h.service.GetMyAppeals(userID, offset, limit) if err != nil { h.logger.Error("Failed to get my appeals", zap.Error(err), zap.Uint("user_id", userID)) respondWithError(w, http.StatusInternalServerError, "Failed to retrieve appeals") return } data := make([]AppealResponse, len(appeals)) for i, appeal := range appeals { data[i] = NewAppealResponse(&appeal) } totalPages := int(total) / limit if int(total)%limit > 0 { totalPages++ } respondWithJSON(w, http.StatusOK, ListAppealsResponse{ Data: data, Total: total, Offset: offset, Limit: limit, TotalPages: totalPages, }) } // GetAppealsByAuthor возвращает обращения пользователя по ID // @Summary Обращения пользователя // @Description Возвращает обращения указанного пользователя (админ доступ) // @Tags appeals // @Produce json // @Param userID path int true "ID пользователя" // @Param offset query int false "Смещение" default(0) // @Param limit query int false "Лимит" default(20) // @Success 200 {object} ListAppealsResponse // @Router /appeals/user/{userID} [get] func (h *Handler) GetAppealsByAuthor(w http.ResponseWriter, r *http.Request) { userRole, _ := middleware.GetUserRole(r.Context()) if userRole != "admin" { respondWithError(w, http.StatusForbidden, "Admin access required") return } authorID, err := parseIDParam(r, "userID") if err != nil { respondWithError(w, http.StatusBadRequest, "Invalid user ID") return } offset, limit := parsePagination(r) appeals, total, err := h.service.GetAppealsByAuthor(authorID, offset, limit) if err != nil { h.logger.Error("Failed to get appeals by author", zap.Error(err), zap.Uint("author_id", authorID)) respondWithError(w, http.StatusInternalServerError, "Failed to retrieve appeals") return } data := make([]AppealResponse, len(appeals)) for i, appeal := range appeals { data[i] = NewAppealResponse(&appeal) } totalPages := int(total) / limit if int(total)%limit > 0 { totalPages++ } respondWithJSON(w, http.StatusOK, ListAppealsResponse{ Data: data, Total: total, Offset: offset, Limit: limit, TotalPages: totalPages, }) } // Вспомогательные функции func parseIDParam(r *http.Request, param string) (uint, error) { idStr := chi.URLParam(r, param) id, err := strconv.ParseUint(idStr, 10, 32) if err != nil { return 0, err } return uint(id), nil } func parsePagination(r *http.Request) (offset, limit int) { offsetStr := r.URL.Query().Get("offset") limitStr := r.URL.Query().Get("limit") offset, _ = strconv.Atoi(offsetStr) limit, _ = strconv.Atoi(limitStr) if offset < 0 { offset = 0 } if limit <= 0 || limit > 100 { limit = 20 } return } func respondWithJSON(w http.ResponseWriter, status int, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if err := json.NewEncoder(w).Encode(data); err != nil { logger.Get().Error("Failed to encode JSON response", zap.Error(err)) } } func respondWithError(w http.ResponseWriter, status int, message string) { respondWithJSON(w, status, map[string]string{"error": message}) } func canAccessAppeal(appeal *models.Appeal, userID uint, userRole string) bool { // Администратор может видеть все if userRole == "admin" { return true } // Модератор может видеть все if userRole == "moderator" { return true } // Обычный пользователь может видеть только свои обращения if appeal.AuthorID != nil && *appeal.AuthorID == userID { return true } // Анонимные обращения могут видеть только админы и модераторы return false }