| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 |
- // 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 = '<tr><td colspan="12" class="text-center text-muted py-4">Loading...</td></tr>';
- 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 = `<tr><td colspan="12" class="text-center text-danger py-4">Error: ${err.message}</td></tr>`;
- }
- }
- function renderKpiBar(kpi) {
- if (!kpi) return '<span class="text-muted">—</span>';
- 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 =>
- `<div class="kpi-seg" style="background:${s.active ? s.color : '#e5e7eb'}" title="${s.label}: ${s.active ? 'active' : 'inactive'}"></div>`
- ).join('');
- return `
- <div class="kpi-bar-wrap" title="${kpi.status} — ${kpi.elapsed_pct}% elapsed">
- <div class="kpi-segs">${segs}</div>
- <div class="kpi-track">
- <div class="kpi-fill" style="width:${fillW}px;background:${fillColor}"></div>
- </div>
- <span class="kpi-pct-label">${kpi.status}</span>
- </div>`;
- }
- 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 `<span class="${cls}">${severity.severity_name || ''}</span>`;
- }
- function statusDot(status) {
- if (!status) return '';
- const name = (status.status_name || '').toLowerCase();
- const cls = name === 'closed' ? 'dot-closed' : 'dot-open';
- return `<span class="status-dot ${cls}" title="${status.status_name || ''}"></span>`;
- }
- 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 = '<tr><td colspan="12" class="text-center text-muted py-4">No alerts found.</td></tr>';
- 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 `
- <tr class="alert-row" style="cursor:pointer" onclick="openAlertDetail(${id}, event)">
- <td><input type="checkbox" class="row-chk" data-id="${id}" ${checked} onchange="toggleRow(this)"></td>
- <td>${statusDot(alert.status)}</td>
- <td>${owner}</td>
- <td>${id}</td>
- <td>${alert.alert_title || '—'}</td>
- <td>${classification}</td>
- <td>${severityBadge(alert.severity)}</td>
- <td>${fmtDate(alert.alert_creation_time)}</td>
- <td>${renderKpiBar(alert.kpi)}</td>
- <td>${fmtDate(alert.alert_source_event_time)}</td>
- <td>${closedAt}</td>
- <td>—</td>
- </tr>`;
- }).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 = '<tr><td colspan="12" class="text-center text-muted py-4">Loading...</td></tr>';
- 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 = `<tr><td colspan="12" class="text-center text-danger py-4">Error: ${err.message}</td></tr>`;
- }
- }
- function renderCasesTable(cases) {
- const body = document.getElementById('alerts-body');
- if (!cases.length) {
- body.innerHTML = '<tr><td colspan="12" class="text-center text-muted py-4">No cases found.</td></tr>';
- 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 `<tr>
- <td></td>
- <td>${statusDot({status_name: stateName})}</td>
- <td>${owner}</td>
- <td>${c.case_id ?? ''}</td>
- <td>${c.case_name || '—'}</td>
- <td>${c.case_soc_id || '—'}</td>
- <td>—</td>
- <td>${fmtDate(c.open_date)}</td>
- <td>${renderKpiBar(c.kpi)}</td>
- <td>—</td>
- <td>${closed}</td>
- <td>—</td>
- </tr>`;
- }).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;
|