Replaces hardcoded JS category filters and config-type buttons with server-pushed settings synced from qt_settings (MariaDB) → local_qt_settings (SQLite). - new table local_qt_settings (AutoMigrate) — synced after component sync - GET /api/configurator-settings returns config_types, tab_config, always_visible_tabs, required_categories with hardcoded fallbacks - new-config modal: type buttons rendered from server data (Сервер/СХД static fallback) - configurator: TAB_CONFIG and category filter driven by server; required-category badge on tabs - SyncQtSettings wired into SyncComponents and SyncAll handlers (non-fatal on old server) - bible-local/server-contract-qt-settings.md — contract for server-side agent - page titles: OFS → QFS Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
227 lines
5.8 KiB
Go
227 lines
5.8 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.mchus.pro/mchus/quoteforge/internal/localdb"
|
|
"git.mchus.pro/mchus/quoteforge/internal/models"
|
|
"git.mchus.pro/mchus/quoteforge/internal/repository"
|
|
"git.mchus.pro/mchus/quoteforge/internal/services"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type ComponentHandler struct {
|
|
componentService *services.ComponentService
|
|
localDB *localdb.LocalDB
|
|
}
|
|
|
|
func NewComponentHandler(componentService *services.ComponentService, localDB *localdb.LocalDB) *ComponentHandler {
|
|
return &ComponentHandler{
|
|
componentService: componentService,
|
|
localDB: localDB,
|
|
}
|
|
}
|
|
|
|
func (h *ComponentHandler) List(c *gin.Context) {
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "20"))
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if perPage < 1 {
|
|
perPage = 20
|
|
}
|
|
|
|
filter := repository.ComponentFilter{
|
|
Category: c.Query("category"),
|
|
Search: c.Query("search"),
|
|
HasPrice: c.Query("has_price") == "true",
|
|
ExcludeHidden: c.Query("include_hidden") != "true", // По умолчанию скрытые не показываются
|
|
}
|
|
|
|
localFilter := localdb.ComponentFilter{
|
|
Category: filter.Category,
|
|
Search: filter.Search,
|
|
HasPrice: filter.HasPrice,
|
|
}
|
|
offset := (page - 1) * perPage
|
|
localComps, total, err := h.localDB.ListComponents(localFilter, offset, perPage)
|
|
if err != nil {
|
|
RespondError(c, http.StatusInternalServerError, "internal server error", err)
|
|
return
|
|
}
|
|
|
|
components := make([]services.ComponentView, len(localComps))
|
|
for i, lc := range localComps {
|
|
components[i] = services.ComponentView{
|
|
LotName: lc.LotName,
|
|
Description: lc.LotDescription,
|
|
Category: lc.Category,
|
|
CategoryName: lc.Category,
|
|
Model: lc.Model,
|
|
}
|
|
}
|
|
|
|
totalPages := int((total + int64(perPage) - 1) / int64(perPage))
|
|
if totalPages < 1 {
|
|
totalPages = 1
|
|
}
|
|
c.JSON(http.StatusOK, &services.ComponentListResult{
|
|
Items: components,
|
|
TotalCount: total,
|
|
Page: page,
|
|
PerPage: perPage,
|
|
TotalPages: totalPages,
|
|
})
|
|
}
|
|
|
|
func (h *ComponentHandler) Get(c *gin.Context) {
|
|
lotName := c.Param("lot_name")
|
|
component, err := h.localDB.GetLocalComponent(lotName)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "component not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, services.ComponentView{
|
|
LotName: component.LotName,
|
|
Description: component.LotDescription,
|
|
Category: component.Category,
|
|
CategoryName: component.Category,
|
|
Model: component.Model,
|
|
})
|
|
}
|
|
|
|
func (h *ComponentHandler) GetCategories(c *gin.Context) {
|
|
// Build display_order lookup from the canonical list.
|
|
orderMap := make(map[string]int, len(models.DefaultCategories))
|
|
for _, cat := range models.DefaultCategories {
|
|
orderMap[strings.ToUpper(cat.Code)] = cat.DisplayOrder
|
|
}
|
|
|
|
codes, err := h.localDB.GetLocalComponentCategories()
|
|
if err == nil && len(codes) > 0 {
|
|
categories := make([]models.Category, 0, len(codes))
|
|
for _, code := range codes {
|
|
trimmed := strings.TrimSpace(code)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
order := orderMap[strings.ToUpper(trimmed)]
|
|
if order == 0 {
|
|
order = models.MaxKnownDisplayOrder + 1
|
|
}
|
|
categories = append(categories, models.Category{
|
|
Code: trimmed,
|
|
Name: trimmed,
|
|
DisplayOrder: order,
|
|
})
|
|
}
|
|
c.JSON(http.StatusOK, categories)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, models.DefaultCategories)
|
|
}
|
|
|
|
func (h *ComponentHandler) GetConfiguratorSettings(c *gin.Context) {
|
|
s, _ := h.localDB.GetConfiguratorSettings()
|
|
if s == nil {
|
|
s = &localdb.ConfiguratorSettings{}
|
|
}
|
|
|
|
if len(s.ConfigTypes) == 0 {
|
|
s.ConfigTypes = defaultConfigTypes()
|
|
}
|
|
if len(s.TabConfig) == 0 {
|
|
s.TabConfig = defaultTabConfig()
|
|
}
|
|
if len(s.AlwaysVisibleTabs) == 0 {
|
|
s.AlwaysVisibleTabs = []string{"base", "storage", "pci"}
|
|
}
|
|
if len(s.RequiredCategories) == 0 {
|
|
s.RequiredCategories = map[string][]string{"server": {"CPU", "MEM", "BB"}}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, s)
|
|
}
|
|
|
|
func defaultConfigTypes() []localdb.ConfigTypeDef {
|
|
return []localdb.ConfigTypeDef{
|
|
{
|
|
Code: "server",
|
|
NameRu: "Сервер",
|
|
DisplayOrder: 10,
|
|
Categories: []string{
|
|
"MB", "CPU", "MEM", "RAID",
|
|
"SSD", "HDD", "M2", "EDSFF", "HHHL",
|
|
"GPU", "NIC", "HCA", "DPU", "HBA",
|
|
"PSU", "PS", "ACC", "RISERS", "CARD", "BB",
|
|
},
|
|
},
|
|
{
|
|
Code: "storage",
|
|
NameRu: "СХД",
|
|
DisplayOrder: 20,
|
|
Categories: []string{
|
|
"DKC", "CPU", "MEM", "PS",
|
|
"SSD", "HDD", "M2", "EDSFF", "HHHL",
|
|
"NIC", "HBA", "HCA", "ACC", "CARD",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func defaultTabConfig() []localdb.TabDef {
|
|
return []localdb.TabDef{
|
|
{
|
|
Key: "base",
|
|
Label: "Base",
|
|
SingleSelect: true,
|
|
Categories: []string{"MB", "CPU", "MEM", "ENC", "DKC", "CTL"},
|
|
},
|
|
{
|
|
Key: "storage",
|
|
Label: "Storage",
|
|
SingleSelect: false,
|
|
Categories: []string{"RAID", "M2", "SSD", "HDD", "EDSFF", "HHHL"},
|
|
Sections: []localdb.TabSection{
|
|
{Title: "RAID Контроллеры", Categories: []string{"RAID"}},
|
|
{Title: "Диски", Categories: []string{"M2", "SSD", "HDD", "EDSFF", "HHHL"}},
|
|
},
|
|
},
|
|
{
|
|
Key: "pci",
|
|
Label: "PCI",
|
|
SingleSelect: false,
|
|
Categories: []string{"GPU", "DPU", "NIC", "HCA", "HBA", "HIC"},
|
|
Sections: []localdb.TabSection{
|
|
{Title: "GPU / DPU", Categories: []string{"GPU", "DPU"}},
|
|
{Title: "NIC / HCA", Categories: []string{"NIC", "HCA"}},
|
|
{Title: "HBA", Categories: []string{"HBA"}},
|
|
{Title: "HIC", Categories: []string{"HIC"}},
|
|
},
|
|
},
|
|
{
|
|
Key: "power",
|
|
Label: "Power",
|
|
SingleSelect: false,
|
|
Categories: []string{"PS", "PSU"},
|
|
},
|
|
{
|
|
Key: "accessories",
|
|
Label: "Accessories",
|
|
SingleSelect: false,
|
|
Categories: []string{"ACC", "CARD"},
|
|
},
|
|
{
|
|
Key: "sw",
|
|
Label: "SW",
|
|
SingleSelect: false,
|
|
Categories: []string{"SW"},
|
|
},
|
|
}
|
|
}
|