dana-medical-site/internal/api/admin_inbox.go
2026-10-14 13:33:13 +00:00

199 lines
6.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package api
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"time"
"dana/internal/storage"
)
// допустимые значения статусов — совпадают с CHECK в схеме
var leadStatuses = map[string]bool{"new": true, "in_progress": true, "done": true, "spam": true}
var reviewStatuses = map[string]bool{"pending": true, "published": true, "rejected": true}
func pageParams(r *http.Request) (limit, offset int) {
limit = queryInt(r, "limit", 50)
if limit <= 0 || limit > 200 {
limit = 50
}
offset = queryInt(r, "offset", 0)
if offset < 0 {
offset = 0
}
return limit, offset
}
// ---------- Заявки ----------
func (a *API) adminLeads(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) {
limit, offset := pageParams(r)
q := r.URL.Query()
var from, to *time.Time
for _, filter := range []struct {
key string
out **time.Time
}{{"from", &from}, {"to", &to}} {
if value := q.Get(filter.key); value != "" {
day, err := time.ParseInLocation("2006-01-02", value, time.Local)
if err != nil {
fail(w, http.StatusBadRequest, "bad_request", "Некорректная дата в фильтре")
return
}
if filter.key == "to" {
day = day.AddDate(0, 0, 1)
}
*filter.out = &day
}
}
if from != nil && to != nil && !from.Before(*to) {
fail(w, http.StatusBadRequest, "bad_request", "Дата начала позже даты окончания")
return
}
list, total, err := a.store.AdminLeads(r.Context(), q.Get("status"), q.Get("q"), from, to, limit, offset)
if err != nil {
handleErr(w, err)
return
}
counts, err := a.store.LeadCounts(r.Context())
if err != nil {
handleErr(w, err)
return
}
today, err := a.store.LeadTodayCount(r.Context())
if err != nil {
handleErr(w, err)
return
}
// Просмотр заявок — это доступ к персональным данным, фиксируем в журнале.
a.store.Audit(r.Context(), u.ID, "view_leads", "lead", 0, clientIP(r))
ok(w, list, map[string]any{"total": total, "limit": limit, "offset": offset, "counts": counts, "today": today})
}
func (a *API) adminUpdateLead(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
fail(w, http.StatusBadRequest, "bad_request", "Некорректный id")
return
}
var in storage.LeadUpdate
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
fail(w, http.StatusBadRequest, "bad_request", "Некорректный запрос")
return
}
if !leadStatuses[in.Status] {
fail(w, http.StatusUnprocessableEntity, "validation", "Недопустимый статус заявки")
return
}
if err := a.store.AdminUpdateLead(r.Context(), id, in); err != nil {
handleErr(w, err)
return
}
a.store.Audit(r.Context(), u.ID, "update", "lead", id, clientIP(r))
ok(w, map[string]any{"id": id}, nil)
}
// ---------- Отзывы ----------
func (a *API) adminReviews(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) {
limit, offset := pageParams(r)
q := r.URL.Query()
list, total, err := a.store.AdminReviews(r.Context(), q.Get("status"), q.Get("q"), limit, offset)
if err != nil {
handleErr(w, err)
return
}
counts, err := a.store.ReviewCounts(r.Context())
if err != nil {
handleErr(w, err)
return
}
ok(w, list, map[string]any{"total": total, "limit": limit, "offset": offset, "counts": counts})
}
func decodeReview(w http.ResponseWriter, r *http.Request) (storage.ReviewUpdate, bool) {
var in storage.ReviewUpdate
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&in); err != nil {
fail(w, http.StatusBadRequest, "bad_request", "Некорректный запрос")
return in, false
}
in.AuthorName = strings.TrimSpace(in.AuthorName)
in.Text = strings.TrimSpace(strings.ReplaceAll(in.Text, "\r", ""))
in.Reply = strings.TrimSpace(strings.ReplaceAll(in.Reply, "\r", ""))
if !reviewStatuses[in.Status] {
fail(w, http.StatusUnprocessableEntity, "validation", "Недопустимый статус отзыва")
return in, false
}
video, err := embedVideoURL(in.VideoURL)
if err != nil {
fail(w, http.StatusUnprocessableEntity, "validation",
"Видео — ссылка на YouTube или Rutube, например https://youtu.be/…")
return in, false
}
in.VideoURL = video
// У видео-отзывов текста нет — содержимое там в ролике.
if in.Text == "" && in.VideoURL == "" {
fail(w, http.StatusUnprocessableEntity, "validation",
"Отзыв должен содержать текст или ссылку на видео")
return in, false
}
if in.AuthorName == "" {
fail(w, http.StatusUnprocessableEntity, "validation", "Укажите имя автора отзыва")
return in, false
}
if in.Rating != nil && (*in.Rating < 1 || *in.Rating > 5) {
fail(w, http.StatusUnprocessableEntity, "validation", "Оценка должна быть от 1 до 5")
return in, false
}
return in, true
}
// adminCreateReview — отзыв, записанный со слов пациента или видеоотзыв с YouTube.
func (a *API) adminCreateReview(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) {
in, valid := decodeReview(w, r)
if !valid {
return
}
id, err := a.store.AdminCreateReview(r.Context(), in)
if err != nil {
handleErr(w, err)
return
}
a.store.Audit(r.Context(), u.ID, "create", "review", id, clientIP(r))
writeJSON(w, http.StatusCreated, envelope{Data: map[string]any{"id": id}})
}
func (a *API) adminUpdateReview(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
fail(w, http.StatusBadRequest, "bad_request", "Некорректный id")
return
}
in, valid := decodeReview(w, r)
if !valid {
return
}
if err := a.store.AdminUpdateReview(r.Context(), id, in); err != nil {
handleErr(w, err)
return
}
a.store.Audit(r.Context(), u.ID, "update", "review", id, clientIP(r))
ok(w, map[string]any{"id": id}, nil)
}
func (a *API) adminDeleteReview(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
fail(w, http.StatusBadRequest, "bad_request", "Некорректный id")
return
}
if err := a.store.AdminDeleteReview(r.Context(), id); err != nil {
handleErr(w, err)
return
}
a.store.Audit(r.Context(), u.ID, "delete", "review", id, clientIP(r))
ok(w, map[string]any{"id": id}, nil)
}