From e2065313643a0ef66b03516c7465340dae908b94 Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Mon, 2 Feb 2026 11:18:24 +0300 Subject: [PATCH] feat: implement sync icon + pricelist badge UI improvements - Replace text 'Online/Offline' with SVG icons in sync status - Change sync button to circular arrow icon - Add dropdown menu with push changes, full sync, and last sync status - Add pricelist version badge to configuration page - Load pricelist version via /api/pricelists/latest on DOMContentLoaded This completes task 1 of Phase 2.5 (UI Improvements) as specified in CLAUDE.md --- web/templates/base.html | 91 +++++++++++++++++++++++-- web/templates/configs.html | 29 +++++++- web/templates/partials/sync_status.html | 67 +++++++++++------- 3 files changed, 159 insertions(+), 28 deletions(-) diff --git a/web/templates/base.html b/web/templates/base.html index e96b823..498d8da 100644 --- a/web/templates/base.html +++ b/web/templates/base.html @@ -60,6 +60,75 @@ setTimeout(() => el.innerHTML = '', 3000); } + // Dropdown functionality + document.addEventListener('DOMContentLoaded', function() { + const dropdownButton = document.getElementById('sync-dropdown-button'); + const dropdownMenu = document.getElementById('sync-dropdown-menu'); + + if (dropdownButton && dropdownMenu) { + dropdownButton.addEventListener('click', function(e) { + e.stopPropagation(); + dropdownMenu.classList.toggle('hidden'); + }); + + // Close dropdown when clicking outside + document.addEventListener('click', function(e) { + if (!dropdownButton.contains(e.target) && !dropdownMenu.contains(e.target)) { + dropdownMenu.classList.add('hidden'); + } + }); + } + + checkDbStatus(); + checkWritePermission(); + }); + + function pushPendingChanges() { + fetch('/api/sync/push', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast('Синхронизировано: ' + data.synced + ' изменений', 'success'); + } else { + showToast('Ошибка: ' + (data.error || 'неизвестная ошибка'), 'error'); + } + htmx.trigger('#sync-status', 'refresh'); + document.getElementById('sync-dropdown-menu').classList.add('hidden'); + }) + .catch(error => { + showToast('Ошибка синхронизации: ' + error.message, 'error'); + document.getElementById('sync-dropdown-menu').classList.add('hidden'); + }); + } + + function fullSync() { + fetch('/api/sync/all', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast('Полная синхронизация завершена', 'success'); + } else { + showToast('Ошибка: ' + (data.error || 'неизвестная ошибка'), 'error'); + } + htmx.trigger('#sync-status', 'refresh'); + document.getElementById('sync-dropdown-menu').classList.add('hidden'); + }) + .catch(error => { + showToast('Ошибка полной синхронизации: ' + error.message, 'error'); + document.getElementById('sync-dropdown-menu').classList.add('hidden'); + }); + } + async function checkDbStatus() { try { const resp = await fetch('/api/db-status'); @@ -96,10 +165,24 @@ } } - document.addEventListener('DOMContentLoaded', function() { - checkDbStatus(); - checkWritePermission(); - }); + // Load last sync time for dropdown + async function loadLastSyncTime() { + try { + const resp = await fetch('/api/sync/status'); + const data = await resp.json(); + if (data.last_pricelist_sync) { + const date = new Date(data.last_pricelist_sync); + document.getElementById('last-sync-time').textContent = date.toLocaleString('ru-RU'); + } else { + document.getElementById('last-sync-time').textContent = 'Нет данных'; + } + } catch(e) { + console.error('Failed to load last sync time:', e); + } + } + + // Load last sync time when page loads + document.addEventListener('DOMContentLoaded', loadLastSyncTime); diff --git a/web/templates/configs.html b/web/templates/configs.html index 664b552..b81a5d6 100644 --- a/web/templates/configs.html +++ b/web/templates/configs.html @@ -10,6 +10,15 @@ + +
Загрузка...
@@ -398,7 +407,25 @@ async function loadConfigs() { } } -document.addEventListener('DOMContentLoaded', loadConfigs); +document.addEventListener('DOMContentLoaded', function() { + loadConfigs(); + + // Load latest pricelist version for badge + loadLatestPricelistVersion(); +}); + +async function loadLatestPricelistVersion() { + try { + const resp = await fetch('/api/pricelists/latest'); + if (resp.ok) { + const pricelist = await resp.json(); + document.getElementById('pricelist-version').textContent = pricelist.version; + document.getElementById('pricelist-badge').classList.remove('hidden'); + } + } catch(e) { + console.error('Failed to load pricelist version:', e); + } +} {{end}} diff --git a/web/templates/partials/sync_status.html b/web/templates/partials/sync_status.html index 219c241..2879697 100644 --- a/web/templates/partials/sync_status.html +++ b/web/templates/partials/sync_status.html @@ -1,37 +1,58 @@ {{define "sync_status"}} -
+
{{if .IsOffline}} - - Offline + + + {{else}} - - Online + + + {{end}} {{if gt .PendingCount 0}} - - {{.PendingCount}} pending + + + + + {{.PendingCount}} - {{end}} + + +
+ + + + +
{{end}}