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/services" "github.com/gin-gonic/gin" ) type ComponentHandler struct { localDB *localdb.LocalDB } func NewComponentHandler(localDB *localdb.LocalDB) *ComponentHandler { return &ComponentHandler{ 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 } localFilter := localdb.ComponentFilter{ Category: c.Query("category"), Search: c.Query("search"), HasPrice: c.Query("has_price") == "true", } 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"}} } if s.SupportPricing == nil { s.SupportPricing = defaultSupportPricing() } c.JSON(http.StatusOK, s) } // defaultSupportPricing mirrors "Регламент расчёта стоимости технической // поддержки серверов" (BASE/STANDARD/PREMIUM tiers, x86 % of sale price / // HGX fixed multi-year price by chip generation). Used only when the // qt_settings "support_pricing" key is absent so it can be edited in // MariaDB without a QuoteForge release. func defaultSupportPricing() *localdb.SupportPricingTable { return &localdb.SupportPricingTable{ X86Percent: map[string]map[string]float64{ "W": {"3": 0.06}, "B": {"1": 0.05, "3": 0.10, "5": 0.14}, "S": {"1": 0.07, "3": 0.13, "5": 0.18}, "P": {"1": 0.12, "3": 0.20, "5": 0.45}, }, HGXPrice: map[string]map[string]map[string]float64{ "HGX-H200": { "B": {"1": 20000, "3": 55000, "5": 80000}, "S": {"1": 28000, "3": 70000, "5": 100000}, "P": {"1": 63000, "3": 105000}, }, "HGX-B200": { "B": {"1": 40000, "3": 100000, "5": 145000}, "S": {"1": 55000, "3": 130000, "5": 185000}, "P": {"1": 101000, "3": 176000}, }, "HGX-B300": { "B": {"1": 40000, "3": 100000, "5": 145000}, "S": {"1": 55000, "3": 130000, "5": 185000}, "P": {"1": 112000, "3": 187000}, }, }, } } 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"}, }, } }