dana-medical-site/internal/storage/admin_doctors.go
2026-10-22 07:40:00 +00:00

250 lines
9.0 KiB
Go

package storage
import "context"
type AdminDoctor struct {
ID int64 `json:"id"`
Slug string `json:"slug"`
LastName string `json:"last_name"`
FirstName string `json:"first_name"`
MiddleName string `json:"middle_name"`
Position string `json:"position"`
Category string `json:"category"`
Degree string `json:"degree"`
PracticeYear *int `json:"practice_start_year"`
Rating *float64 `json:"rating"`
PhotoID *int64 `json:"photo_id"`
Photo string `json:"photo,omitempty"`
Quote string `json:"quote"`
Bio string `json:"bio"`
Specialties string `json:"specialties,omitempty"` // через запятую, для списка
// Основная специальность — раздел в адресе страницы: /company/staff/<раздел>/<slug>/.
SpecialtyID *int64 `json:"specialty_id"`
SpecialtySlug string `json:"specialty_slug"`
Booking string `json:"booking_provider"`
Sort int `json:"sort"`
Published bool `json:"published"`
Absent bool `json:"absent"`
ShowOnHome bool `json:"show_on_home"`
BioCount int `json:"bio_count"`
SEOTitle string `json:"seo_title"`
SEODesc string `json:"seo_description"`
SEOH1 string `json:"seo_h1"`
// Связи для вкладки «Врачи» в админке: какие услуги ведёт и в каких филиалах принимает.
ServiceIDs []int64 `json:"service_ids"`
BranchIDs []int64 `json:"branch_ids"`
}
func (s *Storage) AdminDoctors(ctx context.Context, q string) ([]AdminDoctor, error) {
rows, err := s.pool.Query(ctx, `
SELECT d.id, d.slug, d.last_name, d.first_name, d.middle_name,
d.position, d.category, d.degree, d.practice_start_year, d.rating_override::float8,
d.photo_id, COALESCE(m.path, ''), d.quote, d.bio,
COALESCE((
SELECT string_agg(sp.name, ', ' ORDER BY sp.name)
FROM doctor_specialties ds
JOIN specialties sp ON sp.id = ds.specialty_id
WHERE ds.doctor_id = d.id
), ''),
ps.id, COALESCE(ps.slug, ''),
d.booking_provider, d.sort, d.published,
d.absent, d.show_on_home,
(SELECT count(*) FROM doctor_bio_entries e WHERE e.doctor_id = d.id),
d.seo_title, d.seo_description, d.seo_h1,
ARRAY(SELECT service_id FROM doctor_services WHERE doctor_id = d.id ORDER BY service_id),
ARRAY(SELECT branch_id FROM doctor_branches WHERE doctor_id = d.id ORDER BY branch_id)
FROM doctors d
LEFT JOIN media m ON m.id = d.photo_id
LEFT JOIN LATERAL (
SELECT sp.id, sp.slug FROM doctor_specialties ds
JOIN specialties sp ON sp.id = ds.specialty_id
WHERE ds.doctor_id = d.id
ORDER BY ds.is_primary DESC, sp.sort, sp.id LIMIT 1
) ps ON true
WHERE d.deleted_at IS NULL
AND ($1 = '' OR d.last_name ILIKE '%' || $1 || '%'
OR d.first_name ILIKE '%' || $1 || '%'
OR d.position ILIKE '%' || $1 || '%')
ORDER BY d.sort, d.last_name, d.first_name`, q)
if err != nil {
return nil, err
}
defer rows.Close()
var out []AdminDoctor
for rows.Next() {
var a AdminDoctor
if err := rows.Scan(&a.ID, &a.Slug, &a.LastName, &a.FirstName, &a.MiddleName,
&a.Position, &a.Category, &a.Degree, &a.PracticeYear, &a.Rating,
&a.PhotoID, &a.Photo, &a.Quote, &a.Bio, &a.Specialties,
&a.SpecialtyID, &a.SpecialtySlug,
&a.Booking, &a.Sort, &a.Published,
&a.Absent, &a.ShowOnHome, &a.BioCount,
&a.SEOTitle, &a.SEODesc, &a.SEOH1, &a.ServiceIDs, &a.BranchIDs); err != nil {
return nil, err
}
if a.Booking != "native" {
a.Booking = "none"
}
out = append(out, a)
}
return out, rows.Err()
}
type DoctorUpdate struct {
Slug string `json:"slug"`
LastName string `json:"last_name"`
FirstName string `json:"first_name"`
MiddleName string `json:"middle_name"`
Position string `json:"position"`
Category string `json:"category"`
Degree string `json:"degree"`
PracticeYear *int `json:"practice_start_year"`
Rating *float64 `json:"rating"`
PhotoID *int64 `json:"photo_id"`
Quote string `json:"quote"`
Bio string `json:"bio"`
Booking string `json:"booking_provider"`
Sort int `json:"sort"`
Published bool `json:"published"`
Absent bool `json:"absent"`
ShowOnHome bool `json:"show_on_home"`
SEOTitle string `json:"seo_title"`
SEODesc string `json:"seo_description"`
SEOH1 string `json:"seo_h1"`
}
func (s *Storage) AdminUpdateDoctor(ctx context.Context, id int64, in DoctorUpdate) error {
tag, err := s.pool.Exec(ctx, `
UPDATE doctors SET slug=$2, last_name=$3, first_name=$4, middle_name=$5,
position=$6, category=$7, degree=$8, practice_start_year=$9, rating_override=$10,
quote=$11, bio=$12, booking_provider=$13, medflex_user=$14,
sort=$15, published=$16, absent=$17, show_on_home=$18,
seo_title=$19, seo_description=$20, seo_h1=$21, photo_id=$22, updated_at=now()
WHERE id=$1 AND deleted_at IS NULL`,
id, in.Slug, in.LastName, in.FirstName, in.MiddleName,
in.Position, in.Category, in.Degree, in.PracticeYear, in.Rating,
in.Quote, in.Bio, in.Booking, "",
in.Sort, in.Published, in.Absent, in.ShowOnHome,
in.SEOTitle, in.SEODesc, in.SEOH1, in.PhotoID)
if err != nil {
return wrapConflict(err)
}
if tag.RowsAffected() == 0 {
return ErrNotFound
}
return nil
}
// SetDoctorLinks заменяет услуги и филиалы врача целиком, в одной транзакции.
func (s *Storage) SetDoctorLinks(ctx context.Context, doctorID int64, serviceIDs, branchIDs []int64) error {
tx, err := s.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx) //nolint:errcheck // после Commit откат ничего не делает
var exists bool
if err := tx.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM doctors WHERE id=$1 AND deleted_at IS NULL)`, doctorID).Scan(&exists); err != nil {
return err
}
if !exists {
return ErrNotFound
}
if _, err := tx.Exec(ctx, `DELETE FROM doctor_services WHERE doctor_id=$1`, doctorID); err != nil {
return err
}
if _, err := tx.Exec(ctx, `
INSERT INTO doctor_services (doctor_id, service_id)
SELECT $1, unnest($2::bigint[]) ON CONFLICT DO NOTHING`, doctorID, serviceIDs); err != nil {
return wrapConflict(err)
}
if _, err := tx.Exec(ctx, `DELETE FROM doctor_branches WHERE doctor_id=$1`, doctorID); err != nil {
return err
}
if _, err := tx.Exec(ctx, `
INSERT INTO doctor_branches (doctor_id, branch_id)
SELECT $1, unnest($2::bigint[]) ON CONFLICT DO NOTHING`, doctorID, branchIDs); err != nil {
return wrapConflict(err)
}
return tx.Commit(ctx)
}
// ---------- Записи биографии врача ----------
type AdminBioEntry struct {
ID int64 `json:"id"`
DoctorID int64 `json:"doctor_id"`
Kind string `json:"kind"`
YearFrom *int `json:"year_from"`
YearTo *int `json:"year_to"`
Title string `json:"title"`
Description string `json:"description"`
Sort int `json:"sort"`
}
func (s *Storage) AdminBioEntries(ctx context.Context, doctorID int64) ([]AdminBioEntry, error) {
rows, err := s.pool.Query(ctx, `
SELECT id, doctor_id, kind, year_from, year_to, title, description, sort
FROM doctor_bio_entries WHERE doctor_id=$1
ORDER BY kind, sort, id`, doctorID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []AdminBioEntry
for rows.Next() {
var a AdminBioEntry
if err := rows.Scan(&a.ID, &a.DoctorID, &a.Kind, &a.YearFrom, &a.YearTo,
&a.Title, &a.Description, &a.Sort); err != nil {
return nil, err
}
out = append(out, a)
}
return out, rows.Err()
}
type BioEntryInput struct {
DoctorID int64 `json:"doctor_id"`
Kind string `json:"kind"`
YearFrom *int `json:"year_from"`
YearTo *int `json:"year_to"`
Title string `json:"title"`
Description string `json:"description"`
Sort int `json:"sort"`
}
func (s *Storage) AdminCreateBioEntry(ctx context.Context, in BioEntryInput) (int64, error) {
var id int64
err := s.pool.QueryRow(ctx, `
INSERT INTO doctor_bio_entries (doctor_id, kind, year_from, year_to, title, description, sort)
VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING id`,
in.DoctorID, in.Kind, in.YearFrom, in.YearTo, in.Title, in.Description, in.Sort).Scan(&id)
return id, err
}
func (s *Storage) AdminUpdateBioEntry(ctx context.Context, id int64, in BioEntryInput) error {
tag, err := s.pool.Exec(ctx, `
UPDATE doctor_bio_entries SET kind=$2, year_from=$3, year_to=$4,
title=$5, description=$6, sort=$7
WHERE id=$1`,
id, in.Kind, in.YearFrom, in.YearTo, in.Title, in.Description, in.Sort)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return ErrNotFound
}
return nil
}
func (s *Storage) AdminDeleteBioEntry(ctx context.Context, id int64) error {
tag, err := s.pool.Exec(ctx, `DELETE FROM doctor_bio_entries WHERE id=$1`, id)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return ErrNotFound
}
return nil
}