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
This commit is contained in:
Mikhail Chusavitin
2026-02-02 11:18:24 +03:00
parent 9bd2acd4f7
commit e206531364
3 changed files with 159 additions and 28 deletions

View File

@@ -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);
</script>
</body>
</html>