From a0a57e0969aff89a03407e55aabd9aa4be10ca05 Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Fri, 13 Mar 2026 13:14:14 +0300 Subject: [PATCH] Redesign pricelist detail: differentiated layout by source type Co-Authored-By: Claude Sonnet 4.6 --- internal/handlers/pricelist.go | 36 ++++++++++-- web/templates/pricelist_detail.html | 86 +++++++++++++++++++++++------ 2 files changed, 98 insertions(+), 24 deletions(-) diff --git a/internal/handlers/pricelist.go b/internal/handlers/pricelist.go index 6807006..cbb7e03 100644 --- a/internal/handlers/pricelist.go +++ b/internal/handlers/pricelist.go @@ -181,15 +181,39 @@ func (h *PricelistHandler) GetItems(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } + lotNames := make([]string, len(items)) + for i, item := range items { + lotNames[i] = item.LotName + } + type compRow struct { + LotName string + LotDescription string + } + var comps []compRow + if len(lotNames) > 0 { + h.localDB.DB().Table("local_components"). + Select("lot_name, lot_description"). + Where("lot_name IN ?", lotNames). + Scan(&comps) + } + descMap := make(map[string]string, len(comps)) + for _, c := range comps { + descMap[c.LotName] = c.LotDescription + } + resultItems := make([]gin.H, 0, len(items)) for _, item := range items { resultItems = append(resultItems, gin.H{ - "id": item.ID, - "lot_name": item.LotName, - "price": item.Price, - "category": item.LotCategory, - "available_qty": item.AvailableQty, - "partnumbers": []string(item.Partnumbers), + "id": item.ID, + "lot_name": item.LotName, + "lot_description": descMap[item.LotName], + "price": item.Price, + "category": item.LotCategory, + "available_qty": item.AvailableQty, + "partnumbers": []string(item.Partnumbers), + "partnumber_qtys": map[string]interface{}{}, + "competitor_names": []string{}, + "price_spread_pct": nil, }) } diff --git a/web/templates/pricelist_detail.html b/web/templates/pricelist_detail.html index 169f793..564582a 100644 --- a/web/templates/pricelist_detail.html +++ b/web/templates/pricelist_detail.html @@ -60,8 +60,9 @@ Описание Доступно Partnumbers + Поставщик Цена, $ - Настройки + Настройки @@ -150,18 +151,25 @@ } } + function isStockSource() { + const src = (currentSource || '').toLowerCase(); + return src === 'warehouse' || src === 'competitor'; + } + function isWarehouseSource() { return (currentSource || '').toLowerCase() === 'warehouse'; } function itemsColspan() { - return isWarehouseSource() ? 7 : 5; + return isStockSource() ? 6 : 5; } function toggleWarehouseColumns() { - const visible = isWarehouseSource(); - document.getElementById('th-qty').classList.toggle('hidden', !visible); - document.getElementById('th-partnumbers').classList.toggle('hidden', !visible); + const stock = isStockSource(); + document.getElementById('th-qty').classList.toggle('hidden', true); + document.getElementById('th-partnumbers').classList.toggle('hidden', !stock); + document.getElementById('th-competitors').classList.toggle('hidden', !stock); + document.getElementById('th-settings').classList.toggle('hidden', stock); } function formatQty(qty) { @@ -234,27 +242,69 @@ return; } - const showWarehouse = isWarehouseSource(); + const stock = isStockSource(); + const p = stock ? 'px-3 py-2' : 'px-6 py-3'; + const descMax = stock ? 30 : 60; + const html = items.map(item => { const price = item.price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const description = item.lot_description || '-'; - const truncatedDesc = description.length > 60 ? description.substring(0, 60) + '...' : description; - const qty = formatQty(item.available_qty); - const partnumbers = Array.isArray(item.partnumbers) && item.partnumbers.length > 0 ? item.partnumbers.join(', ') : '—'; + const truncatedDesc = description.length > descMax ? description.substring(0, descMax) + '...' : description; + + // Partnumbers cell (stock sources only) + let pnHtml = '—'; + if (stock) { + const qtys = item.partnumber_qtys || {}; + const allPNs = Array.isArray(item.partnumbers) ? item.partnumbers : []; + const withQty = allPNs.filter(pn => qtys[pn] > 0); + const list = withQty.length > 0 ? withQty : allPNs; + if (list.length === 0) { + pnHtml = '—'; + } else { + const shown = list.slice(0, 4); + const rest = list.length - shown.length; + const formatPN = pn => { + const q = qtys[pn]; + const qStr = (q > 0) ? ` (${formatQty(q)} шт.)` : ''; + return `
${escapeHtml(pn)}${qStr}
`; + }; + pnHtml = shown.map(formatPN).join(''); + if (rest > 0) pnHtml += `
+${rest} ещё
`; + } + } + + // Supplier cell (stock sources only) + let supplierHtml = ''; + if (stock) { + if (isWarehouseSource()) { + supplierHtml = `склад`; + } else { + const names = Array.isArray(item.competitor_names) && item.competitor_names.length > 0 + ? item.competitor_names + : ['конкурент']; + supplierHtml = names.map(n => `${escapeHtml(n)}`).join(' '); + } + } + + // Price cell — add spread badge for competitor + let priceHtml = price; + if (!isWarehouseSource() && item.price_spread_pct > 0) { + priceHtml += ` ±${item.price_spread_pct.toFixed(0)}%`; + } return ` - - ${item.lot_name} + + ${escapeHtml(item.lot_name)} - - ${item.category || '-'} + + ${escapeHtml(item.category || '-')} - ${truncatedDesc} - ${showWarehouse ? `${qty}` : ''} - ${showWarehouse ? `${escapeHtml(partnumbers)}` : ''} - ${price} - ${formatPriceSettings(item)} + ${escapeHtml(truncatedDesc)} + ${stock ? `${pnHtml}` : ''} + ${stock ? `${supplierHtml}` : ''} + ${priceHtml} + ${!stock ? `${formatPriceSettings(item)}` : ''} `; }).join('');