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:
2026-06-11 04:26:25 +03:00
parent 56782fa718
commit 09d694234d
3 changed files with 226 additions and 106 deletions

View File

@@ -2817,7 +2817,6 @@ async function exportCSVWithCustomPrice() {
}
async function refreshPrices() {
// RBAC disabled - no token check required
if (!configUUID) return;
if (disablePriceRefresh) {
showToast('Обновление цен отключено в настройках', 'error');
@@ -2834,30 +2833,7 @@ async function refreshPrices() {
refreshBtn.className = 'px-4 py-2 bg-gray-300 text-gray-500 rounded cursor-not-allowed';
}
let serverSyncSkipped = false;
try {
const statusResp = await fetch('/api/sync/status');
const statusData = statusResp.ok ? await statusResp.json() : null;
if (statusData && statusData.is_online) {
const componentSyncResp = await fetch('/api/sync/components', { method: 'POST' });
if (!componentSyncResp.ok) throw new Error('component sync failed');
const pricelistSyncResp = await fetch('/api/sync/pricelists', { method: 'POST' });
if (!pricelistSyncResp.ok) throw new Error('pricelist sync failed');
} else {
serverSyncSkipped = true;
}
} catch(syncErr) {
if (syncErr.message === 'component sync failed' || syncErr.message === 'pricelist sync failed') {
throw syncErr;
}
serverSyncSkipped = true;
}
await Promise.all([
loadActivePricelists(true),
loadAllComponents()
]);
await loadActivePricelists(true);
['estimate', 'warehouse', 'competitor'].forEach(source => {
const latest = activePricelistsBySource[source]?.[0];
@@ -2871,22 +2847,44 @@ async function refreshPrices() {
renderPricelistSettingsSummary();
persistLocalPriceSettings();
// Snapshot prices before refresh for diff
const beforePricesMap = {};
let beforeTotal = 0;
for (const item of cart) {
const p = getDisplayPrice(item);
beforePricesMap[item.lot_name] = { price: p, qty: item.quantity };
beforeTotal += p * item.quantity;
}
beforeTotal *= serverCount;
await saveConfig(false);
await refreshPriceLevels({ force: true, noCache: true });
renderTab();
updateCartUI();
// Compute diff after refresh
const itemDiffs = [];
let afterTotal = 0;
for (const item of cart) {
const newPrice = getDisplayPrice(item);
afterTotal += newPrice * item.quantity;
const before = beforePricesMap[item.lot_name];
if (before && Math.abs(before.price - newPrice) > 0.01) {
itemDiffs.push({ lot_name: item.lot_name, quantity: item.quantity, prevPrice: before.price, newPrice });
}
}
afterTotal *= serverCount;
if (configUUID) {
const configResp = await fetch('/api/configs/' + configUUID);
if (configResp.ok) {
const config = await configResp.json();
if (config.price_updated_at) {
updatePriceUpdateDate(config.price_updated_at);
}
if (config.price_updated_at) updatePriceUpdateDate(config.price_updated_at);
}
}
showToast(serverSyncSkipped ? 'Цены обновлены (без связи с сервером)' : 'Цены обновлены', 'success');
showToast('Цены обновлены', 'success');
showPriceDiffModal([{ configName: configName || 'Конфигурация', prevTotal: beforeTotal, newTotal: afterTotal, serverCount, itemDiffs }]);
} catch(e) {
showToast('Ошибка обновления цен', 'error');
} finally {