fix: регистронезависимый поиск lot_name и удаление мёртвого кода

- SQLite-запросы по lot_name теперь используют UPPER(lot_name) IN/= для
  совместимости с легаси-данными, синхронизированными до нормализации регистра
- Удалена таблица local_components и весь связанный код синхронизации;
  источник данных для компонентов — local_pricelist_items
- Удалена функция getCategoryFromLotName из JS: категория берётся только
  из прайслиста, без инференса из имени лота
- Регистронезависимые сравнения lot_name в JS (warehouse stock set,
  addedLots, cartLots, allComponents.find, _bomLotValid)
- В support bundle добавлены: latest_pricelist_items.json, local.db,
  autocomplete_lots.json для диагностики

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 08:52:22 +03:00
parent 8b2dc6652a
commit 7d190cc7a8
19 changed files with 500 additions and 585 deletions
+106 -1
View File
@@ -74,7 +74,7 @@ func (h *SupportBundleHandler) DownloadBundle(c *gin.Context) {
// local_db_stats.json
writeJSON("local_db_stats.json", map[string]any{
"components": h.localDB.CountLocalComponents(),
"components": h.localDB.CountComponents(),
"configurations": h.localDB.CountConfigurations(),
"projects": h.localDB.CountProjects(),
"pricelists": h.localDB.CountLocalPricelists(),
@@ -139,6 +139,7 @@ func (h *SupportBundleHandler) DownloadBundle(c *gin.Context) {
CreatedAt time.Time `json:"created_at"`
SyncedAt time.Time `json:"synced_at"`
IsUsed bool `json:"is_used"`
IsActive bool `json:"is_active"`
}
bySource := map[string][]plEntry{}
for _, pl := range pricelists {
@@ -150,12 +151,78 @@ func (h *SupportBundleHandler) DownloadBundle(c *gin.Context) {
CreatedAt: pl.CreatedAt,
SyncedAt: pl.SyncedAt,
IsUsed: pl.IsUsed,
IsActive: pl.IsActive,
}
bySource[pl.Source] = append(bySource[pl.Source], e)
}
writeJSON("pricelists.json", bySource)
}
// pricelist_coverage.json — for each local estimate pricelist: item count by lot_category
if pl, err := h.localDB.GetLatestLocalPricelist(); err == nil {
type catRow struct {
Category string `json:"category"`
Count int64 `json:"count"`
}
type plCoverage struct {
Version string `json:"version"`
ServerID uint `json:"server_id"`
TotalItems int64 `json:"total_items"`
Categories []catRow `json:"categories"`
}
rows, total, catErr := h.localDB.GetLocalPricelistCoverageByCategory(pl.ID)
if catErr == nil {
cats := make([]catRow, 0, len(rows))
for cat, cnt := range rows {
cats = append(cats, catRow{Category: cat, Count: cnt})
}
writeJSON("pricelist_coverage.json", plCoverage{
Version: pl.Version,
ServerID: pl.ServerID,
TotalItems: total,
Categories: cats,
})
}
}
// configurator_settings.json — what /api/configurator-settings actually returns
if cfgSettings, err := h.localDB.GetConfiguratorSettings(); err == nil {
writeJSON("configurator_settings.json", cfgSettings)
} else {
writeJSON("configurator_settings.json", map[string]any{"error": err.Error()})
}
// component_categories.json — distinct categories in active estimate pricelist
if cats, err := h.localDB.GetLocalComponentCategories(); err == nil {
writeJSON("component_categories.json", cats)
}
// autocomplete_lots.json — per-category breakdown of lots with their prices
// Mirrors what filterAutocomplete() works with: lot_name + estimate_price per category.
if pl, err := h.localDB.GetLatestLocalPricelist(); err == nil {
if items, err := h.localDB.GetLocalPricelistItems(pl.ID); err == nil {
type lotEntry struct {
LotName string `json:"lot_name"`
Price float64 `json:"price"`
HasPrice bool `json:"has_price"`
}
byCategory := map[string][]lotEntry{}
for _, it := range items {
entry := lotEntry{
LotName: it.LotName,
Price: it.Price,
HasPrice: it.Price > 0,
}
byCategory[it.LotCategory] = append(byCategory[it.LotCategory], entry)
}
writeJSON("autocomplete_lots.json", map[string]any{
"pricelist_version": pl.Version,
"pricelist_id": pl.ServerID,
"by_category": byCategory,
})
}
}
// schema_migrations.json
migrations, err := h.localDB.GetSchemaMigrations()
if err != nil {
@@ -163,6 +230,44 @@ func (h *SupportBundleHandler) DownloadBundle(c *gin.Context) {
}
writeJSON("schema_migrations.json", migrations)
// latest_pricelist_items.json — all items from the most recent active estimate pricelist
if pl, err := h.localDB.GetLatestLocalPricelist(); err == nil {
if items, err := h.localDB.GetLocalPricelistItems(pl.ID); err == nil {
type plItem struct {
LotName string `json:"lot_name"`
LotCategory string `json:"lot_category"`
Price float64 `json:"price"`
}
out := make([]plItem, len(items))
for i, it := range items {
out[i] = plItem{
LotName: it.LotName,
LotCategory: it.LotCategory,
Price: it.Price,
}
}
writeJSON("latest_pricelist_items.json", map[string]any{
"pricelist_version": pl.Version,
"pricelist_id": pl.ServerID,
"source": pl.Source,
"item_count": len(out),
"items": out,
})
}
}
// local.db — full SQLite database file (for deep diagnostics)
if dbPath := h.localDB.DBFilePath(); dbPath != "" {
if f, err := os.Open(dbPath); err == nil {
defer f.Close()
if w, err := zw.Create("local.db"); err == nil {
if _, err := io.Copy(w, f); err != nil {
slog.Warn("support bundle: error copying local.db", "err", err)
}
}
}
}
// app.log (tail 5 MiB)
if h.logFilePath != "" {
if f, err := os.Open(h.logFilePath); err == nil {