// KPI Alert Dashboard — vanilla JS, compiled by Vite into /static/assets/js/iris/kpi_dashboard.js import '../css/kpi_dashboard.css'; const API_BASE = '/kpi-dashboard/api'; const state = { page: 1, perPage: 20, sortBy: 'alert_id', sortDir: 'desc', filterTitle: '', filterOwner: '', total: 0, selected: new Set(), }; // Debounce helper for filter inputs let _debounceTimer = null; function debouncedLoad() { clearTimeout(_debounceTimer); _debounceTimer = setTimeout(() => { state.page = 1; loadAlerts(); }, 350); } async function loadAlerts() { state.filterTitle = document.getElementById('filter-title').value; state.filterOwner = document.getElementById('filter-owner').value; const params = new URLSearchParams({ page: state.page, per_page: state.perPage, sort_by: state.sortBy, sort_dir: state.sortDir, }); if (state.filterTitle) params.set('filter_title', state.filterTitle); if (state.filterOwner) params.set('filter_owner', state.filterOwner); const body = document.getElementById('alerts-body'); body.innerHTML = 'Loading...'; try { const res = await fetch(`${API_BASE}/alerts?${params}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const json = await res.json(); const alertsData = json.data?.alerts ?? {}; const alerts = alertsData.data ?? []; state.total = alertsData.total ?? alerts.length; renderTable(alerts); updatePageInfo(); updateSelectionLabel(); } catch (err) { body.innerHTML = `Error: ${err.message}`; } } function renderKpiBar(kpi) { if (!kpi) return ''; const containerW = 100; const fillW = Math.round((kpi.elapsed_pct / 100) * containerW); const fillColor = kpi.resolved ? '#6b7280' : kpi.elapsed_pct >= 75 ? '#ef4444' : kpi.elapsed_pct >= 50 ? '#f97316' : kpi.elapsed_pct >= 25 ? '#eab308' : '#22c55e'; const segs = (kpi.segments || []).map(s => `
` ).join(''); return `
${segs}
${kpi.status}
`; } function severityBadge(severity) { if (!severity) return ''; const name = (severity.severity_name || '').toLowerCase(); const cls = name === 'high' ? 'badge-high' : name === 'medium' ? 'badge-medium' : 'badge-low'; return `${severity.severity_name || ''}`; } function statusDot(status) { if (!status) return ''; const name = (status.status_name || '').toLowerCase(); const cls = name === 'closed' ? 'dot-closed' : 'dot-open'; return ``; } function fmtDate(str) { if (!str) return '—'; try { return new Date(str).toLocaleString(); } catch { return str; } } function renderTable(alerts) { const body = document.getElementById('alerts-body'); if (!alerts.length) { body.innerHTML = 'No alerts found.'; return; } body.innerHTML = alerts.map(alert => { const id = alert.alert_id ?? ''; const checked = state.selected.has(id) ? 'checked' : ''; const owner = (alert.owner || {}).user_name || '—'; const classification = (alert.classification || {}).name || '—'; const closedAt = alert.alert_close_timestamp ? fmtDate(alert.alert_close_timestamp) : '—'; return ` ${statusDot(alert.status)} ${owner} ${id} ${alert.alert_title || '—'} ${classification} ${severityBadge(alert.severity)} ${fmtDate(alert.alert_creation_time)} ${renderKpiBar(alert.kpi)} ${fmtDate(alert.alert_source_event_time)} ${closedAt} — `; }).join(''); } function toggleAll(chk) { document.querySelectorAll('.row-chk').forEach(el => { el.checked = chk.checked; const id = Number(el.dataset.id); if (chk.checked) state.selected.add(id); else state.selected.delete(id); }); updateSelectionLabel(); } function toggleRow(el) { const id = Number(el.dataset.id); if (el.checked) state.selected.add(id); else state.selected.delete(id); updateSelectionLabel(); } function updateSelectionLabel() { // selection label removed from UI } function updatePageInfo() { const el = document.getElementById('page-info'); if (el) { const totalPages = Math.max(1, Math.ceil(state.total / state.perPage)); el.textContent = `Page ${state.page} of ${totalPages} (${state.total} total)`; } } function prevPage() { if (state.page > 1) { state.page--; refreshActive(); } } function nextPage() { const totalPages = Math.ceil(state.total / state.perPage); if (state.page < totalPages) { state.page++; refreshActive(); } } function sortBy(col) { if (state.sortBy === col) { state.sortDir = state.sortDir === 'asc' ? 'desc' : 'asc'; } else { state.sortBy = col; state.sortDir = 'desc'; } state.page = 1; loadAlerts(); } async function assignSelected() { if (!state.selected.size) { alert('No alerts selected.'); return; } const ownerIdStr = prompt('Enter owner user ID to assign:'); if (!ownerIdStr) return; const ownerId = parseInt(ownerIdStr, 10); if (isNaN(ownerId)) { alert('Invalid owner ID.'); return; } const ids = Array.from(state.selected); try { await Promise.all(ids.map(id => fetch(`${API_BASE}/alerts/${id}/assign`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ owner_id: ownerId }), }) )); state.selected.clear(); loadAlerts(); } catch (err) { alert(`Assignment failed: ${err.message}`); } } function exportCsv() { const params = new URLSearchParams({ sort_by: state.sortBy, sort_dir: state.sortDir, }); if (state.filterTitle) params.set('filter_title', state.filterTitle); if (state.filterOwner) params.set('filter_owner', state.filterOwner); window.location.href = `${API_BASE}/alerts/export-csv?${params}`; } let activeTab = 'alerts'; function switchTab(tab) { activeTab = tab; document.getElementById('tab-alerts').classList.toggle('active', tab === 'alerts'); document.getElementById('tab-cases').classList.toggle('active', tab === 'cases'); state.page = 1; activeTab === 'alerts' ? loadAlerts() : loadCases(); } async function loadCases() { const params = new URLSearchParams({ page: state.page, per_page: state.perPage, sort_by: state.sortBy, sort_dir: state.sortDir, }); const body = document.getElementById('alerts-body'); body.innerHTML = 'Loading...'; try { const res = await fetch(`${API_BASE}/cases?${params}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const json = await res.json(); const casesData = json.data?.cases ?? {}; const cases = casesData.data ?? []; state.total = casesData.total ?? cases.length; renderCasesTable(cases); updatePageInfo(); } catch (err) { body.innerHTML = `Error: ${err.message}`; } } function renderCasesTable(cases) { const body = document.getElementById('alerts-body'); if (!cases.length) { body.innerHTML = 'No cases found.'; return; } body.innerHTML = cases.map(c => { const owner = (c.owner || {}).user_name || '—'; const stateName = (c.state || {}).state_name || '—'; const closed = c.close_date ? fmtDate(c.close_date) : '—'; return ` ${statusDot({status_name: stateName})} ${owner} ${c.case_id ?? ''} ${c.case_name || '—'} ${c.case_soc_id || '—'} — ${fmtDate(c.open_date)} ${renderKpiBar(c.kpi)} — ${closed} — `; }).join(''); } function openAlertDetail(alertId, event) { // Don't navigate when clicking the checkbox if (event && event.target.type === 'checkbox') return; window.location.href = `/alerts?alert_ids=${alertId}&cid=1`; } function refreshActive() { activeTab === 'alerts' ? loadAlerts() : loadCases(); } // Auto-load on page ready document.addEventListener('DOMContentLoaded', loadAlerts); setInterval(refreshActive, 60_000); // Expose for inline onclick handlers window.debouncedLoad = debouncedLoad; window.loadAlerts = loadAlerts; window.toggleAll = toggleAll; window.toggleRow = toggleRow; window.assignSelected = assignSelected; window.exportCsv = exportCsv; window.prevPage = prevPage; window.nextPage = nextPage; window.sortBy = sortBy; window.switchTab = switchTab; window.openAlertDetail = openAlertDetail;