From 6b56cad2488aa27f54f571eabd28085c69ec3939 Mon Sep 17 00:00:00 2001 From: Michael Chus Date: Sun, 24 May 2026 18:46:01 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D0=B0=20?= =?UTF-8?q?=C2=AB=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D1=82=D1=8C=20=D1=86?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=C2=BB=20=D0=BD=D0=B0=20=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=86=D0=B5=20=D0=B2=D0=B0=D1=80=D0=B8=D0=B0?= =?UTF-8?q?=D0=BD=D1=82=D0=B0=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлена кнопка «Обновить цены» в панель действий страницы варианта. При нажатии синхронизирует прайс-листы с сервером (если онлайн), затем последовательно вызывает POST /api/configs/:uuid/refresh-prices для каждой активной конфигурации — тот же механизм, что и кнопка внутри конфигурации. Цены и итоговая сумма обновляются в таблице без перезагрузки страницы. Co-Authored-By: Claude Sonnet 4.6 --- web/templates/project_detail.html | 82 +++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/web/templates/project_detail.html b/web/templates/project_detail.html index 95bb6fb..3a5801a 100644 --- a/web/templates/project_detail.html +++ b/web/templates/project_detail.html @@ -40,6 +40,9 @@ + @@ -1569,6 +1572,85 @@ async function exportProject() { } } +async function refreshAllPrices() { + const configs = (allConfigs || []).filter(c => c.is_active !== false); + if (!configs.length) { + if (typeof showToast === 'function') showToast('Нет активных конфигураций', 'error'); + return; + } + + const btn = document.getElementById('refresh-all-prices-btn'); + if (btn) { + btn.disabled = true; + btn.textContent = 'Обновление...'; + 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; + } + + let failed = 0; + let newTotalSum = 0; + for (const cfg of configs) { + try { + const resp = await fetch('/api/configs/' + cfg.uuid + '/refresh-prices', { method: 'POST' }); + 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); + } + } + 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'); + } +} + document.addEventListener('DOMContentLoaded', async function() { applyStatusModeUI(); const ok = await loadProject();