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>
|
||||||
</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 -->
|
<!-- 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 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">
|
<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
|
// Call functions immediately to ensure they run even before DOMContentLoaded
|
||||||
// This ensures username and admin link are visible ASAP
|
// This ensures username and admin link are visible ASAP
|
||||||
loadDBUser();
|
loadDBUser();
|
||||||
|
|||||||
@@ -2817,7 +2817,6 @@ async function exportCSVWithCustomPrice() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function refreshPrices() {
|
async function refreshPrices() {
|
||||||
// RBAC disabled - no token check required
|
|
||||||
if (!configUUID) return;
|
if (!configUUID) return;
|
||||||
if (disablePriceRefresh) {
|
if (disablePriceRefresh) {
|
||||||
showToast('Обновление цен отключено в настройках', 'error');
|
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';
|
refreshBtn.className = 'px-4 py-2 bg-gray-300 text-gray-500 rounded cursor-not-allowed';
|
||||||
}
|
}
|
||||||
|
|
||||||
let serverSyncSkipped = false;
|
await loadActivePricelists(true);
|
||||||
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()
|
|
||||||
]);
|
|
||||||
|
|
||||||
['estimate', 'warehouse', 'competitor'].forEach(source => {
|
['estimate', 'warehouse', 'competitor'].forEach(source => {
|
||||||
const latest = activePricelistsBySource[source]?.[0];
|
const latest = activePricelistsBySource[source]?.[0];
|
||||||
@@ -2871,22 +2847,44 @@ async function refreshPrices() {
|
|||||||
renderPricelistSettingsSummary();
|
renderPricelistSettingsSummary();
|
||||||
persistLocalPriceSettings();
|
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 saveConfig(false);
|
||||||
await refreshPriceLevels({ force: true, noCache: true });
|
await refreshPriceLevels({ force: true, noCache: true });
|
||||||
renderTab();
|
renderTab();
|
||||||
updateCartUI();
|
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) {
|
if (configUUID) {
|
||||||
const configResp = await fetch('/api/configs/' + configUUID);
|
const configResp = await fetch('/api/configs/' + configUUID);
|
||||||
if (configResp.ok) {
|
if (configResp.ok) {
|
||||||
const config = await configResp.json();
|
const config = await configResp.json();
|
||||||
if (config.price_updated_at) {
|
if (config.price_updated_at) updatePriceUpdateDate(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) {
|
} catch(e) {
|
||||||
showToast('Ошибка обновления цен', 'error');
|
showToast('Ошибка обновления цен', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
<button onclick="openCreateModal()" class="py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium">
|
<button onclick="openCreateModal()" class="py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium">
|
||||||
+ Конфигурация
|
+ Конфигурация
|
||||||
</button>
|
</button>
|
||||||
<button id="refresh-all-prices-btn" onclick="refreshAllPrices()" class="py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium">
|
<button id="refresh-all-prices-btn" onclick="refreshPrices()" class="py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium">
|
||||||
Обновить цены
|
Обновить цены
|
||||||
</button>
|
</button>
|
||||||
<button onclick="openVendorImportModal()" class="py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700 font-medium">
|
<button onclick="openVendorImportModal()" class="py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700 font-medium">
|
||||||
@@ -1572,10 +1572,10 @@ async function exportProject() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshAllPrices() {
|
async function refreshPrices() {
|
||||||
const configs = (allConfigs || []).filter(c => c.is_active !== false);
|
const configs = (allConfigs || []).filter(c => c.is_active !== false);
|
||||||
if (!configs.length) {
|
if (!configs.length) {
|
||||||
if (typeof showToast === 'function') showToast('Нет активных конфигураций', 'error');
|
showToast('Нет активных конфигураций', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1586,87 +1586,76 @@ async function refreshAllPrices() {
|
|||||||
btn.className = 'py-2 bg-gray-300 text-gray-500 rounded-lg cursor-not-allowed font-medium';
|
btn.className = 'py-2 bg-gray-300 text-gray-500 rounded-lg cursor-not-allowed font-medium';
|
||||||
}
|
}
|
||||||
|
|
||||||
let serverSyncSkipped = false;
|
|
||||||
try {
|
try {
|
||||||
const statusResp = await fetch('/api/sync/status');
|
const latestEstimatePricelistId = await fetchLatestEstimatePricelistId();
|
||||||
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') {
|
|
||||||
if (btn) {
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = 'Обновить цены';
|
|
||||||
btn.className = 'py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium';
|
|
||||||
}
|
|
||||||
if (typeof showToast === 'function') showToast('Ошибка синхронизации прайс-листов', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
serverSyncSkipped = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve latest estimate pricelist ID to pass explicitly, so each config
|
const diffResults = [];
|
||||||
// is updated to the newest pricelist rather than the one stored in the config.
|
for (const cfg of configs) {
|
||||||
let latestEstimatePricelistId = null;
|
if (cfg.disable_price_refresh) {
|
||||||
try {
|
diffResults.push({ configName: cfg.name, skipped: true });
|
||||||
const plResp = await fetch('/api/pricelists?active_only=true&source=estimate&per_page=1');
|
continue;
|
||||||
if (plResp.ok) {
|
|
||||||
const plData = await plResp.json();
|
|
||||||
const list = plData.pricelists || plData.items || plData;
|
|
||||||
if (Array.isArray(list) && list.length > 0 && list[0].id) {
|
|
||||||
latestEstimatePricelistId = Number(list[0].id);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (_) {}
|
|
||||||
|
|
||||||
let failed = 0;
|
const prevTotal = cfg.total_price || 0;
|
||||||
let newTotalSum = 0;
|
const prevItemsMap = {};
|
||||||
for (const cfg of configs) {
|
if (cfg.items) {
|
||||||
try {
|
for (const item of cfg.items) prevItemsMap[item.lot_name] = item.unit_price;
|
||||||
const body = latestEstimatePricelistId ? JSON.stringify({ pricelist_id: latestEstimatePricelistId }) : undefined;
|
}
|
||||||
const resp = await fetch('/api/configs/' + cfg.uuid + '/refresh-prices', {
|
|
||||||
method: 'POST',
|
try {
|
||||||
headers: body ? { 'Content-Type': 'application/json' } : {},
|
const body = latestEstimatePricelistId ? JSON.stringify({ pricelist_id: latestEstimatePricelistId }) : undefined;
|
||||||
body,
|
const resp = await fetch('/api/configs/' + cfg.uuid + '/refresh-prices', {
|
||||||
});
|
method: 'POST',
|
||||||
if (!resp.ok) { failed++; continue; }
|
headers: body ? { 'Content-Type': 'application/json' } : {},
|
||||||
const updated = await resp.json();
|
body,
|
||||||
if (updated && updated.total_price != null) {
|
});
|
||||||
cfg.total_price = updated.total_price;
|
if (!resp.ok) {
|
||||||
const totalCell = document.querySelector('[data-total-uuid="' + cfg.uuid + '"]');
|
diffResults.push({ configName: cfg.name, error: true });
|
||||||
if (totalCell) totalCell.textContent = formatMoneyNoDecimals(updated.total_price);
|
continue;
|
||||||
const serverCount = cfg.server_count || 1;
|
|
||||||
const unitPrice = serverCount > 0 ? (updated.total_price / serverCount) : 0;
|
|
||||||
const row = totalCell && totalCell.closest('tr');
|
|
||||||
if (row) {
|
|
||||||
const cells = row.querySelectorAll('td');
|
|
||||||
if (cells[4]) cells[4].textContent = formatMoneyNoDecimals(unitPrice);
|
|
||||||
}
|
}
|
||||||
|
const updated = await resp.json();
|
||||||
|
|
||||||
|
cfg.total_price = updated.total_price ?? cfg.total_price;
|
||||||
|
if (updated.items) cfg.items = updated.items;
|
||||||
|
|
||||||
|
const totalCell = document.querySelector('[data-total-uuid="' + cfg.uuid + '"]');
|
||||||
|
if (totalCell && updated.total_price != null) {
|
||||||
|
totalCell.textContent = formatMoneyNoDecimals(updated.total_price);
|
||||||
|
const sc = cfg.server_count || 1;
|
||||||
|
const row = totalCell.closest('tr');
|
||||||
|
if (row) {
|
||||||
|
const cells = row.querySelectorAll('td');
|
||||||
|
if (cells[4]) cells[4].textContent = formatMoneyNoDecimals(sc > 0 ? updated.total_price / sc : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemDiffs = [];
|
||||||
|
if (updated.items) {
|
||||||
|
for (const item of updated.items) {
|
||||||
|
const prevPrice = prevItemsMap[item.lot_name];
|
||||||
|
if (prevPrice !== undefined && Math.abs(prevPrice - item.unit_price) > 0.01) {
|
||||||
|
itemDiffs.push({ lot_name: item.lot_name, quantity: item.quantity, prevPrice, newPrice: item.unit_price });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diffResults.push({ configName: cfg.name, prevTotal, newTotal: updated.total_price || 0, serverCount: cfg.server_count || 1, itemDiffs });
|
||||||
|
} catch(_) {
|
||||||
|
diffResults.push({ configName: cfg.name, error: true });
|
||||||
}
|
}
|
||||||
newTotalSum += cfg.total_price || 0;
|
}
|
||||||
} catch { failed++; }
|
|
||||||
}
|
|
||||||
|
|
||||||
const footerTotal = document.querySelector('[data-footer-total="1"]');
|
updateFooterTotal();
|
||||||
if (footerTotal) footerTotal.textContent = formatMoneyNoDecimals(newTotalSum);
|
showToast('Цены обновлены', 'success');
|
||||||
|
showPriceDiffModal(diffResults);
|
||||||
if (btn) {
|
} catch(e) {
|
||||||
btn.disabled = false;
|
showToast('Ошибка обновления цен', 'error');
|
||||||
btn.textContent = 'Обновить цены';
|
} finally {
|
||||||
btn.className = 'py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium';
|
if (btn) {
|
||||||
}
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Обновить цены';
|
||||||
if (failed > 0) {
|
btn.className = 'py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium';
|
||||||
if (typeof showToast === 'function') showToast('Часть конфигураций не обновилась (' + failed + ')', 'error');
|
}
|
||||||
} else {
|
|
||||||
const msg = serverSyncSkipped ? 'Цены обновлены (без связи с сервером)' : 'Цены обновлены';
|
|
||||||
if (typeof showToast === 'function') showToast(msg, 'success');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user