package api import ( "encoding/json" "errors" "net/http" "strconv" "strings" "dana/internal/storage" ) type specialtyRequest struct { Slug string `json:"slug"` Name string `json:"name"` NamePlural string `json:"name_plural"` Sort int `json:"sort"` } func decodeSpecialty(w http.ResponseWriter, r *http.Request, withSlug bool) (storage.SpecialtyInput, bool) { var req specialtyRequest if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10)).Decode(&req); err != nil { fail(w, http.StatusBadRequest, "bad_request", "Некорректный запрос") return storage.SpecialtyInput{}, false } in := storage.SpecialtyInput{ Slug: strings.ToLower(strings.TrimSpace(req.Slug)), Name: strings.TrimSpace(req.Name), NamePlural: strings.TrimSpace(req.NamePlural), Sort: req.Sort, } if in.Sort == 0 { in.Sort = 500 } if in.NamePlural == "" { in.NamePlural = in.Name } if in.Name == "" { in.Name = in.NamePlural } if in.Name == "" || len([]rune(in.Name)) > 100 || len([]rune(in.NamePlural)) > 100 { fail(w, http.StatusUnprocessableEntity, "validation", "Укажите название специальности (до 100 символов)") return in, false } if withSlug && (!slugRE.MatchString(in.Slug) || len(in.Slug) > 100) { fail(w, http.StatusUnprocessableEntity, "validation", "Адрес — латиница, цифры и дефис, например «kardiologi»") return in, false } return in, true } func (a *API) adminSpecialties(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) { list, err := a.store.AdminSpecialties(r.Context()) if err != nil { handleErr(w, err) return } ok(w, list, map[string]any{"total": len(list)}) } func (a *API) adminCreateSpecialty(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) { in, valid := decodeSpecialty(w, r, true) if !valid { return } id, err := a.store.AdminCreateSpecialty(r.Context(), in) if err != nil { handleErr(w, err) return } a.store.Audit(r.Context(), u.ID, "create", "specialty", id, clientIP(r)) writeJSON(w, http.StatusCreated, envelope{Data: map[string]any{"id": id}}) } func (a *API) adminUpdateSpecialty(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 := decodeSpecialty(w, r, false) if !valid { return } if err := a.store.AdminUpdateSpecialty(r.Context(), id, in); err != nil { handleErr(w, err) return } a.store.Audit(r.Context(), u.ID, "update", "specialty", id, clientIP(r)) ok(w, map[string]any{"id": id}, nil) } func (a *API) adminDeleteSpecialty(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.AdminDeleteSpecialty(r.Context(), id); err != nil { if errors.Is(err, storage.ErrInUse) { fail(w, http.StatusConflict, "in_use", "В этой специальности есть врачи — сначала перенесите или удалите их") return } handleErr(w, err) return } a.store.Audit(r.Context(), u.ID, "delete", "specialty", id, clientIP(r)) ok(w, map[string]any{"id": id}, nil) } type newDoctorRequest struct { Slug string `json:"slug"` LastName string `json:"last_name"` FirstName string `json:"first_name"` MiddleName string `json:"middle_name"` Position string `json:"position"` SpecialtyID int64 `json:"specialty_id"` } // adminCreateDoctor заводит врача скрытым; фото, биографию и прочее заполняют в карточке. func (a *API) adminCreateDoctor(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) { var req newDoctorRequest if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10)).Decode(&req); err != nil { fail(w, http.StatusBadRequest, "bad_request", "Некорректный запрос") return } in := storage.NewDoctor{ Slug: strings.ToLower(strings.TrimSpace(req.Slug)), LastName: strings.TrimSpace(req.LastName), FirstName: strings.TrimSpace(req.FirstName), MiddleName: strings.TrimSpace(req.MiddleName), Position: strings.TrimSpace(req.Position), SpecialtyID: req.SpecialtyID, } switch { case in.LastName == "" || in.FirstName == "": fail(w, http.StatusUnprocessableEntity, "validation", "Укажите фамилию и имя") return case in.SpecialtyID <= 0: fail(w, http.StatusUnprocessableEntity, "validation", "Выберите специальность — от неё зависит адрес страницы врача") return case !slugRE.MatchString(in.Slug) || len(in.Slug) > 100: fail(w, http.StatusUnprocessableEntity, "validation", "Адрес — латиница, цифры и дефис, например «ivanov-ivan»") return } id, err := a.store.AdminCreateDoctor(r.Context(), in) if err != nil { handleErr(w, err) return } a.store.Audit(r.Context(), u.ID, "create", "doctor", id, clientIP(r)) writeJSON(w, http.StatusCreated, envelope{Data: map[string]any{"id": id}}) }