package storage import ( "context" "errors" "github.com/jackc/pgx/v5" ) // ---------- Услуги ---------- type AdminService struct { ID int64 `json:"id"` ParentID *int64 `json:"parent_id"` ParentName string `json:"parent_name,omitempty"` Slug string `json:"slug"` Path string `json:"path"` Name string `json:"name"` ShortName string `json:"short_name"` Lead string `json:"lead"` Sort int `json:"sort"` Published bool `json:"published"` SEOTitle string `json:"seo_title"` SEODesc string `json:"seo_description"` SEOH1 string `json:"seo_h1"` PriceFrom *float64 `json:"price_from,omitempty"` PriceCount int `json:"price_count"` } // AdminServices возвращает плоский список услуг, включая скрытые. func (s *Storage) AdminServices(ctx context.Context, q string) ([]AdminService, error) { rows, err := s.pool.Query(ctx, ` SELECT s.id, s.parent_id, COALESCE(p.name, ''), s.slug, s.path, s.name, s.short_name, s.lead, s.sort, s.published, s.seo_title, s.seo_description, s.seo_h1, (SELECT min(pi.price) FROM price_items pi WHERE pi.service_id = s.id AND pi.published AND NOT pi.exclude_from_min), (SELECT count(*) FROM price_items pi WHERE pi.service_id = s.id) FROM services s LEFT JOIN services p ON p.id = s.parent_id WHERE s.deleted_at IS NULL AND ($1 = '' OR s.name ILIKE '%' || $1 || '%' OR s.path ILIKE '%' || $1 || '%') ORDER BY COALESCE(p.sort, s.sort), COALESCE(p.name, s.name), s.parent_id NULLS FIRST, s.sort, s.name`, q) if err != nil { return nil, err } defer rows.Close() var out []AdminService for rows.Next() { var a AdminService if err := rows.Scan(&a.ID, &a.ParentID, &a.ParentName, &a.Slug, &a.Path, &a.Name, &a.ShortName, &a.Lead, &a.Sort, &a.Published, &a.SEOTitle, &a.SEODesc, &a.SEOH1, &a.PriceFrom, &a.PriceCount); err != nil { return nil, err } out = append(out, a) } return out, rows.Err() } type ServiceUpdate struct { Name string `json:"name"` ShortName string `json:"short_name"` Lead string `json:"lead"` Sort int `json:"sort"` Published bool `json:"published"` SEOTitle string `json:"seo_title"` SEODesc string `json:"seo_description"` SEOH1 string `json:"seo_h1"` } func (s *Storage) AdminUpdateService(ctx context.Context, id int64, u ServiceUpdate) error { tag, err := s.pool.Exec(ctx, ` UPDATE services SET name=$2, short_name=$3, lead=$4, sort=$5, published=$6, seo_title=$7, seo_description=$8, seo_h1=$9, updated_at=now() WHERE id=$1 AND deleted_at IS NULL`, id, u.Name, u.ShortName, u.Lead, u.Sort, u.Published, u.SEOTitle, u.SEODesc, u.SEOH1) if err != nil { return err } if tag.RowsAffected() == 0 { return ErrNotFound } return nil } // ---------- Прайс ---------- type AdminPriceSection struct { ID int64 `json:"id"` Slug string `json:"slug"` Name string `json:"name"` Count int `json:"count"` } func (s *Storage) AdminPriceSections(ctx context.Context) ([]AdminPriceSection, error) { rows, err := s.pool.Query(ctx, ` SELECT ps.id, ps.slug, ps.name, (SELECT count(*) FROM price_items pi WHERE pi.section_id = ps.id) FROM price_sections ps ORDER BY ps.sort, ps.name`) if err != nil { return nil, err } defer rows.Close() var out []AdminPriceSection for rows.Next() { var a AdminPriceSection if err := rows.Scan(&a.ID, &a.Slug, &a.Name, &a.Count); err != nil { return nil, err } out = append(out, a) } return out, rows.Err() } type AdminPriceItem struct { ID int64 `json:"id"` SectionID int64 `json:"section_id"` SectionName string `json:"section_name"` ServiceID *int64 `json:"service_id"` ServiceName string `json:"service_name,omitempty"` Name string `json:"name"` Price float64 `json:"price"` Unit string `json:"unit"` PriceFrom bool `json:"price_from"` Sort int `json:"sort"` Published bool `json:"published"` } // AdminPriceItems: постранично, с поиском и фильтром по разделу. func (s *Storage) AdminPriceItems(ctx context.Context, sectionID int64, q string, limit, offset int) ([]AdminPriceItem, int, error) { var total int if err := s.pool.QueryRow(ctx, ` SELECT count(*) FROM price_items pi WHERE ($1 = 0 OR pi.section_id = $1) AND ($2 = '' OR pi.name ILIKE '%' || $2 || '%')`, sectionID, q).Scan(&total); err != nil { return nil, 0, err } rows, err := s.pool.Query(ctx, ` SELECT pi.id, pi.section_id, ps.name, pi.service_id, COALESCE(sv.name, ''), pi.name, pi.price, pi.unit, pi.price_from, pi.sort, pi.published FROM price_items pi JOIN price_sections ps ON ps.id = pi.section_id LEFT JOIN services sv ON sv.id = pi.service_id WHERE ($1 = 0 OR pi.section_id = $1) AND ($2 = '' OR pi.name ILIKE '%' || $2 || '%') ORDER BY ps.sort, pi.sort, pi.name LIMIT $3 OFFSET $4`, sectionID, q, limit, offset) if err != nil { return nil, 0, err } defer rows.Close() var out []AdminPriceItem for rows.Next() { var a AdminPriceItem if err := rows.Scan(&a.ID, &a.SectionID, &a.SectionName, &a.ServiceID, &a.ServiceName, &a.Name, &a.Price, &a.Unit, &a.PriceFrom, &a.Sort, &a.Published); err != nil { return nil, 0, err } out = append(out, a) } return out, total, rows.Err() } type PriceItemInput struct { SectionID int64 `json:"section_id"` ServiceID *int64 `json:"service_id"` Name string `json:"name"` Price float64 `json:"price"` Unit string `json:"unit"` PriceFrom bool `json:"price_from"` Sort int `json:"sort"` Published bool `json:"published"` } func (s *Storage) AdminUpdatePriceItem(ctx context.Context, id int64, in PriceItemInput) error { tag, err := s.pool.Exec(ctx, ` UPDATE price_items SET section_id=$2, service_id=$3, name=$4, price=$5, unit=$6, price_from=$7, sort=$8, published=$9, updated_at=now() WHERE id=$1`, id, in.SectionID, in.ServiceID, in.Name, in.Price, in.Unit, in.PriceFrom, in.Sort, in.Published) if err != nil { return err } if tag.RowsAffected() == 0 { return ErrNotFound } return nil } func (s *Storage) AdminCreatePriceItem(ctx context.Context, in PriceItemInput) (int64, error) { var id int64 err := s.pool.QueryRow(ctx, ` INSERT INTO price_items (section_id, service_id, name, price, unit, price_from, sort, published) VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING id`, in.SectionID, in.ServiceID, in.Name, in.Price, in.Unit, in.PriceFrom, in.Sort, in.Published).Scan(&id) return id, err } func (s *Storage) AdminDeletePriceItem(ctx context.Context, id int64) error { tag, err := s.pool.Exec(ctx, `DELETE FROM price_items WHERE id=$1`, id) if err != nil { return err } if tag.RowsAffected() == 0 { return ErrNotFound } return nil } func (s *Storage) priceItemExists(ctx context.Context, id int64) error { var x int err := s.pool.QueryRow(ctx, `SELECT 1 FROM price_items WHERE id=$1`, id).Scan(&x) if errors.Is(err, pgx.ErrNoRows) { return ErrNotFound } return err }