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();