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/<раздел>//. 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 }