281 lines
8.7 KiB
Go
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})
|
|
}
|