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 }