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

231 lines
7.3 KiB
Go
Raw 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"
"errors"
"net/http"
"strconv"
"strings"
"dana/internal/storage"
)
// documentKinds — допустимые виды документов (как в CHECK таблицы documents).
var documentKinds = map[string]bool{
"license": true, "certificate": true, "award": true, "supervisor": true, "other": true,
}
type documentRequest struct {
Kind string `json:"kind"`
Title string `json:"title"`
Body string `json:"body"`
FileID *int64 `json:"file_id"`
PreviewID *int64 `json:"preview_id"`
IssuedAt *string `json:"issued_at"`
Sort int `json:"sort"`
Published bool `json:"published"`
}
func decodeDocument(w http.ResponseWriter, r *http.Request) (storage.DocumentInput, bool) {
var req documentRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&req); err != nil {
fail(w, http.StatusBadRequest, "bad_request", "Некорректный запрос")
return storage.DocumentInput{}, false
}
in := storage.DocumentInput{
Kind: req.Kind,
Title: strings.TrimSpace(req.Title),
Body: strings.TrimSpace(strings.ReplaceAll(req.Body, "\r", "")),
FileID: req.FileID,
PreviewID: req.PreviewID,
Sort: req.Sort,
Published: req.Published,
}
if in.Sort == 0 {
in.Sort = 100
}
if !documentKinds[in.Kind] {
fail(w, http.StatusUnprocessableEntity, "validation", "Выберите вид документа")
return in, false
}
if in.Title == "" {
fail(w, http.StatusUnprocessableEntity, "validation", "Укажите название")
return in, false
}
var err error
if in.IssuedAt, err = parseDate(req.IssuedAt); err != nil {
fail(w, http.StatusUnprocessableEntity, "validation", "Дата выдачи в формате ГГГГ-ММ-ДД")
return in, false
}
// лицензию или награду без скана и файла на сайте показать нечем
if in.Published && in.Kind != "supervisor" && in.FileID == nil && in.PreviewID == nil {
fail(w, http.StatusUnprocessableEntity, "validation", "Чтобы показать документ на сайте, загрузите скан или PDF")
return in, false
}
return in, true
}
// checkDocumentMedia не даёт поставить PDF на место картинки-скана.
func (a *API) checkDocumentMedia(w http.ResponseWriter, r *http.Request, in storage.DocumentInput) bool {
if in.PreviewID == nil {
return true
}
mime, err := a.store.MediaMime(r.Context(), *in.PreviewID)
if errors.Is(err, storage.ErrNotFound) || (err == nil && !strings.HasPrefix(mime, "image/")) {
fail(w, http.StatusUnprocessableEntity, "validation", "Скан для просмотра должен быть картинкой, а PDF загрузите в поле «Файл документа»")
return false
}
if err != nil {
handleErr(w, err)
return false
}
return true
}
func (a *API) adminDocuments(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) {
list, err := a.store.AdminDocuments(r.Context())
if err != nil {
handleErr(w, err)
return
}
ok(w, list, map[string]any{"total": len(list)})
}
func (a *API) adminCreateDocument(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) {
in, valid := decodeDocument(w, r)
if !valid || !a.checkDocumentMedia(w, r, in) {
return
}
id, err := a.store.AdminCreateDocument(r.Context(), in)
if err != nil {
handleErr(w, err)
return
}
a.store.Audit(r.Context(), u.ID, "create", "document", id, clientIP(r))
writeJSON(w, http.StatusCreated, envelope{Data: map[string]any{"id": id}})
}
func (a *API) adminUpdateDocument(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 := decodeDocument(w, r)
if !valid || !a.checkDocumentMedia(w, r, in) {
return
}
if err := a.store.AdminUpdateDocument(r.Context(), id, in); err != nil {
handleErr(w, err)
return
}
a.store.Audit(r.Context(), u.ID, "update", "document", id, clientIP(r))
ok(w, map[string]any{"id": id}, nil)
}
func (a *API) adminDeleteDocument(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.AdminDeleteDocument(r.Context(), id); err != nil {
handleErr(w, err)
return
}
a.store.Audit(r.Context(), u.ID, "delete", "document", id, clientIP(r))
ok(w, map[string]any{"id": id}, nil)
}
// ---------- вопросы и ответы ----------
type faqRequest struct {
Question string `json:"question"`
Answer string `json:"answer"`
ServiceID *int64 `json:"service_id"`
Sort int `json:"sort"`
Published bool `json:"published"`
}
func decodeFAQ(w http.ResponseWriter, r *http.Request) (storage.FAQInput, bool) {
var req faqRequest
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&req); err != nil {
fail(w, http.StatusBadRequest, "bad_request", "Некорректный запрос")
return storage.FAQInput{}, false
}
in := storage.FAQInput{
Question: strings.TrimSpace(req.Question),
Answer: strings.TrimSpace(strings.ReplaceAll(req.Answer, "\r", "")),
ServiceID: req.ServiceID,
Sort: req.Sort,
Published: req.Published,
}
if in.Sort == 0 {
in.Sort = 100
}
if in.Question == "" {
fail(w, http.StatusUnprocessableEntity, "validation", "Укажите вопрос")
return in, false
}
if in.Published && in.Answer == "" {
fail(w, http.StatusUnprocessableEntity, "validation", "Чтобы показать вопрос на сайте, напишите ответ")
return in, false
}
return in, true
}
func (a *API) adminFAQ(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) {
list, err := a.store.AdminFAQ(r.Context())
if err != nil {
handleErr(w, err)
return
}
ok(w, list, map[string]any{"total": len(list)})
}
func (a *API) adminCreateFAQ(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) {
in, valid := decodeFAQ(w, r)
if !valid {
return
}
id, err := a.store.AdminCreateFAQ(r.Context(), in)
if err != nil {
handleErr(w, err)
return
}
a.store.Audit(r.Context(), u.ID, "create", "faq", id, clientIP(r))
writeJSON(w, http.StatusCreated, envelope{Data: map[string]any{"id": id}})
}
func (a *API) adminUpdateFAQ(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 := decodeFAQ(w, r)
if !valid {
return
}
if err := a.store.AdminUpdateFAQ(r.Context(), id, in); err != nil {
handleErr(w, err)
return
}
a.store.Audit(r.Context(), u.ID, "update", "faq", id, clientIP(r))
ok(w, map[string]any{"id": id}, nil)
}
func (a *API) adminDeleteFAQ(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.AdminDeleteFAQ(r.Context(), id); err != nil {
handleErr(w, err)
return
}
a.store.Audit(r.Context(), u.ID, "delete", "faq", id, clientIP(r))
ok(w, map[string]any{"id": id}, nil)
}