222 lines
7.0 KiB
Go
222 lines
7.0 KiB
Go
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
|
||
}
|