feat(reports): stabilize on-demand generation, live presence and 1C fallback
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* ===============================================================================
|
||||
* FILE: modules/web_api/static/js/presence.js
|
||||
* ROLE: Контроллер экрана оперативного мониторинга («Кто в здании прямо сейчас»).
|
||||
* ===============================================================================
|
||||
*/
|
||||
|
||||
window.PresenceManager = {
|
||||
records: [],
|
||||
activeTab: 'ALL',
|
||||
|
||||
open() {
|
||||
const modal = document.getElementById('presence-modal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
this.bindEventsOnce();
|
||||
this.loadData(false);
|
||||
}
|
||||
},
|
||||
|
||||
close() {
|
||||
const modal = document.getElementById('presence-modal');
|
||||
if (modal) modal.classList.add('hidden');
|
||||
},
|
||||
|
||||
bindEventsOnce() {
|
||||
if (this._eventsBound) return;
|
||||
this._eventsBound = true;
|
||||
|
||||
document.getElementById('btn-close-presence-modal')?.addEventListener('click', () => this.close());
|
||||
document.getElementById('btn-presence-force-refresh')?.addEventListener('click', () => this.loadData(true));
|
||||
|
||||
document.querySelectorAll('.presence-tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
document.querySelectorAll('.presence-tab-btn').forEach(b => {
|
||||
b.classList.remove('bg-white', 'shadow-xs', 'text-slate-800');
|
||||
b.classList.add('text-slate-600');
|
||||
});
|
||||
const target = e.currentTarget;
|
||||
target.classList.add('bg-white', 'shadow-xs', 'text-slate-800');
|
||||
target.classList.remove('text-slate-600');
|
||||
this.activeTab = target.getAttribute('data-tab') || 'ALL';
|
||||
this.applyFilters();
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('presence-search-input')?.addEventListener('input', () => this.applyFilters());
|
||||
document.getElementById('presence-dept-filter')?.addEventListener('change', () => this.applyFilters());
|
||||
},
|
||||
|
||||
async loadData(forceRefresh = false) {
|
||||
const subtitle = document.getElementById('presence-modal-subtitle');
|
||||
const btnRefresh = document.getElementById('btn-presence-force-refresh');
|
||||
|
||||
if (btnRefresh && forceRefresh) {
|
||||
btnRefresh.disabled = true;
|
||||
btnRefresh.innerHTML = '<span>⏳</span><span>Опрос СКУД...</span>';
|
||||
}
|
||||
|
||||
try {
|
||||
const url = `/api/v1/presence/live?force_refresh=${forceRefresh}`;
|
||||
const res = await fetch(url, { headers: AuthManager.getAuthHeaders() });
|
||||
if (!res.ok) throw new Error('Ошибка ответа сервера');
|
||||
const data = await res.json();
|
||||
this.render(data);
|
||||
} catch (err) {
|
||||
console.error('[Presence] Ошибка загрузки:', err);
|
||||
if (subtitle) subtitle.textContent = 'Ошибка загрузки данных присутствия';
|
||||
} finally {
|
||||
if (btnRefresh) {
|
||||
btnRefresh.disabled = false;
|
||||
btnRefresh.innerHTML = '<span>🔄</span><span>Запросить из СКУД</span>';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render(data) {
|
||||
this.records = data.records || [];
|
||||
const m = data.metrics || {};
|
||||
|
||||
// Заголовок
|
||||
const subtitle = document.getElementById('presence-modal-subtitle');
|
||||
const sourceLabel = data.data_source === 'LIVE_MSSQL' ? '🟢 Онлайн срез Орион' : '⏱️ Локальная база СКУД';
|
||||
if (subtitle) {
|
||||
subtitle.textContent = `${sourceLabel} на ${data.latest_event_time || data.timestamp} | Всего в штате 1С: ${m.total_staff || 0} чел.`;
|
||||
}
|
||||
|
||||
// Плашки метрик (строго сбалансированы)
|
||||
document.getElementById('metric-total').textContent = m.total_staff || 0;
|
||||
document.getElementById('metric-inside').textContent = m.inside || 0;
|
||||
document.getElementById('metric-outside').textContent = m.outside || 0;
|
||||
document.getElementById('metric-remote').textContent = m.remote || 0;
|
||||
document.getElementById('metric-absence').textContent = m.official_absence || 0;
|
||||
document.getElementById('metric-not-entered').textContent = m.not_entered || 0;
|
||||
document.getElementById('metric-excluded').textContent = m.excluded || 0;
|
||||
|
||||
// Счетчики на табах
|
||||
document.getElementById('tab-cnt-all').textContent = m.total_staff || 0;
|
||||
document.getElementById('tab-cnt-inside').textContent = m.inside || 0;
|
||||
document.getElementById('tab-cnt-outside').textContent = m.outside || 0;
|
||||
document.getElementById('tab-cnt-remote').textContent = m.remote || 0;
|
||||
document.getElementById('tab-cnt-absence').textContent = m.official_absence || 0;
|
||||
document.getElementById('tab-cnt-not-entered').textContent = m.not_entered || 0;
|
||||
document.getElementById('tab-cnt-excluded').textContent = m.excluded || 0;
|
||||
|
||||
// Выпадающий список подразделений
|
||||
const deptSelect = document.getElementById('presence-dept-filter');
|
||||
if (deptSelect) {
|
||||
const depts = Array.from(new Set(this.records.map(r => r.department).filter(Boolean))).sort();
|
||||
deptSelect.innerHTML = '<option value="ALL">Все подразделения</option>' +
|
||||
depts.map(d => `<option value="${escapeHtml(d)}">${escapeHtml(d)}</option>`).join('');
|
||||
}
|
||||
|
||||
this.applyFilters();
|
||||
},
|
||||
|
||||
applyFilters() {
|
||||
const search = (document.getElementById('presence-search-input')?.value || '').toLowerCase().trim();
|
||||
const dept = document.getElementById('presence-dept-filter')?.value || 'ALL';
|
||||
|
||||
const filtered = this.records.filter(r => {
|
||||
if (this.activeTab === 'INSIDE' && r.status !== 'INSIDE') return false;
|
||||
if (this.activeTab === 'OUTSIDE' && r.status !== 'OUTSIDE') return false;
|
||||
if (this.activeTab === 'REMOTE' && r.status !== 'REMOTE') return false;
|
||||
if (this.activeTab === 'ABSENCE' && r.status !== 'OFFICIAL_ABSENCE') return false;
|
||||
if (this.activeTab === 'NOT_ENTERED' && r.status !== 'NOT_ENTERED') return false;
|
||||
if (this.activeTab === 'EXCLUDED' && r.status !== 'EXCLUDED') return false;
|
||||
|
||||
if (dept !== 'ALL' && r.department !== dept) return false;
|
||||
if (search && !r.fio.toLowerCase().includes(search) && !r.position.toLowerCase().includes(search)) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const tbody = document.getElementById('presence-table-body');
|
||||
if (!tbody) return;
|
||||
|
||||
if (!filtered.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="py-10 text-center text-slate-400">Сотрудники не найдены</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = filtered.map(r => {
|
||||
let badgeClass = 'bg-slate-100 text-slate-700';
|
||||
if (r.status === 'INSIDE') {
|
||||
badgeClass = r.is_fligel
|
||||
? 'bg-emerald-100 text-emerald-900 border border-emerald-300 font-bold'
|
||||
: 'bg-emerald-100 text-emerald-800 font-bold';
|
||||
} else if (r.status === 'OUTSIDE') {
|
||||
badgeClass = 'bg-amber-100 text-amber-800 font-bold';
|
||||
} else if (r.status === 'REMOTE') {
|
||||
badgeClass = 'bg-blue-100 text-blue-800 font-semibold';
|
||||
} else if (r.status === 'OFFICIAL_ABSENCE') {
|
||||
badgeClass = 'bg-purple-100 text-purple-800 font-medium';
|
||||
} else if (r.status === 'NOT_ENTERED') {
|
||||
badgeClass = 'bg-rose-100 text-rose-800 font-semibold';
|
||||
} else if (r.status === 'EXCLUDED') {
|
||||
badgeClass = 'bg-slate-200 text-slate-600 font-mono text-[10px]';
|
||||
}
|
||||
|
||||
return `
|
||||
<tr class="hover:bg-slate-50 transition-colors">
|
||||
<td class="py-2.5 px-3 font-medium text-slate-800">${escapeHtml(r.fio)}</td>
|
||||
<td class="py-2.5 px-3 text-slate-600 font-mono text-[11px]">${escapeHtml(r.department)}</td>
|
||||
<td class="py-2.5 px-3 text-slate-500">${escapeHtml(r.position)}</td>
|
||||
<td class="py-2.5 px-3 text-center">
|
||||
<span class="px-2 py-0.5 rounded-md text-[11px] ${badgeClass}">${escapeHtml(r.status_label)}</span>
|
||||
</td>
|
||||
<td class="py-2.5 px-3 text-center text-slate-600 font-mono font-semibold">${escapeHtml(r.last_time || '—')}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
};
|
||||
|
||||
window.openPresenceModal = () => window.PresenceManager.open();
|
||||
Reference in New Issue
Block a user