feat: price_quality в конфигураторе; фикс пустого lot_description в экспорте цен
price_quality теперь синхронизируется не только в прайслист, но и в LocalComponent/services.ComponentView (/api/components) — виден в выпадающем списке поиска (цветная точка), в таблице конфигуратора (левая колонка-пиктограмма) и на вкладке «Ценообразование» (подсветка строки). Цветовая шкала вынесена в один модуль web/static/price-quality.js (подключён в base.html), чтобы не дублировать расчёт цвета по шаблонам. Шкала — градиент 0 (красный) → 5 (жёлтый) → 9 (зелёный). Экспорт цен (internal/services/export.go): resolveLotDescriptions был заглушкой, всегда возвращавшей пустую карту — строки BOM/не-BOM в экспорте всегда оставались без описания LOT. Заменён на реальный запрос к local_pricelist_items текущего выбранного прайслиста (LocalDB.GetLocalDescriptionsForLots), по аналогии с уже существующим GetLocalLotCategoriesByServerPricelistID. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
3348008198
commit
ee92c3b392
@@ -8,6 +8,7 @@
|
||||
<link rel="stylesheet" href="/static/app.css">
|
||||
<script src="/static/vendor/tailwindcss.browser.js"></script>
|
||||
<script src="/static/vendor/htmx-1.9.10.min.js"></script>
|
||||
<script src="/static/price-quality.js"></script>
|
||||
<style>
|
||||
.htmx-request { opacity: 0.5; }
|
||||
.line-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
|
||||
@@ -1483,6 +1483,7 @@ function renderSingleSelectTab(categories) {
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-2 py-2 w-6" title="Качество цены"></th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase w-24">Тип</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">LOT</th>
|
||||
<th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Описание</th>
|
||||
@@ -1509,6 +1510,7 @@ function renderSingleSelectTab(categories) {
|
||||
|
||||
html += `
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-2 py-2 text-center">${qualityDotHtml(comp?.price_quality)}</td>
|
||||
<td class="px-3 py-2 text-sm font-medium text-gray-700">${catLabel}</td>
|
||||
<td class="px-3 py-2">
|
||||
<div class="autocomplete-wrapper relative">
|
||||
@@ -1859,7 +1861,7 @@ function renderAutocomplete() {
|
||||
return `
|
||||
<div class="autocomplete-item ${idx === autocompleteIndex ? 'selected' : ''}"
|
||||
onmousedown="${onmousedown}">
|
||||
<div class="font-mono text-sm">${escapeHtml(comp.lot_name)}</div>
|
||||
<div class="font-mono text-sm flex items-center gap-1.5">${qualityDotHtml(comp.price_quality)}${escapeHtml(comp.lot_name)}</div>
|
||||
<div class="text-xs text-gray-500 truncate">${escapeHtml(comp.description || '')}</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -4539,6 +4541,7 @@ async function renderPricingTab() {
|
||||
lotCell: escapeHtml(item.lot_name), lotText: item.lot_name,
|
||||
vendorPN: null,
|
||||
desc: (compMap[U(item.lot_name)] || {}).description || '',
|
||||
priceQuality: (compMap[U(item.lot_name)] || {}).price_quality,
|
||||
qty: item.quantity,
|
||||
estUnit, warehouseUnit: u.warehouseUnit, competitorUnit: u.competitorUnit,
|
||||
est: estUnit * item.quantity,
|
||||
@@ -4586,6 +4589,7 @@ async function renderPricingTab() {
|
||||
warehouse: u.warehouseUnit != null ? u.warehouseUnit * qty : null,
|
||||
competitor: u.competitorUnit != null ? u.competitorUnit * qty : null,
|
||||
estWorld: u.estWorld, whWorld: u.whWorld, compWorld: u.compWorld,
|
||||
priceQuality: (compMap[U(baseLot)] || {}).price_quality,
|
||||
});
|
||||
}
|
||||
allocs.forEach(a => {
|
||||
@@ -4599,6 +4603,7 @@ async function renderPricingTab() {
|
||||
warehouse: u.warehouseUnit != null ? u.warehouseUnit * qty : null,
|
||||
competitor: u.competitorUnit != null ? u.competitorUnit * qty : null,
|
||||
estWorld: u.estWorld, whWorld: u.whWorld, compWorld: u.compWorld,
|
||||
priceQuality: (compMap[U(a.lot_name)] || {}).price_quality,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4611,6 +4616,7 @@ async function renderPricingTab() {
|
||||
vendorOrig, vendorOrigUnit, isEstOnly: false,
|
||||
groupStart: true, groupSize: 1,
|
||||
estWorld: false, whWorld: false, compWorld: false,
|
||||
priceQuality: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -4629,6 +4635,7 @@ async function renderPricingTab() {
|
||||
groupStart: idx === 0,
|
||||
groupSize: idx === 0 ? groupSize : 0,
|
||||
estWorld: sub.estWorld, whWorld: sub.whWorld, compWorld: sub.compWorld,
|
||||
priceQuality: sub.priceQuality,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4657,7 +4664,11 @@ async function renderPricingTab() {
|
||||
rowData.forEach(r => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.classList.add('pricing-row-buy');
|
||||
if (r.isEstOnly) tr.classList.add('bg-blue-50');
|
||||
if (typeof r.priceQuality === 'number') {
|
||||
tr.setAttribute('style', qualityRowStyle(r.priceQuality));
|
||||
} else if (r.isEstOnly) {
|
||||
tr.classList.add('bg-blue-50');
|
||||
}
|
||||
tr.dataset.est = r.est;
|
||||
tr.dataset.qty = r.qty;
|
||||
tr.dataset.vendorOrig = r.vendorOrig != null ? r.vendorOrig : '';
|
||||
@@ -4706,7 +4717,11 @@ async function renderPricingTab() {
|
||||
rowData.forEach(r => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.classList.add('pricing-row-sale');
|
||||
if (r.isEstOnly) tr.classList.add('bg-blue-50');
|
||||
if (typeof r.priceQuality === 'number') {
|
||||
tr.setAttribute('style', qualityRowStyle(r.priceQuality));
|
||||
} else if (r.isEstOnly) {
|
||||
tr.classList.add('bg-blue-50');
|
||||
}
|
||||
const saleEstUnit = r.estUnit > 0 ? r.estUnit * saleUplift : 0;
|
||||
const saleWhUnit = r.warehouseUnit != null ? r.warehouseUnit * SALE_FIXED_MULT : null;
|
||||
const saleCompUnit = r.competitorUnit != null ? r.competitorUnit * SALE_FIXED_MULT : null;
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
<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 class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Цена, $</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase">Качество</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="items-body" class="bg-white divide-y divide-gray-200">
|
||||
@@ -155,14 +156,7 @@
|
||||
}
|
||||
|
||||
function itemsColspan() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
function formatPriceQualityWarning(quality) {
|
||||
if (typeof quality !== 'number') return '';
|
||||
if (quality >= 7) return '';
|
||||
const color = quality >= 4 ? 'text-amber-500' : 'text-red-600';
|
||||
return ` <span class="${color} font-bold" title="Качество цены: ${quality}/9">!</span>`;
|
||||
return 5;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
@@ -198,12 +192,11 @@
|
||||
|
||||
|
||||
|
||||
// Price cell — add spread badge for competitor, plus a price-quality warning
|
||||
// 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>`;
|
||||
}
|
||||
priceHtml += formatPriceQualityWarning(item.price_quality);
|
||||
|
||||
return `
|
||||
<tr class="hover:bg-gray-50">
|
||||
@@ -215,6 +208,7 @@
|
||||
</td>
|
||||
<td class="${p} text-sm text-gray-500" title="${escapeHtml(description)}">${escapeHtml(truncatedDesc)}</td>
|
||||
<td class="${p} whitespace-nowrap text-right font-mono">${priceHtml}</td>
|
||||
<td class="${p} whitespace-nowrap text-center">${qualityBadgeHtml(item.price_quality)}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
Reference in New Issue
Block a user