117 lines
4.0 KiB
Go
117 lines
4.0 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
)
|
|
|
|
type AdminPromo struct {
|
|
ID int64 `json:"id"`
|
|
Slug string `json:"slug"`
|
|
Title string `json:"title"`
|
|
Lead string `json:"lead"`
|
|
ImageID *int64 `json:"image_id"`
|
|
Image string `json:"image,omitempty"`
|
|
DiscountPercent *int `json:"discount_percent"`
|
|
Terms string `json:"terms"`
|
|
CheckupID *int64 `json:"checkup_id"`
|
|
CheckupName string `json:"checkup_name,omitempty"`
|
|
StartsAt *time.Time `json:"starts_at"`
|
|
EndsAt *time.Time `json:"ends_at"`
|
|
Published bool `json:"published"`
|
|
PublishedAt time.Time `json:"published_at"`
|
|
SEOTitle string `json:"seo_title"`
|
|
SEODesc string `json:"seo_description"`
|
|
SEOH1 string `json:"seo_h1"`
|
|
}
|
|
|
|
// AdminPromos — все акции, включая скрытые и завершённые.
|
|
func (s *Storage) AdminPromos(ctx context.Context) ([]AdminPromo, error) {
|
|
rows, err := s.pool.Query(ctx, `
|
|
SELECT p.id, p.slug, p.title, p.lead, p.image_id, COALESCE(m.path, ''),
|
|
p.discount_percent, p.terms, p.checkup_id, COALESCE(c.name, ''),
|
|
p.starts_at, p.ends_at, p.published, p.published_at,
|
|
p.seo_title, p.seo_description, p.seo_h1
|
|
FROM posts p
|
|
LEFT JOIN media m ON m.id = p.image_id
|
|
LEFT JOIN checkups c ON c.id = p.checkup_id
|
|
WHERE p.kind = 'promo' AND p.deleted_at IS NULL
|
|
ORDER BY p.published_at DESC, p.id DESC`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []AdminPromo
|
|
for rows.Next() {
|
|
var a AdminPromo
|
|
if err := rows.Scan(&a.ID, &a.Slug, &a.Title, &a.Lead, &a.ImageID, &a.Image,
|
|
&a.DiscountPercent, &a.Terms, &a.CheckupID, &a.CheckupName,
|
|
&a.StartsAt, &a.EndsAt, &a.Published, &a.PublishedAt,
|
|
&a.SEOTitle, &a.SEODesc, &a.SEOH1); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, a)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
type PromoInput struct {
|
|
Slug string
|
|
Title string
|
|
Lead string
|
|
ImageID *int64
|
|
DiscountPercent *int
|
|
Terms string
|
|
CheckupID *int64
|
|
StartsAt *time.Time
|
|
EndsAt *time.Time
|
|
Published bool
|
|
SEOTitle string
|
|
SEODesc string
|
|
SEOH1 string
|
|
}
|
|
|
|
func (s *Storage) AdminCreatePromo(ctx context.Context, in PromoInput) (int64, error) {
|
|
var id int64
|
|
err := s.pool.QueryRow(ctx, `
|
|
INSERT INTO posts (kind, slug, title, lead, image_id, discount_percent, terms, checkup_id,
|
|
starts_at, ends_at, published, seo_title, seo_description, seo_h1)
|
|
VALUES ('promo', $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
|
|
RETURNING id`,
|
|
in.Slug, in.Title, in.Lead, in.ImageID, in.DiscountPercent, in.Terms, in.CheckupID,
|
|
in.StartsAt, in.EndsAt, in.Published, in.SEOTitle, in.SEODesc, in.SEOH1).Scan(&id)
|
|
return id, wrapConflict(err)
|
|
}
|
|
|
|
func (s *Storage) AdminUpdatePromo(ctx context.Context, id int64, in PromoInput) error {
|
|
tag, err := s.pool.Exec(ctx, `
|
|
UPDATE posts SET slug=$2, title=$3, lead=$4, image_id=$5, discount_percent=$6, terms=$7,
|
|
checkup_id=$8, starts_at=$9, ends_at=$10, published=$11,
|
|
seo_title=$12, seo_description=$13, seo_h1=$14, updated_at=now()
|
|
WHERE id=$1 AND kind='promo' AND deleted_at IS NULL`,
|
|
id, in.Slug, in.Title, in.Lead, in.ImageID, in.DiscountPercent, in.Terms,
|
|
in.CheckupID, in.StartsAt, in.EndsAt, in.Published, in.SEOTitle, in.SEODesc, in.SEOH1)
|
|
if err != nil {
|
|
return wrapConflict(err)
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AdminDeletePromo скрывает акцию мягким удалением: запись остаётся для истории и аудита.
|
|
func (s *Storage) AdminDeletePromo(ctx context.Context, id int64) error {
|
|
tag, err := s.pool.Exec(ctx, `
|
|
UPDATE posts SET deleted_at=now(), published=false, updated_at=now()
|
|
WHERE id=$1 AND kind='promo' AND deleted_at IS NULL`, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|