/** * =============================================================================== * 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 = '⏳Опрос СКУД...'; } 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 = '🔄Запросить из СКУД'; } } }, 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 = '' + depts.map(d => ``).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 = '