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()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
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))
|
resultItems := make([]gin.H, 0, len(items))
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
resultItems = append(resultItems, gin.H{
|
resultItems = append(resultItems, gin.H{
|
||||||
"id": item.ID,
|
"id": item.ID,
|
||||||
"lot_name": item.LotName,
|
"lot_name": item.LotName,
|
||||||
"price": item.Price,
|
"lot_description": descMap[item.LotName],
|
||||||
"category": item.LotCategory,
|
"price": item.Price,
|
||||||
"available_qty": item.AvailableQty,
|
"category": item.LotCategory,
|
||||||
"partnumbers": []string(item.Partnumbers),
|
"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 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-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-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-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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="items-body" class="bg-white divide-y divide-gray-200">
|
<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() {
|
function isWarehouseSource() {
|
||||||
return (currentSource || '').toLowerCase() === 'warehouse';
|
return (currentSource || '').toLowerCase() === 'warehouse';
|
||||||
}
|
}
|
||||||
|
|
||||||
function itemsColspan() {
|
function itemsColspan() {
|
||||||
return isWarehouseSource() ? 7 : 5;
|
return isStockSource() ? 6 : 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleWarehouseColumns() {
|
function toggleWarehouseColumns() {
|
||||||
const visible = isWarehouseSource();
|
const stock = isStockSource();
|
||||||
document.getElementById('th-qty').classList.toggle('hidden', !visible);
|
document.getElementById('th-qty').classList.toggle('hidden', true);
|
||||||
document.getElementById('th-partnumbers').classList.toggle('hidden', !visible);
|
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) {
|
function formatQty(qty) {
|
||||||
@@ -234,27 +242,69 @@
|
|||||||
return;
|
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 html = items.map(item => {
|
||||||
const price = item.price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
const price = item.price.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||||
const description = item.lot_description || '-';
|
const description = item.lot_description || '-';
|
||||||
const truncatedDesc = description.length > 60 ? description.substring(0, 60) + '...' : description;
|
const truncatedDesc = description.length > descMax ? description.substring(0, descMax) + '...' : description;
|
||||||
const qty = formatQty(item.available_qty);
|
|
||||||
const partnumbers = Array.isArray(item.partnumbers) && item.partnumbers.length > 0 ? item.partnumbers.join(', ') : '—';
|
// 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 `
|
return `
|
||||||
<tr class="hover:bg-gray-50">
|
<tr class="hover:bg-gray-50">
|
||||||
<td class="px-6 py-3 whitespace-nowrap">
|
<td class="${p} max-w-[160px]">
|
||||||
<span class="font-mono text-sm">${item.lot_name}</span>
|
<span class="font-mono text-sm break-all">${escapeHtml(item.lot_name)}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-3 whitespace-nowrap">
|
<td class="${p} whitespace-nowrap">
|
||||||
<span class="px-2 py-1 text-xs bg-gray-100 rounded">${item.category || '-'}</span>
|
<span class="px-2 py-1 text-xs bg-gray-100 rounded">${escapeHtml(item.category || '-')}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-3 text-sm text-gray-500" title="${description}">${truncatedDesc}</td>
|
<td class="${p} text-sm text-gray-500" title="${escapeHtml(description)}">${escapeHtml(truncatedDesc)}</td>
|
||||||
${showWarehouse ? `<td class="px-6 py-3 whitespace-nowrap text-right font-mono">${qty}</td>` : ''}
|
${stock ? `<td class="${p} text-sm text-gray-600">${pnHtml}</td>` : ''}
|
||||||
${showWarehouse ? `<td class="px-6 py-3 text-sm text-gray-600" title="${escapeHtml(partnumbers)}">${escapeHtml(partnumbers)}</td>` : ''}
|
${stock ? `<td class="${p} text-sm">${supplierHtml}</td>` : ''}
|
||||||
<td class="px-6 py-3 whitespace-nowrap text-right font-mono">${price}</td>
|
<td class="${p} whitespace-nowrap text-right font-mono">${priceHtml}</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>
|
${!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>
|
</tr>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|||||||
Reference in New Issue
Block a user