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:
Mikhail Chusavitin
2026-07-22 19:01:47 +03:00
co-authored by Claude Sonnet 5
parent 3348008198
commit ee92c3b392
11 changed files with 169 additions and 24 deletions
+9 -7
View File
@@ -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
+45
View File
@@ -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).
+1
View File
@@ -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 {