package storage import ( "context" "encoding/json" "time" ) // ---------- Страницы ---------- type AdminPage struct { ID int64 `json:"id"` Slug string `json:"slug"` Title string `json:"title"` Blocks json.RawMessage `json:"blocks"` Published bool `json:"published"` SEOTitle string `json:"seo_title"` SEODesc string `json:"seo_description"` SEOH1 string `json:"seo_h1"` UpdatedAt time.Time `json:"updated_at"` } func (s *Storage) AdminPages(ctx context.Context) ([]AdminPage, error) { rows, err := s.pool.Query(ctx, ` SELECT id, slug, title, blocks, published, seo_title, seo_description, seo_h1, updated_at FROM pages ORDER BY slug`) if err != nil { return nil, err } defer rows.Close() var out []AdminPage for rows.Next() { var p AdminPage if err := rows.Scan(&p.ID, &p.Slug, &p.Title, &p.Blocks, &p.Published, &p.SEOTitle, &p.SEODesc, &p.SEOH1, &p.UpdatedAt); err != nil { return nil, err } out = append(out, p) } return out, rows.Err() } func (s *Storage) AdminPage(ctx context.Context, id int64) (*AdminPage, error) { var p AdminPage err := s.pool.QueryRow(ctx, ` SELECT id, slug, title, blocks, published, seo_title, seo_description, seo_h1, updated_at FROM pages WHERE id=$1`, id).Scan(&p.ID, &p.Slug, &p.Title, &p.Blocks, &p.Published, &p.SEOTitle, &p.SEODesc, &p.SEOH1, &p.UpdatedAt) if err != nil { return nil, wrapNotFound(err) } return &p, nil } type PageInput struct { Title string Blocks json.RawMessage Published bool SEOTitle string SEODesc string SEOH1 string } // AdminUpdatePage меняет содержимое страницы. Адрес (slug) не меняется: страницы // привязаны к маршрутам сайта. Новых страниц админка не создаёт по той же причине. func (s *Storage) AdminUpdatePage(ctx context.Context, id int64, in PageInput) error { tag, err := s.pool.Exec(ctx, ` UPDATE pages SET title=$2, blocks=$3, published=$4, seo_title=$5, seo_description=$6, seo_h1=$7, updated_at=now() WHERE id=$1`, id, in.Title, in.Blocks, in.Published, in.SEOTitle, in.SEODesc, in.SEOH1) if err != nil { return err } if tag.RowsAffected() == 0 { return ErrNotFound } return nil } // ---------- Настройки ---------- // AdminSetSettings записывает несколько ключей за раз в одной транзакции. func (s *Storage) AdminSetSettings(ctx context.Context, values map[string]any) error { tx, err := s.pool.Begin(ctx) if err != nil { return err } defer tx.Rollback(ctx) //nolint:errcheck // после Commit откат ничего не делает for key, value := range values { raw, err := json.Marshal(value) if err != nil { return err } if _, err := tx.Exec(ctx, ` INSERT INTO settings (key, value, updated_at) VALUES ($1, $2, now()) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now()`, key, raw); err != nil { return err } } return tx.Commit(ctx) } // ---------- Филиалы ---------- type AdminBranch struct { ID int64 `json:"id"` Slug string `json:"slug"` Name string `json:"name"` Address string `json:"address"` Phones []string `json:"phones"` WorkHours []string `json:"work_hours"` Lat *float64 `json:"lat"` Lon *float64 `json:"lon"` Sort int `json:"sort"` Published bool `json:"published"` } func (s *Storage) AdminBranches(ctx context.Context) ([]AdminBranch, error) { rows, err := s.pool.Query(ctx, ` SELECT id, slug, name, address, phones, work_hours, lat, lon, sort, published FROM branches ORDER BY sort, name`) if err != nil { return nil, err } defer rows.Close() var out []AdminBranch for rows.Next() { var b AdminBranch if err := rows.Scan(&b.ID, &b.Slug, &b.Name, &b.Address, &b.Phones, &b.WorkHours, &b.Lat, &b.Lon, &b.Sort, &b.Published); err != nil { return nil, err } out = append(out, b) } return out, rows.Err() } type BranchInput struct { Slug string Name string Address string Phones []string WorkHours []string Lat *float64 Lon *float64 Sort int Published bool } func (s *Storage) AdminCreateBranch(ctx context.Context, in BranchInput) (int64, error) { phones, _ := json.Marshal(in.Phones) hours, _ := json.Marshal(in.WorkHours) var id int64 err := s.pool.QueryRow(ctx, ` INSERT INTO branches (slug, name, address, phones, work_hours, lat, lon, sort, published) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING id`, in.Slug, in.Name, in.Address, phones, hours, in.Lat, in.Lon, in.Sort, in.Published).Scan(&id) return id, wrapConflict(err) } func (s *Storage) AdminUpdateBranch(ctx context.Context, id int64, in BranchInput) error { phones, _ := json.Marshal(in.Phones) hours, _ := json.Marshal(in.WorkHours) tag, err := s.pool.Exec(ctx, ` UPDATE branches SET slug=$2, name=$3, address=$4, phones=$5, work_hours=$6, lat=$7, lon=$8, sort=$9, published=$10, updated_at=now() WHERE id=$1`, id, in.Slug, in.Name, in.Address, phones, hours, in.Lat, in.Lon, in.Sort, in.Published) if err != nil { return wrapConflict(err) } if tag.RowsAffected() == 0 { return ErrNotFound } return nil }