feat: price_quality в конфигураторе; фикс пустого lot_description в экспорте цен
price_quality теперь синхронизируется не только в прайслист, но и в LocalComponent/services.ComponentView (/api/components) — виден в выпадающем списке поиска (цветная точка), в таблице конфигуратора (левая колонка-пиктограмма) и на вкладке «Ценообразование» (подсветка строки). Цветовая шкала вынесена в один модуль web/static/price-quality.js (подключён в base.html), чтобы не дублировать расчёт цвета по шаблонам. Шкала — градиент 0 (красный) → 5 (жёлтый) → 9 (зелёный). Экспорт цен (internal/services/export.go): resolveLotDescriptions был заглушкой, всегда возвращавшей пустую карту — строки BOM/не-BOM в экспорте всегда оставались без описания LOT. Заменён на реальный запрос к local_pricelist_items текущего выбранного прайслиста (LocalDB.GetLocalDescriptionsForLots), по аналогии с уже существующим GetLocalLotCategoriesByServerPricelistID. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
3348008198
commit
ee92c3b392
@@ -51,6 +51,7 @@ func (h *ComponentHandler) List(c *gin.Context) {
|
||||
Category: lc.Category,
|
||||
CategoryName: lc.Category,
|
||||
Model: lc.Model,
|
||||
PriceQuality: lc.PriceQuality,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +82,7 @@ func (h *ComponentHandler) Get(c *gin.Context) {
|
||||
Category: component.Category,
|
||||
CategoryName: component.Category,
|
||||
Model: component.Model,
|
||||
PriceQuality: component.PriceQuality,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -42,9 +42,10 @@ func (l *LocalDB) latestActivePricelistID(source string) (uint, error) {
|
||||
|
||||
// pricelistItemRow is used for scanning rows from local_pricelist_items.
|
||||
type pricelistItemRow struct {
|
||||
LotName string `gorm:"column:lot_name"`
|
||||
Category string `gorm:"column:lot_category"`
|
||||
Description string `gorm:"column:lot_description"`
|
||||
LotName string `gorm:"column:lot_name"`
|
||||
Category string `gorm:"column:lot_category"`
|
||||
Description string `gorm:"column:lot_description"`
|
||||
PriceQuality *int `gorm:"column:price_quality"`
|
||||
}
|
||||
|
||||
func (r pricelistItemRow) toLocalComponent() LocalComponent {
|
||||
@@ -52,6 +53,7 @@ func (r pricelistItemRow) toLocalComponent() LocalComponent {
|
||||
LotName: r.LotName,
|
||||
Category: r.Category,
|
||||
LotDescription: r.Description,
|
||||
PriceQuality: r.PriceQuality,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +75,7 @@ func (l *LocalDB) SearchLocalComponents(query string, limit int) ([]LocalCompone
|
||||
}
|
||||
|
||||
var rows []pricelistItemRow
|
||||
if err := db.Select("lot_name, lot_category, lot_description").Order("lot_name").Limit(limit).Scan(&rows).Error; err != nil {
|
||||
if err := db.Select("lot_name, lot_category, lot_description, price_quality").Order("lot_name").Limit(limit).Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
components := make([]LocalComponent, len(rows))
|
||||
@@ -101,7 +103,7 @@ func (l *LocalDB) SearchLocalComponentsByCategory(category, query string, limit
|
||||
}
|
||||
|
||||
var rows []pricelistItemRow
|
||||
if err := db.Select("lot_name, lot_category, lot_description").Order("lot_name").Limit(limit).Scan(&rows).Error; err != nil {
|
||||
if err := db.Select("lot_name, lot_category, lot_description, price_quality").Order("lot_name").Limit(limit).Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
components := make([]LocalComponent, len(rows))
|
||||
@@ -135,7 +137,7 @@ func (l *LocalDB) ListComponents(filter ComponentFilter, offset, limit int) ([]L
|
||||
}
|
||||
|
||||
var rows []pricelistItemRow
|
||||
if err := db.Select("lot_name, lot_category, lot_description").Order("lot_name").Offset(offset).Limit(limit).Scan(&rows).Error; err != nil {
|
||||
if err := db.Select("lot_name, lot_category, lot_description, price_quality").Order("lot_name").Offset(offset).Limit(limit).Scan(&rows).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
components := make([]LocalComponent, len(rows))
|
||||
@@ -155,7 +157,7 @@ func (l *LocalDB) GetLocalComponent(lotName string) (*LocalComponent, error) {
|
||||
|
||||
var row pricelistItemRow
|
||||
if err := l.db.Table("local_pricelist_items").
|
||||
Select("lot_name, lot_category, lot_description").
|
||||
Select("lot_name, lot_category, lot_description, price_quality").
|
||||
Where("pricelist_id = ? AND UPPER(lot_name) = ?", pricelistID, strings.ToUpper(lotName)).
|
||||
First(&row).Error; err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1630,6 +1630,51 @@ func (l *LocalDB) GetLocalLotCategoriesByServerPricelistID(serverPricelistID uin
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetLocalDescriptionsForLots returns lot_description for each lot_name from a local
|
||||
// pricelist resolved by server ID. Missing lots are not included in the map.
|
||||
func (l *LocalDB) GetLocalDescriptionsForLots(serverPricelistID uint, lotNames []string) (map[string]string, error) {
|
||||
result := make(map[string]string, len(lotNames))
|
||||
if serverPricelistID == 0 || len(lotNames) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
localPL, err := l.GetLocalPricelistByServerID(serverPricelistID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type row struct {
|
||||
LotName string `gorm:"column:lot_name"`
|
||||
LotDescription string `gorm:"column:lot_description"`
|
||||
}
|
||||
// Build uppercase → original mapping so result keys match what the caller passed.
|
||||
upperToOrig := make(map[string]string, len(lotNames))
|
||||
upper := make([]string, len(lotNames))
|
||||
for i, n := range lotNames {
|
||||
u := strings.ToUpper(n)
|
||||
upper[i] = u
|
||||
upperToOrig[u] = n
|
||||
}
|
||||
var rows []row
|
||||
if err := l.db.Model(&LocalPricelistItem{}).
|
||||
Select("lot_name, lot_description").
|
||||
Where("pricelist_id = ? AND UPPER(lot_name) IN ?", localPL.ID, upper).
|
||||
Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range rows {
|
||||
if r.LotDescription == "" {
|
||||
continue
|
||||
}
|
||||
orig := upperToOrig[strings.ToUpper(r.LotName)]
|
||||
if orig == "" {
|
||||
orig = r.LotName
|
||||
}
|
||||
result[orig] = r.LotDescription
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// MarkPricelistAsUsed marks a pricelist as used by a configuration
|
||||
func (l *LocalDB) MarkPricelistAsUsed(pricelistID uint, isUsed bool) error {
|
||||
return l.db.Model(&LocalPricelist{}).Where("id = ?", pricelistID).
|
||||
|
||||
@@ -213,6 +213,7 @@ type LocalComponent struct {
|
||||
LotDescription string `json:"lot_description"`
|
||||
Category string `json:"category"`
|
||||
Model string `json:"model"`
|
||||
PriceQuality *int `json:"price_quality,omitempty"`
|
||||
}
|
||||
|
||||
func (LocalComponent) TableName() string {
|
||||
|
||||
@@ -14,4 +14,5 @@ type ComponentView struct {
|
||||
Category string `json:"category"`
|
||||
CategoryName string `json:"category_name"`
|
||||
Model string `json:"model"`
|
||||
PriceQuality *int `json:"price_quality,omitempty"`
|
||||
}
|
||||
|
||||
@@ -687,8 +687,35 @@ func (s *ExportService) batchLookupPrices(serverPricelistID *uint, lots []string
|
||||
return prices
|
||||
}
|
||||
|
||||
func (s *ExportService) resolveLotDescriptions(_ *models.Configuration, _ *localdb.LocalConfiguration) map[string]string {
|
||||
return map[string]string{}
|
||||
// resolveLotDescriptions returns each LOT's description from the configuration's
|
||||
// currently selected estimate pricelist (falling back to the latest active one),
|
||||
// mirroring the pricelist resolution in resolvePricingTotals. This is the single
|
||||
// source of LOT description across the project — see bible-local/03-database.md.
|
||||
func (s *ExportService) resolveLotDescriptions(cfg *models.Configuration, localCfg *localdb.LocalConfiguration) map[string]string {
|
||||
if s.localDB == nil {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
lots := collectPricingLots(cfg, localCfg, true)
|
||||
if len(lots) == 0 {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
estimateID := cfg.PricelistID
|
||||
if estimateID == nil || *estimateID == 0 {
|
||||
if latest, err := s.localDB.GetLatestLocalPricelistBySource("estimate"); err == nil && latest != nil {
|
||||
estimateID = &latest.ServerID
|
||||
}
|
||||
}
|
||||
if estimateID == nil || *estimateID == 0 {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
descriptions, err := s.localDB.GetLocalDescriptionsForLots(*estimateID, lots)
|
||||
if err != nil {
|
||||
return map[string]string{}
|
||||
}
|
||||
return descriptions
|
||||
}
|
||||
|
||||
func collectPricingLots(cfg *models.Configuration, localCfg *localdb.LocalConfiguration, includeBOM bool) []string {
|
||||
|
||||
Reference in New Issue
Block a user