Redesign configurator UI with tabs and remove Excel export

- Add tab-based configurator (Base, Storage, PCI, Power, Accessories, Other)
- Base tab: single-select with autocomplete for MB, CPU, MEM
- Other tabs: multi-select with autocomplete and quantity input
- Table view with LOT, Description, Price, Quantity, Total columns
- Add configuration list page with create modal (opportunity number)
- Remove Excel export functionality and excelize dependency
- Increase component list limit from 100 to 5000
- Add web templates (base, index, configs, login, admin_pricing)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-01-26 15:57:15 +03:00
parent 44ccb01203
commit a93644131c
14 changed files with 2041 additions and 201 deletions

View File

@@ -3,8 +3,8 @@ package services
import (
"strings"
"github.com/mchus/quoteforge/internal/models"
"github.com/mchus/quoteforge/internal/repository"
"git.mchus.pro/mchus/quoteforge/internal/models"
"git.mchus.pro/mchus/quoteforge/internal/repository"
)
type ComponentService struct {
@@ -25,19 +25,16 @@ func NewComponentService(
}
}
// ParsePartNumber extracts category, vendor, model from lot_name
// "CPU_AMD_9654" → category="CPU", vendor="AMD", model="9654"
// "MB_INTEL_4.Sapphire_2S_32xDDR5" → category="MB", vendor="INTEL", model="4.Sapphire_2S_32xDDR5"
func ParsePartNumber(lotName string) (category, vendor, model string) {
parts := strings.SplitN(lotName, "_", 3)
// ParsePartNumber extracts category and model from lot_name
// "CPU_AMD_9654" → category="CPU", model="AMD_9654"
// "MB_INTEL_4.Sapphire_2S_32xDDR5" → category="MB", model="INTEL_4.Sapphire_2S_32xDDR5"
func ParsePartNumber(lotName string) (category, model string) {
parts := strings.SplitN(lotName, "_", 2)
if len(parts) >= 1 {
category = parts[0]
}
if len(parts) >= 2 {
vendor = parts[1]
}
if len(parts) >= 3 {
model = parts[2]
model = parts[1]
}
return
}
@@ -50,25 +47,27 @@ type ComponentListResult struct {
}
type ComponentView struct {
LotName string `json:"lot_name"`
Description string `json:"description"`
Category string `json:"category"`
CategoryName string `json:"category_name"`
Vendor string `json:"vendor"`
Model string `json:"model"`
CurrentPrice *float64 `json:"current_price"`
PriceFreshness models.PriceFreshness `json:"price_freshness"`
PopularityScore float64 `json:"popularity_score"`
Specs models.Specs `json:"specs,omitempty"`
LotName string `json:"lot_name"`
Description string `json:"description"`
Category string `json:"category"`
CategoryName string `json:"category_name"`
Model string `json:"model"`
CurrentPrice *float64 `json:"current_price"`
PriceFreshness models.PriceFreshness `json:"price_freshness"`
PopularityScore float64 `json:"popularity_score"`
Specs models.Specs `json:"specs,omitempty"`
}
func (s *ComponentService) List(filter repository.ComponentFilter, page, perPage int) (*ComponentListResult, error) {
if page < 1 {
page = 1
}
if perPage < 1 || perPage > 100 {
if perPage < 1 {
perPage = 20
}
if perPage > 5000 {
perPage = 5000
}
offset := (page - 1) * perPage
components, total, err := s.componentRepo.List(filter, offset, perPage)
@@ -80,7 +79,6 @@ func (s *ComponentService) List(filter repository.ComponentFilter, page, perPage
for i, c := range components {
view := ComponentView{
LotName: c.LotName,
Vendor: c.Vendor,
Model: c.Model,
CurrentPrice: c.CurrentPrice,
PriceFreshness: c.GetPriceFreshness(30, 60, 90, 3),
@@ -118,7 +116,6 @@ func (s *ComponentService) GetByLotName(lotName string) (*ComponentView, error)
view := &ComponentView{
LotName: c.LotName,
Vendor: c.Vendor,
Model: c.Model,
CurrentPrice: c.CurrentPrice,
PriceFreshness: c.GetPriceFreshness(30, 60, 90, 3),
@@ -141,10 +138,6 @@ func (s *ComponentService) GetCategories() ([]models.Category, error) {
return s.categoryRepo.GetAll()
}
func (s *ComponentService) GetVendors(category string) ([]string, error) {
return s.componentRepo.GetVendors(category)
}
// ImportFromLot creates metadata entries for lots that don't have them
func (s *ComponentService) ImportFromLot() (int, error) {
lots, err := s.componentRepo.GetLotsWithoutMetadata()
@@ -164,11 +157,10 @@ func (s *ComponentService) ImportFromLot() (int, error) {
imported := 0
for _, lot := range lots {
category, vendor, model := ParsePartNumber(lot.LotName)
category, model := ParsePartNumber(lot.LotName)
metadata := &models.LotMetadata{
LotName: lot.LotName,
Vendor: vendor,
Model: model,
Specs: make(models.Specs),
}