Redesign pricelist detail: differentiated layout by source type
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
"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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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('');
|
||||
|
||||
Reference in New Issue
Block a user