Redesign pricelist detail: differentiated layout by source type

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-03-13 13:14:14 +03:00
parent b3003c4858
commit a0a57e0969
2 changed files with 98 additions and 24 deletions

View File

@@ -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,
})
}

View File

@@ -60,8 +60,9 @@
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Описание</th>
<th id="th-qty" class="hidden px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Доступно</th>
<th id="th-partnumbers" class="hidden px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Partnumbers</th>
<th id="th-competitors" class="hidden px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Поставщик</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Цена, $</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Настройки</th>
<th id="th-settings" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Настройки</th>
</tr>
</thead>
<tbody id="items-body" class="bg-white divide-y divide-gray-200">
@@ -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) ? ` <span class="text-gray-400">(${formatQty(q)} шт.)</span>` : '';
return `<div><span class="font-mono text-xs">${escapeHtml(pn)}</span>${qStr}</div>`;
};
pnHtml = shown.map(formatPN).join('');
if (rest > 0) pnHtml += `<div class="text-gray-400 text-xs">+${rest} ещё</div>`;
}
}
// Supplier cell (stock sources only)
let supplierHtml = '';
if (stock) {
if (isWarehouseSource()) {
supplierHtml = `<span class="text-gray-500 text-sm">склад</span>`;
} else {
const names = Array.isArray(item.competitor_names) && item.competitor_names.length > 0
? item.competitor_names
: ['конкурент'];
supplierHtml = names.map(n => `<span class="px-1.5 py-0.5 text-xs bg-blue-50 text-blue-700 rounded">${escapeHtml(n)}</span>`).join(' ');
}
}
// Price cell — add spread badge for competitor
let priceHtml = price;
if (!isWarehouseSource() && item.price_spread_pct > 0) {
priceHtml += ` <span class="text-xs text-amber-600 font-medium" title="Разброс цен конкурентов">±${item.price_spread_pct.toFixed(0)}%</span>`;
}
return `
<tr class="hover:bg-gray-50">
<td class="px-6 py-3 whitespace-nowrap">
<span class="font-mono text-sm">${item.lot_name}</span>
<td class="${p} max-w-[160px]">
<span class="font-mono text-sm break-all">${escapeHtml(item.lot_name)}</span>
</td>
<td class="px-6 py-3 whitespace-nowrap">
<span class="px-2 py-1 text-xs bg-gray-100 rounded">${item.category || '-'}</span>
<td class="${p} whitespace-nowrap">
<span class="px-2 py-1 text-xs bg-gray-100 rounded">${escapeHtml(item.category || '-')}</span>
</td>
<td class="px-6 py-3 text-sm text-gray-500" title="${description}">${truncatedDesc}</td>
${showWarehouse ? `<td class="px-6 py-3 whitespace-nowrap text-right font-mono">${qty}</td>` : ''}
${showWarehouse ? `<td class="px-6 py-3 text-sm text-gray-600" title="${escapeHtml(partnumbers)}">${escapeHtml(partnumbers)}</td>` : ''}
<td class="px-6 py-3 whitespace-nowrap text-right font-mono">${price}</td>
<td class="px-6 py-3 whitespace-nowrap text-sm"><span class="text-xs bg-gray-100 px-2 py-1 rounded">${formatPriceSettings(item)}</span></td>
<td class="${p} text-sm text-gray-500" title="${escapeHtml(description)}">${escapeHtml(truncatedDesc)}</td>
${stock ? `<td class="${p} text-sm text-gray-600">${pnHtml}</td>` : ''}
${stock ? `<td class="${p} text-sm">${supplierHtml}</td>` : ''}
<td class="${p} whitespace-nowrap text-right font-mono">${priceHtml}</td>
${!stock ? `<td class="${p} whitespace-nowrap text-sm"><span class="text-xs bg-gray-100 px-2 py-1 rounded">${formatPriceSettings(item)}</span></td>` : ''}
</tr>
`;
}).join('');