250 lines
9.0 KiB
Go
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
|
|
}
|