feat: кнопка "Обновить цены" использует последний скачанный прайслист без синхронизации и показывает diff
- убрать вызовы /api/sync/components и /api/sync/pricelists из обеих кнопок - брать самый свежий прайслист из уже скачанных (active_only) - проверять галочку disable_price_refresh (пропускать конфиг если включена) - показывать модальное окно diff: компонент / цена за шт. / сумма (было → стало) + итог конфиги - общие утилиты (fetchLatestEstimatePricelistId, showPriceDiffModal) вынесены в base.html - обе кнопки вызывают refreshPrices() без дублирования кода Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -79,6 +79,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Price Diff Modal -->
|
||||
<div id="price-diff-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden p-4">
|
||||
<div class="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] flex flex-col">
|
||||
<div class="p-5 border-b border-gray-200 flex-shrink-0 flex justify-between items-center">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Изменение цен</h3>
|
||||
<button onclick="closePriceDiffModal()" class="text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="price-diff-modal-body" class="overflow-y-auto flex-1 p-5 space-y-5"></div>
|
||||
<div class="p-4 border-t border-gray-200 flex-shrink-0 flex justify-end">
|
||||
<button onclick="closePriceDiffModal()" class="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700 text-sm">Закрыть</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sync Info Modal -->
|
||||
<div id="sync-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden p-4">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-lg w-full max-h-[90vh] flex flex-col">
|
||||
@@ -517,6 +535,121 @@
|
||||
// }
|
||||
// }
|
||||
|
||||
// ==================== SHARED PRICE REFRESH UTILITIES ====================
|
||||
|
||||
async function fetchLatestEstimatePricelistId() {
|
||||
try {
|
||||
const resp = await fetch('/api/pricelists?active_only=true&source=estimate&per_page=1');
|
||||
if (!resp.ok) return null;
|
||||
const data = await resp.json();
|
||||
const list = data.pricelists || data.items || data;
|
||||
if (Array.isArray(list) && list.length > 0) return Number(list[0].id);
|
||||
} catch(_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
function _fmtMoneyDiff(value) {
|
||||
if (!Number.isFinite(Number(value))) return 'N/A';
|
||||
return '$ ' + Math.round(Number(value)).toLocaleString('ru-RU');
|
||||
}
|
||||
|
||||
function _fmtArrow(prev, next) {
|
||||
const diff = next - prev;
|
||||
if (Math.abs(diff) < 0.5) return '';
|
||||
const pct = prev > 0 ? Math.round((diff / prev) * 100) : 0;
|
||||
const sign = diff > 0 ? '+' : '';
|
||||
const color = diff > 0 ? 'text-red-600' : 'text-green-600';
|
||||
return ` <span class="${color} text-xs font-medium">(${sign}${pct}%)</span>`;
|
||||
}
|
||||
|
||||
function _buildDiffRow(lot, qty, prev, next) {
|
||||
const prevLine = prev * qty;
|
||||
const nextLine = next * qty;
|
||||
const delta = next - prev;
|
||||
const arrowColor = delta > 0 ? 'text-red-600' : 'text-green-600';
|
||||
return `<tr class="border-b border-gray-100 last:border-0">
|
||||
<td class="py-1.5 pr-3 text-sm text-gray-700 font-mono">${lot}</td>
|
||||
<td class="py-1.5 px-2 text-sm text-right text-gray-500">${qty}</td>
|
||||
<td class="py-1.5 px-2 text-sm text-right whitespace-nowrap">
|
||||
<span class="text-gray-400 line-through text-xs">${_fmtMoneyDiff(prev)}</span>
|
||||
<span class="${arrowColor} font-medium ml-1">${_fmtMoneyDiff(next)}</span>
|
||||
</td>
|
||||
<td class="py-1.5 pl-2 text-sm text-right whitespace-nowrap">
|
||||
<span class="text-gray-400 line-through text-xs">${_fmtMoneyDiff(prevLine)}</span>
|
||||
<span class="${arrowColor} font-medium ml-1">${_fmtMoneyDiff(nextLine)}</span>
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
function showPriceDiffModal(results) {
|
||||
const body = document.getElementById('price-diff-modal-body');
|
||||
if (!body) return;
|
||||
|
||||
const sections = results.filter(r => !r.skipped);
|
||||
if (sections.length === 0) {
|
||||
body.innerHTML = '<p class="text-gray-500 text-sm text-center py-4">Обновление цен отключено для всех конфигураций</p>';
|
||||
document.getElementById('price-diff-modal').classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
let anyChanges = false;
|
||||
|
||||
for (const r of sections) {
|
||||
if (r.error) {
|
||||
html += `<div class="text-sm text-red-600 bg-red-50 rounded px-3 py-2">${r.configName ? `<span class="font-medium">${r.configName}:</span> ` : ''}Ошибка обновления цен</div>`;
|
||||
continue;
|
||||
}
|
||||
|
||||
const diffs = (r.itemDiffs || []).filter(d => Math.abs(d.prevPrice - d.newPrice) > 0.01);
|
||||
const totalDelta = (r.newTotal || 0) - (r.prevTotal || 0);
|
||||
|
||||
if (results.length > 1) {
|
||||
html += `<div class="text-sm font-semibold text-gray-800 mb-1">${r.configName || '—'}</div>`;
|
||||
}
|
||||
|
||||
if (diffs.length === 0) {
|
||||
html += `<div class="text-sm text-gray-500 bg-gray-50 rounded px-3 py-2 mb-2">Изменений нет</div>`;
|
||||
} else {
|
||||
anyChanges = true;
|
||||
html += `<div class="overflow-x-auto mb-2">
|
||||
<table class="w-full text-left">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-200 text-xs text-gray-500 uppercase">
|
||||
<th class="pb-1 pr-3 font-medium">Компонент</th>
|
||||
<th class="pb-1 px-2 text-right font-medium">Кол.</th>
|
||||
<th class="pb-1 px-2 text-right font-medium">Цена / шт.</th>
|
||||
<th class="pb-1 pl-2 text-right font-medium">Сумма</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${diffs.map(d => _buildDiffRow(d.lot_name, d.quantity, d.prevPrice, d.newPrice)).join('')}</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const totalColor = totalDelta > 0 ? 'text-red-600' : totalDelta < 0 ? 'text-green-600' : 'text-gray-600';
|
||||
const totalArrow = _fmtArrow(r.prevTotal || 0, r.newTotal || 0);
|
||||
html += `<div class="flex justify-between items-center text-sm bg-gray-50 rounded px-3 py-2 mb-1">
|
||||
<span class="text-gray-600 font-medium">Итог конфигурации</span>
|
||||
<span>
|
||||
<span class="text-gray-400 line-through text-xs mr-1">${_fmtMoneyDiff(r.prevTotal || 0)}</span>
|
||||
<span class="${totalColor} font-semibold">${_fmtMoneyDiff(r.newTotal || 0)}</span>${totalArrow}
|
||||
</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (!anyChanges && sections.every(r => !r.error)) {
|
||||
html = '<div class="text-sm text-gray-500 text-center py-6">Цены актуальны — изменений нет</div>' + html;
|
||||
}
|
||||
|
||||
body.innerHTML = html;
|
||||
document.getElementById('price-diff-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closePriceDiffModal() {
|
||||
document.getElementById('price-diff-modal')?.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Call functions immediately to ensure they run even before DOMContentLoaded
|
||||
// This ensures username and admin link are visible ASAP
|
||||
loadDBUser();
|
||||
|
||||
Reference in New Issue
Block a user