Add projects table controls and sync status tab with app version
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
<button onclick="loadTab('alerts')" id="btn-alerts" class="text-blue-600 font-medium">Алерты</button>
|
||||
<button onclick="loadTab('components')" id="btn-components" class="text-gray-600">Компоненты</button>
|
||||
<button onclick="loadTab('pricelists')" id="btn-pricelists" class="text-gray-600">Прайслисты</button>
|
||||
<button onclick="loadTab('sync-status')" id="btn-sync-status" class="text-gray-600 hidden">Статус синхронизации</button>
|
||||
<button onclick="loadTab('all-configs')" id="btn-all-configs" class="text-gray-600 hidden">Все конфигурации</button>
|
||||
</div>
|
||||
<button onclick="recalculateAll()" id="btn-recalc" class="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700">
|
||||
@@ -85,6 +86,30 @@
|
||||
<div id="pricelists-pagination" class="flex justify-center space-x-2 mt-4"></div>
|
||||
</div>
|
||||
|
||||
<!-- Sync Status Tab Content (hidden by default) -->
|
||||
<div id="sync-status-tab-content" class="hidden">
|
||||
<div class="mb-4">
|
||||
<h2 class="text-xl font-semibold">Статус синхронизации</h2>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Пользователь</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Версия приложения</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sync-users-status-body" class="bg-white divide-y divide-gray-200">
|
||||
<tr>
|
||||
<td colspan="3" class="px-6 py-4 text-sm text-gray-500">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Modal -->
|
||||
<div id="pricelists-create-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg p-6 max-w-md w-full mx-4">
|
||||
@@ -226,16 +251,21 @@ let pricelistsPage = 1;
|
||||
let pricelistsCanWrite = false;
|
||||
let isCreatingPricelist = false;
|
||||
let cachedDbUsername = null;
|
||||
let syncUsersStatusTimer = null;
|
||||
|
||||
async function loadTab(tab) {
|
||||
currentTab = tab;
|
||||
currentPage = 1;
|
||||
currentSearch = '';
|
||||
document.getElementById('search-input').value = '';
|
||||
if (tab !== 'sync-status') {
|
||||
stopSyncUsersStatusRefresh();
|
||||
}
|
||||
|
||||
document.getElementById('btn-alerts').className = tab === 'alerts' ? 'text-blue-600 font-medium' : 'text-gray-600';
|
||||
document.getElementById('btn-components').className = tab === 'components' ? 'text-blue-600 font-medium' : 'text-gray-600';
|
||||
document.getElementById('btn-pricelists').className = tab === 'pricelists' ? 'text-blue-600 font-medium' : 'text-gray-600';
|
||||
document.getElementById('btn-sync-status').className = (tab === 'sync-status' ? 'text-blue-600 font-medium' : 'text-gray-600') + (pricelistsCanWrite ? '' : ' hidden');
|
||||
document.getElementById('btn-all-configs').className = tab === 'all-configs' ? 'text-blue-600 font-medium' : 'text-gray-600 hidden';
|
||||
|
||||
// Show/hide elements based on tab
|
||||
@@ -244,35 +274,69 @@ async function loadTab(tab) {
|
||||
document.getElementById('pagination').className = 'flex justify-between items-center mt-4 pt-4 border-t';
|
||||
document.getElementById('btn-all-configs').className = 'text-gray-600 hidden'; // Hide this tab for components
|
||||
document.getElementById('pricelists-tab-content').className = 'hidden';
|
||||
document.getElementById('sync-status-tab-content').className = 'hidden';
|
||||
document.getElementById('tab-content').className = '';
|
||||
} else if (tab === 'all-configs') {
|
||||
document.getElementById('search-bar').className = 'mb-4 hidden'; // Hide search for all configs
|
||||
document.getElementById('pagination').className = 'flex justify-between items-center mt-4 pt-4 border-t'; // Show pagination
|
||||
document.getElementById('btn-all-configs').className = 'text-blue-600 font-medium'; // Show this tab for all configs
|
||||
document.getElementById('pricelists-tab-content').className = 'hidden';
|
||||
document.getElementById('sync-status-tab-content').className = 'hidden';
|
||||
document.getElementById('tab-content').className = '';
|
||||
} else if (tab === 'pricelists') {
|
||||
document.getElementById('search-bar').className = 'mb-4 hidden';
|
||||
document.getElementById('pagination').className = 'hidden';
|
||||
document.getElementById('btn-all-configs').className = 'text-gray-600 hidden';
|
||||
document.getElementById('pricelists-tab-content').className = '';
|
||||
document.getElementById('sync-status-tab-content').className = 'hidden';
|
||||
document.getElementById('tab-content').className = 'hidden';
|
||||
// Load pricelists when pricelists tab is selected
|
||||
checkPricelistWritePermission();
|
||||
loadPricelists(1);
|
||||
} else if (tab === 'sync-status') {
|
||||
document.getElementById('search-bar').className = 'mb-4 hidden';
|
||||
document.getElementById('pagination').className = 'hidden';
|
||||
document.getElementById('btn-all-configs').className = 'text-gray-600 hidden';
|
||||
document.getElementById('pricelists-tab-content').className = 'hidden';
|
||||
document.getElementById('sync-status-tab-content').className = '';
|
||||
document.getElementById('tab-content').className = 'hidden';
|
||||
await checkPricelistWritePermission();
|
||||
if (!pricelistsCanWrite) {
|
||||
await loadTab('alerts');
|
||||
return;
|
||||
}
|
||||
await loadUsersSyncStatus();
|
||||
startSyncUsersStatusRefresh();
|
||||
} else {
|
||||
document.getElementById('search-bar').className = 'mb-4 hidden';
|
||||
document.getElementById('pagination').className = 'hidden';
|
||||
document.getElementById('btn-all-configs').className = 'text-gray-600 hidden';
|
||||
document.getElementById('pricelists-tab-content').className = 'hidden';
|
||||
document.getElementById('sync-status-tab-content').className = 'hidden';
|
||||
document.getElementById('tab-content').className = '';
|
||||
}
|
||||
|
||||
if (tab !== 'pricelists') {
|
||||
if (tab !== 'pricelists' && tab !== 'sync-status') {
|
||||
await loadData();
|
||||
}
|
||||
}
|
||||
|
||||
function stopSyncUsersStatusRefresh() {
|
||||
if (syncUsersStatusTimer) {
|
||||
clearInterval(syncUsersStatusTimer);
|
||||
syncUsersStatusTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startSyncUsersStatusRefresh() {
|
||||
stopSyncUsersStatusRefresh();
|
||||
syncUsersStatusTimer = setInterval(() => {
|
||||
if (currentTab === 'sync-status' && pricelistsCanWrite) {
|
||||
loadUsersSyncStatus();
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
document.getElementById('tab-content').innerHTML = '<div class="text-center py-8 text-gray-500">Загрузка...</div>';
|
||||
|
||||
@@ -902,11 +966,12 @@ function renderAllConfigs(configs) {
|
||||
document.getElementById('tab-content').innerHTML = html;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await checkPricelistWritePermission();
|
||||
// Check URL params for initial tab
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const initialTab = urlParams.get('tab') || 'alerts';
|
||||
loadTab(initialTab);
|
||||
await loadTab(initialTab);
|
||||
|
||||
// Add event listeners for preview updates
|
||||
document.getElementById('modal-period').addEventListener('change', fetchPreview);
|
||||
@@ -930,9 +995,89 @@ async function checkPricelistWritePermission() {
|
||||
Создать прайслист
|
||||
</button>
|
||||
`;
|
||||
document.getElementById('btn-sync-status').classList.remove('hidden');
|
||||
if (currentTab === 'sync-status') {
|
||||
await loadUsersSyncStatus();
|
||||
startSyncUsersStatusRefresh();
|
||||
}
|
||||
} else {
|
||||
document.getElementById('pricelists-create-btn-container').innerHTML = '';
|
||||
document.getElementById('btn-sync-status').classList.add('hidden');
|
||||
stopSyncUsersStatusRefresh();
|
||||
if (currentTab === 'sync-status') {
|
||||
await loadTab('alerts');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to check pricelist write permission:', e);
|
||||
document.getElementById('btn-sync-status').classList.add('hidden');
|
||||
stopSyncUsersStatusRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
function formatRelativeTime(lastSyncAt) {
|
||||
const timestamp = new Date(lastSyncAt);
|
||||
if (Number.isNaN(timestamp.getTime())) return '—';
|
||||
const diffMinutes = Math.max(1, Math.floor((Date.now() - timestamp.getTime()) / 60000));
|
||||
if (diffMinutes < 60) return `${diffMinutes} мин назад`;
|
||||
const diffHours = Math.floor(diffMinutes / 60);
|
||||
if (diffHours < 24) return `${diffHours} ч назад`;
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
if (diffDays < 7) return `${diffDays} дн назад`;
|
||||
const diffWeeks = Math.floor(diffDays / 7);
|
||||
if (diffWeeks < 5) return `${diffWeeks} нед назад`;
|
||||
const diffMonths = Math.floor(diffDays / 30);
|
||||
if (diffMonths < 12) return `${diffMonths} мес назад`;
|
||||
const diffYears = Math.floor(diffDays / 365);
|
||||
return `${diffYears} г назад`;
|
||||
}
|
||||
|
||||
async function loadUsersSyncStatus() {
|
||||
if (!pricelistsCanWrite) return;
|
||||
|
||||
const body = document.getElementById('sync-users-status-body');
|
||||
if (!body) return;
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/sync/users-status');
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) {
|
||||
throw new Error(data.error || 'Ошибка загрузки');
|
||||
}
|
||||
|
||||
const users = data.users || [];
|
||||
if (users.length === 0) {
|
||||
body.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="3" class="px-6 py-4 text-sm text-gray-500">
|
||||
Нет данных о синхронизации пользователей
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
body.innerHTML = users.map(u => {
|
||||
const statusClass = u.is_online ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-700';
|
||||
const statusText = u.is_online ? 'онлайн' : formatRelativeTime(u.last_sync_at);
|
||||
return `
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-800">${escapeHtml(u.username || '—')}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">${escapeHtml(u.app_version || '—')}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||
<span class="px-2 py-1 text-xs rounded-full ${statusClass}">${statusText}</span>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
} catch (e) {
|
||||
body.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="3" class="px-6 py-4 text-sm text-red-600">
|
||||
Ошибка загрузки статусов синхронизации: ${escapeHtml(e.message || String(e))}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user