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:
@@ -40,7 +40,7 @@
|
||||
<button onclick="openCreateModal()" class="py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium">
|
||||
+ Конфигурация
|
||||
</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 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);
|
||||
if (!configs.length) {
|
||||
if (typeof showToast === 'function') showToast('Нет активных конфигураций', 'error');
|
||||
showToast('Нет активных конфигураций', 'error');
|
||||
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';
|
||||
}
|
||||
|
||||
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') {
|
||||
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;
|
||||
}
|
||||
const latestEstimatePricelistId = await fetchLatestEstimatePricelistId();
|
||||
|
||||
// Resolve latest estimate pricelist ID to pass explicitly, so each config
|
||||
// is updated to the newest pricelist rather than the one stored in the config.
|
||||
let latestEstimatePricelistId = null;
|
||||
try {
|
||||
const plResp = await fetch('/api/pricelists?active_only=true&source=estimate&per_page=1');
|
||||
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);
|
||||
const diffResults = [];
|
||||
for (const cfg of configs) {
|
||||
if (cfg.disable_price_refresh) {
|
||||
diffResults.push({ configName: cfg.name, skipped: true });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
let failed = 0;
|
||||
let newTotalSum = 0;
|
||||
for (const cfg of configs) {
|
||||
try {
|
||||
const body = latestEstimatePricelistId ? JSON.stringify({ pricelist_id: latestEstimatePricelistId }) : undefined;
|
||||
const resp = await fetch('/api/configs/' + cfg.uuid + '/refresh-prices', {
|
||||
method: 'POST',
|
||||
headers: body ? { 'Content-Type': 'application/json' } : {},
|
||||
body,
|
||||
});
|
||||
if (!resp.ok) { failed++; continue; }
|
||||
const updated = await resp.json();
|
||||
if (updated && updated.total_price != null) {
|
||||
cfg.total_price = updated.total_price;
|
||||
const totalCell = document.querySelector('[data-total-uuid="' + cfg.uuid + '"]');
|
||||
if (totalCell) totalCell.textContent = formatMoneyNoDecimals(updated.total_price);
|
||||
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 prevTotal = cfg.total_price || 0;
|
||||
const prevItemsMap = {};
|
||||
if (cfg.items) {
|
||||
for (const item of cfg.items) prevItemsMap[item.lot_name] = item.unit_price;
|
||||
}
|
||||
|
||||
try {
|
||||
const body = latestEstimatePricelistId ? JSON.stringify({ pricelist_id: latestEstimatePricelistId }) : undefined;
|
||||
const resp = await fetch('/api/configs/' + cfg.uuid + '/refresh-prices', {
|
||||
method: 'POST',
|
||||
headers: body ? { 'Content-Type': 'application/json' } : {},
|
||||
body,
|
||||
});
|
||||
if (!resp.ok) {
|
||||
diffResults.push({ configName: cfg.name, error: true });
|
||||
continue;
|
||||
}
|
||||
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"]');
|
||||
if (footerTotal) footerTotal.textContent = formatMoneyNoDecimals(newTotalSum);
|
||||
|
||||
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 (failed > 0) {
|
||||
if (typeof showToast === 'function') showToast('Часть конфигураций не обновилась (' + failed + ')', 'error');
|
||||
} else {
|
||||
const msg = serverSyncSkipped ? 'Цены обновлены (без связи с сервером)' : 'Цены обновлены';
|
||||
if (typeof showToast === 'function') showToast(msg, 'success');
|
||||
updateFooterTotal();
|
||||
showToast('Цены обновлены', 'success');
|
||||
showPriceDiffModal(diffResults);
|
||||
} catch(e) {
|
||||
showToast('Ошибка обновления цен', 'error');
|
||||
} finally {
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Обновить цены';
|
||||
btn.className = 'py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user