Files
aura-crm/production/backend/internal/devicecatalog/handler.go
T

281 lines
8.7 KiB
Go

package devicecatalog
import (
"context"
"strings"
"production/internal/dbutil"
"github.com/gofiber/fiber/v2"
"github.com/jackc/pgx/v5/pgxpool"
)
type Handler struct {
db *pgxpool.Pool
}
func NewHandler(db *pgxpool.Pool) *Handler {
return &Handler{db: db}
}
type groupRow struct {
ID string `json:"id"`
Name string `json:"name"`
Prefix string `json:"prefix"`
SortOrder int `json:"sort_order"`
}
type brandRow struct {
ID string `json:"id"`
Name string `json:"name"`
}
type faultRow struct {
ID string `json:"id"`
Label string `json:"label"`
}
func (h *Handler) ListGroups(c *fiber.Ctx) error {
rows, err := h.db.Query(context.Background(),
`SELECT id, name, prefix, sort_order FROM device_groups ORDER BY sort_order, name`)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "internal error"})
}
defer rows.Close()
groups := []groupRow{}
for rows.Next() {
var g groupRow
if err := rows.Scan(&g.ID, &g.Name, &g.Prefix, &g.SortOrder); err != nil {
return c.Status(500).JSON(fiber.Map{"error": "internal error"})
}
groups = append(groups, g)
}
return c.JSON(groups)
}
func (h *Handler) CreateGroup(c *fiber.Ctx) error {
var body struct {
Name string `json:"name"`
Prefix string `json:"prefix"`
SortOrder int `json:"sort_order"`
}
if err := c.BodyParser(&body); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "invalid request body"})
}
if msg := (groupInput{Name: body.Name, Prefix: body.Prefix}).validate(); msg != "" {
return c.Status(400).JSON(fiber.Map{"error": msg})
}
name := strings.TrimSpace(body.Name)
prefix := strings.ToUpper(strings.TrimSpace(body.Prefix))
var id string
err := h.db.QueryRow(context.Background(),
`INSERT INTO device_groups (name, prefix, sort_order) VALUES ($1, $2, $3) RETURNING id`,
name, prefix, body.SortOrder,
).Scan(&id)
if err != nil {
if dbutil.IsUniqueViolation(err) {
return c.Status(409).JSON(fiber.Map{"error": "group name or prefix already exists"})
}
return c.Status(500).JSON(fiber.Map{"error": "internal error"})
}
return c.Status(201).JSON(fiber.Map{"id": id, "name": name, "prefix": prefix, "sort_order": body.SortOrder})
}
func (h *Handler) UpdateGroup(c *fiber.Ctx) error {
id := c.Params("id")
var body struct {
Name string `json:"name"`
Prefix string `json:"prefix"`
SortOrder int `json:"sort_order"`
}
if err := c.BodyParser(&body); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "invalid request body"})
}
if msg := (groupInput{Name: body.Name, Prefix: body.Prefix}).validate(); msg != "" {
return c.Status(400).JSON(fiber.Map{"error": msg})
}
name := strings.TrimSpace(body.Name)
prefix := strings.ToUpper(strings.TrimSpace(body.Prefix))
tag, err := h.db.Exec(context.Background(),
`UPDATE device_groups SET name = $1, prefix = $2, sort_order = $3 WHERE id = $4::uuid`,
name, prefix, body.SortOrder, id)
if err != nil {
if dbutil.IsUniqueViolation(err) {
return c.Status(409).JSON(fiber.Map{"error": "group name or prefix already exists"})
}
return c.Status(500).JSON(fiber.Map{"error": "internal error"})
}
if tag.RowsAffected() == 0 {
return c.Status(404).JSON(fiber.Map{"error": "group not found"})
}
return c.JSON(fiber.Map{"ok": true})
}
func (h *Handler) DeleteGroup(c *fiber.Ctx) error {
id := c.Params("id")
tag, err := h.db.Exec(context.Background(), `DELETE FROM device_groups WHERE id = $1::uuid`, id)
if err != nil {
if dbutil.IsFKViolation(err) {
return c.Status(409).JSON(fiber.Map{"error": "group is used by existing orders"})
}
return c.Status(500).JSON(fiber.Map{"error": "internal error"})
}
if tag.RowsAffected() == 0 {
return c.Status(404).JSON(fiber.Map{"error": "group not found"})
}
return c.JSON(fiber.Map{"ok": true})
}
func (h *Handler) ListBrands(c *fiber.Ctx) error {
rows, err := h.db.Query(context.Background(), `SELECT id, name FROM device_brands ORDER BY name`)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "internal error"})
}
defer rows.Close()
brands := []brandRow{}
for rows.Next() {
var b brandRow
if err := rows.Scan(&b.ID, &b.Name); err != nil {
return c.Status(500).JSON(fiber.Map{"error": "internal error"})
}
brands = append(brands, b)
}
return c.JSON(brands)
}
// CreateBrand is open to any staff role (unlike groups, which gate on
// owner via main.go's route wiring) — a brand is just a label, adding one
// carries no structural risk like a group's order-number prefix does.
func (h *Handler) CreateBrand(c *fiber.Ctx) error {
var body struct {
Name string `json:"name"`
}
if err := c.BodyParser(&body); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "invalid request body"})
}
if msg := (brandInput{Name: body.Name}).validate(); msg != "" {
return c.Status(400).JSON(fiber.Map{"error": msg})
}
name := strings.TrimSpace(body.Name)
var id string
err := h.db.QueryRow(context.Background(),
`INSERT INTO device_brands (name) VALUES ($1) RETURNING id`, name,
).Scan(&id)
if err != nil {
if dbutil.IsUniqueViolation(err) {
return c.Status(409).JSON(fiber.Map{"error": "brand already exists"})
}
return c.Status(500).JSON(fiber.Map{"error": "internal error"})
}
return c.Status(201).JSON(fiber.Map{"id": id, "name": name})
}
// RecentModels suggests device_model values already seen on past orders for
// a brand — there's no models catalog table (too many models per brand to
// maintain), so this is a lightweight assist instead: whatever staff typed
// before for this brand becomes a <datalist> suggestion next time.
func (h *Handler) RecentModels(c *fiber.Ctx) error {
brandID := c.Query("brand_id")
if brandID == "" {
return c.JSON([]string{})
}
rows, err := h.db.Query(context.Background(),
`SELECT DISTINCT device_model FROM orders
WHERE device_brand_id = $1::uuid AND device_model IS NOT NULL AND device_model != ''
ORDER BY device_model LIMIT 20`, brandID)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "internal error"})
}
defer rows.Close()
models := []string{}
for rows.Next() {
var m string
if err := rows.Scan(&m); err != nil {
return c.Status(500).JSON(fiber.Map{"error": "internal error"})
}
models = append(models, m)
}
return c.JSON(models)
}
func (h *Handler) DeleteBrand(c *fiber.Ctx) error {
id := c.Params("id")
tag, err := h.db.Exec(context.Background(), `DELETE FROM device_brands WHERE id = $1::uuid`, id)
if err != nil {
if dbutil.IsFKViolation(err) {
return c.Status(409).JSON(fiber.Map{"error": "brand is used by existing orders"})
}
return c.Status(500).JSON(fiber.Map{"error": "internal error"})
}
if tag.RowsAffected() == 0 {
return c.Status(404).JSON(fiber.Map{"error": "brand not found"})
}
return c.JSON(fiber.Map{"ok": true})
}
// ListFaults/CreateFault/DeleteFault manage common_faults — quick-pick
// problem descriptions shown as buttons under "Описание проблемы"
// (migrations/061_common_faults.sql). Same flat, unscoped, any-staff-can-
// add shape as brands above; orders never reference this table, a click
// just inserts the label as plain text.
func (h *Handler) ListFaults(c *fiber.Ctx) error {
rows, err := h.db.Query(context.Background(), `SELECT id, label FROM common_faults ORDER BY label`)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "internal error"})
}
defer rows.Close()
faults := []faultRow{}
for rows.Next() {
var f faultRow
if err := rows.Scan(&f.ID, &f.Label); err != nil {
return c.Status(500).JSON(fiber.Map{"error": "internal error"})
}
faults = append(faults, f)
}
return c.JSON(faults)
}
func (h *Handler) CreateFault(c *fiber.Ctx) error {
var body struct {
Label string `json:"label"`
}
if err := c.BodyParser(&body); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "invalid request body"})
}
if msg := (faultInput{Label: body.Label}).validate(); msg != "" {
return c.Status(400).JSON(fiber.Map{"error": msg})
}
label := strings.TrimSpace(body.Label)
var id string
err := h.db.QueryRow(context.Background(),
`INSERT INTO common_faults (label) VALUES ($1) RETURNING id`, label,
).Scan(&id)
if err != nil {
if dbutil.IsUniqueViolation(err) {
return c.Status(409).JSON(fiber.Map{"error": "fault already exists"})
}
return c.Status(500).JSON(fiber.Map{"error": "internal error"})
}
return c.Status(201).JSON(fiber.Map{"id": id, "label": label})
}
func (h *Handler) DeleteFault(c *fiber.Ctx) error {
id := c.Params("id")
tag, err := h.db.Exec(context.Background(), `DELETE FROM common_faults WHERE id = $1::uuid`, id)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "internal error"})
}
if tag.RowsAffected() == 0 {
return c.Status(404).JSON(fiber.Map{"error": "fault not found"})
}
return c.JSON(fiber.Map{"ok": true})
}