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:
@@ -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>
|
||||
|
||||
@@ -10,6 +10,15 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="pricelist-badge" class="mt-4 text-sm text-gray-600 hidden">
|
||||
<span class="bg-blue-100 text-blue-800 px-2 py-1 rounded-full">
|
||||
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||
</svg>
|
||||
Активный прайслист: <span id="pricelist-version">-</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div id="configs-list">
|
||||
<div class="text-center py-8 text-gray-500">Загрузка...</div>
|
||||
</div>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
|
||||
@@ -1,37 +1,58 @@
|
||||
{{define "sync_status"}}
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-2 relative">
|
||||
{{if .IsOffline}}
|
||||
<span class="flex items-center gap-1 text-red-600" title="Offline">
|
||||
<span class="w-2 h-2 bg-red-500 rounded-full"></span>
|
||||
<span class="text-xs">Offline</span>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
{{else}}
|
||||
<span class="flex items-center gap-1 text-green-600" title="Online">
|
||||
<span class="w-2 h-2 bg-green-500 rounded-full"></span>
|
||||
<span class="text-xs">Online</span>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
{{end}}
|
||||
|
||||
{{if gt .PendingCount 0}}
|
||||
<span class="bg-yellow-100 text-yellow-800 px-2 py-0.5 rounded-full text-xs font-medium">
|
||||
{{.PendingCount}} pending
|
||||
<span class="bg-yellow-100 text-yellow-800 px-2 py-0.5 rounded-full text-xs font-medium flex items-center gap-1">
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
||||
</svg>
|
||||
{{.PendingCount}}
|
||||
</span>
|
||||
<button hx-post="/api/sync/push"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="
|
||||
if(event.detail.successful) {
|
||||
const resp = JSON.parse(event.detail.xhr.response);
|
||||
if(resp.success) {
|
||||
showToast('Синхронизировано: ' + resp.synced + ' изменений', 'success');
|
||||
} else {
|
||||
showToast('Ошибка: ' + (resp.error || 'неизвестная ошибка'), 'error');
|
||||
}
|
||||
htmx.trigger('#sync-status', 'refresh');
|
||||
}
|
||||
"
|
||||
class="text-blue-600 hover:text-blue-800 text-xs underline cursor-pointer">
|
||||
Sync
|
||||
</button>
|
||||
{{end}}
|
||||
|
||||
<!-- Dropdown button for sync actions -->
|
||||
<div class="relative">
|
||||
<button id="sync-dropdown-button" class="text-gray-600 hover:text-gray-800 text-xs flex items-center gap-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Dropdown menu -->
|
||||
<div id="sync-dropdown-menu" class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 hidden z-10">
|
||||
<button onclick="pushPendingChanges()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 w-full text-left">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
Push changes
|
||||
</button>
|
||||
<button onclick="fullSync()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 w-full text-left">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
Full sync
|
||||
</button>
|
||||
<div class="border-t border-gray-100 my-1"></div>
|
||||
<div class="px-4 py-2 text-xs text-gray-500">
|
||||
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
Последняя синхронизация: <span id="last-sync-time">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user