160 lines
4.9 KiB
Go
160 lines
4.9 KiB
Go
package storage
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
|
||
"github.com/jackc/pgx/v5"
|
||
)
|
||
|
||
type AdminCheckup struct {
|
||
ID int64 `json:"id"`
|
||
Slug string `json:"slug"`
|
||
Audience string `json:"audience"`
|
||
Name string `json:"name"`
|
||
Lead string `json:"lead"`
|
||
Blocks json.RawMessage `json:"blocks"`
|
||
Price *float64 `json:"price"`
|
||
OldPrice *float64 `json:"old_price"`
|
||
ImageID *int64 `json:"image_id"`
|
||
Image string `json:"image,omitempty"`
|
||
Items []string `json:"items"`
|
||
Sort int `json:"sort"`
|
||
Published bool `json:"published"`
|
||
SEOTitle string `json:"seo_title"`
|
||
SEODesc string `json:"seo_description"`
|
||
SEOH1 string `json:"seo_h1"`
|
||
}
|
||
|
||
// AdminCheckups — все программы со составом. Blocks отдаём, чтобы админка могла
|
||
// предложить состав из старого описания, пока его не сохранили списком.
|
||
func (s *Storage) AdminCheckups(ctx context.Context) ([]AdminCheckup, error) {
|
||
rows, err := s.pool.Query(ctx, `
|
||
SELECT c.id, c.slug, c.audience, c.name, c.lead, c.blocks, c.price, c.old_price,
|
||
c.image_id, COALESCE(m.path, ''), c.sort, c.published,
|
||
c.seo_title, c.seo_description, c.seo_h1
|
||
FROM checkups c
|
||
LEFT JOIN media m ON m.id = c.image_id
|
||
WHERE c.deleted_at IS NULL
|
||
ORDER BY c.audience, c.sort, c.name`)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
|
||
var out []AdminCheckup
|
||
index := map[int64]int{}
|
||
for rows.Next() {
|
||
var a AdminCheckup
|
||
if err := rows.Scan(&a.ID, &a.Slug, &a.Audience, &a.Name, &a.Lead, &a.Blocks,
|
||
&a.Price, &a.OldPrice, &a.ImageID, &a.Image, &a.Sort, &a.Published,
|
||
&a.SEOTitle, &a.SEODesc, &a.SEOH1); err != nil {
|
||
return nil, err
|
||
}
|
||
a.Items = []string{}
|
||
index[a.ID] = len(out)
|
||
out = append(out, a)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
itemRows, err := s.pool.Query(ctx, `SELECT checkup_id, name FROM checkup_items ORDER BY checkup_id, sort, id`)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer itemRows.Close()
|
||
for itemRows.Next() {
|
||
var checkupID int64
|
||
var name string
|
||
if err := itemRows.Scan(&checkupID, &name); err != nil {
|
||
return nil, err
|
||
}
|
||
if i, found := index[checkupID]; found {
|
||
out[i].Items = append(out[i].Items, name)
|
||
}
|
||
}
|
||
return out, itemRows.Err()
|
||
}
|
||
|
||
type CheckupInput struct {
|
||
Slug string
|
||
Audience string
|
||
Name string
|
||
Lead string
|
||
Price *float64
|
||
OldPrice *float64
|
||
ImageID *int64
|
||
Items []string
|
||
Sort int
|
||
Published bool
|
||
SEOTitle string
|
||
SEODesc string
|
||
SEOH1 string
|
||
}
|
||
|
||
func (s *Storage) AdminCreateCheckup(ctx context.Context, in CheckupInput) (int64, error) {
|
||
tx, err := s.pool.Begin(ctx)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
defer tx.Rollback(ctx) //nolint:errcheck // после Commit откат ничего не делает
|
||
|
||
var id int64
|
||
err = tx.QueryRow(ctx, `
|
||
INSERT INTO checkups (slug, audience, name, lead, price, old_price, image_id, sort, published,
|
||
seo_title, seo_description, seo_h1)
|
||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)
|
||
RETURNING id`,
|
||
in.Slug, in.Audience, in.Name, in.Lead, in.Price, in.OldPrice, in.ImageID, in.Sort, in.Published,
|
||
in.SEOTitle, in.SEODesc, in.SEOH1).Scan(&id)
|
||
if err != nil {
|
||
return 0, wrapConflict(err)
|
||
}
|
||
if err := replaceCheckupItems(ctx, tx, id, in.Items); err != nil {
|
||
return 0, err
|
||
}
|
||
return id, tx.Commit(ctx)
|
||
}
|
||
|
||
func (s *Storage) AdminUpdateCheckup(ctx context.Context, id int64, in CheckupInput) error {
|
||
tx, err := s.pool.Begin(ctx)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer tx.Rollback(ctx) //nolint:errcheck // после Commit откат ничего не делает
|
||
|
||
tag, err := tx.Exec(ctx, `
|
||
UPDATE checkups SET slug=$2, audience=$3, name=$4, lead=$5, price=$6, old_price=$7,
|
||
image_id=$8, sort=$9, published=$10,
|
||
seo_title=$11, seo_description=$12, seo_h1=$13, updated_at=now()
|
||
WHERE id=$1 AND deleted_at IS NULL`,
|
||
id, in.Slug, in.Audience, in.Name, in.Lead, in.Price, in.OldPrice,
|
||
in.ImageID, in.Sort, in.Published, in.SEOTitle, in.SEODesc, in.SEOH1)
|
||
if err != nil {
|
||
return wrapConflict(err)
|
||
}
|
||
if tag.RowsAffected() == 0 {
|
||
return ErrNotFound
|
||
}
|
||
if err := replaceCheckupItems(ctx, tx, id, in.Items); err != nil {
|
||
return err
|
||
}
|
||
return tx.Commit(ctx)
|
||
}
|
||
|
||
// replaceCheckupItems перезаписывает состав целиком: порядок строк в админке — порядок на сайте.
|
||
func replaceCheckupItems(ctx context.Context, tx pgx.Tx, checkupID int64, items []string) error {
|
||
if _, err := tx.Exec(ctx, `DELETE FROM checkup_items WHERE checkup_id = $1`, checkupID); err != nil {
|
||
return err
|
||
}
|
||
for i, name := range items {
|
||
if _, err := tx.Exec(ctx,
|
||
`INSERT INTO checkup_items (checkup_id, name, sort) VALUES ($1, $2, $3)`,
|
||
checkupID, name, (i+1)*10); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|