Nav apraksta

kpi_dashboard.js 9.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. // KPI Alert Dashboard — vanilla JS, compiled by Vite into /static/assets/js/iris/kpi_dashboard.js
  2. import '../css/kpi_dashboard.css';
  3. const API_BASE = '/kpi-dashboard/api';
  4. const state = {
  5. page: 1,
  6. perPage: 20,
  7. sortBy: 'alert_id',
  8. sortDir: 'desc',
  9. filterTitle: '',
  10. filterOwner: '',
  11. total: 0,
  12. selected: new Set(),
  13. };
  14. // Debounce helper for filter inputs
  15. let _debounceTimer = null;
  16. function debouncedLoad() {
  17. clearTimeout(_debounceTimer);
  18. _debounceTimer = setTimeout(() => {
  19. state.page = 1;
  20. loadAlerts();
  21. }, 350);
  22. }
  23. async function loadAlerts() {
  24. state.filterTitle = document.getElementById('filter-title').value;
  25. state.filterOwner = document.getElementById('filter-owner').value;
  26. const params = new URLSearchParams({
  27. page: state.page,
  28. per_page: state.perPage,
  29. sort_by: state.sortBy,
  30. sort_dir: state.sortDir,
  31. });
  32. if (state.filterTitle) params.set('filter_title', state.filterTitle);
  33. if (state.filterOwner) params.set('filter_owner', state.filterOwner);
  34. const body = document.getElementById('alerts-body');
  35. body.innerHTML = '<tr><td colspan="12" class="text-center text-muted py-4">Loading...</td></tr>';
  36. try {
  37. const res = await fetch(`${API_BASE}/alerts?${params}`);
  38. if (!res.ok) throw new Error(`HTTP ${res.status}`);
  39. const json = await res.json();
  40. const alertsData = json.data?.alerts ?? {};
  41. const alerts = alertsData.data ?? [];
  42. state.total = alertsData.total ?? alerts.length;
  43. renderTable(alerts);
  44. updatePageInfo();
  45. updateSelectionLabel();
  46. } catch (err) {
  47. body.innerHTML = `<tr><td colspan="12" class="text-center text-danger py-4">Error: ${err.message}</td></tr>`;
  48. }
  49. }
  50. function renderKpiBar(kpi) {
  51. if (!kpi) return '<span class="text-muted">—</span>';
  52. const containerW = 100;
  53. const fillW = Math.round((kpi.elapsed_pct / 100) * containerW);
  54. const fillColor = kpi.resolved ? '#6b7280'
  55. : kpi.elapsed_pct >= 75 ? '#ef4444'
  56. : kpi.elapsed_pct >= 50 ? '#f97316'
  57. : kpi.elapsed_pct >= 25 ? '#eab308'
  58. : '#22c55e';
  59. const segs = (kpi.segments || []).map(s =>
  60. `<div class="kpi-seg" style="background:${s.active ? s.color : '#e5e7eb'}" title="${s.label}: ${s.active ? 'active' : 'inactive'}"></div>`
  61. ).join('');
  62. return `
  63. <div class="kpi-bar-wrap" title="${kpi.status} — ${kpi.elapsed_pct}% elapsed">
  64. <div class="kpi-segs">${segs}</div>
  65. <div class="kpi-track">
  66. <div class="kpi-fill" style="width:${fillW}px;background:${fillColor}"></div>
  67. </div>
  68. <span class="kpi-pct-label">${kpi.status}</span>
  69. </div>`;
  70. }
  71. function severityBadge(severity) {
  72. if (!severity) return '';
  73. const name = (severity.severity_name || '').toLowerCase();
  74. const cls = name === 'high' ? 'badge-high' : name === 'medium' ? 'badge-medium' : 'badge-low';
  75. return `<span class="${cls}">${severity.severity_name || ''}</span>`;
  76. }
  77. function statusDot(status) {
  78. if (!status) return '';
  79. const name = (status.status_name || '').toLowerCase();
  80. const cls = name === 'closed' ? 'dot-closed' : 'dot-open';
  81. return `<span class="status-dot ${cls}" title="${status.status_name || ''}"></span>`;
  82. }
  83. function fmtDate(str) {
  84. if (!str) return '—';
  85. try {
  86. return new Date(str).toLocaleString();
  87. } catch {
  88. return str;
  89. }
  90. }
  91. function renderTable(alerts) {
  92. const body = document.getElementById('alerts-body');
  93. if (!alerts.length) {
  94. body.innerHTML = '<tr><td colspan="12" class="text-center text-muted py-4">No alerts found.</td></tr>';
  95. return;
  96. }
  97. body.innerHTML = alerts.map(alert => {
  98. const id = alert.alert_id ?? '';
  99. const checked = state.selected.has(id) ? 'checked' : '';
  100. const owner = (alert.owner || {}).user_name || '—';
  101. const classification = (alert.classification || {}).name || '—';
  102. const closedAt = alert.alert_close_timestamp ? fmtDate(alert.alert_close_timestamp) : '—';
  103. return `
  104. <tr class="alert-row" style="cursor:pointer" onclick="openAlertDetail(${id}, event)">
  105. <td><input type="checkbox" class="row-chk" data-id="${id}" ${checked} onchange="toggleRow(this)"></td>
  106. <td>${statusDot(alert.status)}</td>
  107. <td>${owner}</td>
  108. <td>${id}</td>
  109. <td>${alert.alert_title || '—'}</td>
  110. <td>${classification}</td>
  111. <td>${severityBadge(alert.severity)}</td>
  112. <td>${fmtDate(alert.alert_creation_time)}</td>
  113. <td>${renderKpiBar(alert.kpi)}</td>
  114. <td>${fmtDate(alert.alert_source_event_time)}</td>
  115. <td>${closedAt}</td>
  116. <td>—</td>
  117. </tr>`;
  118. }).join('');
  119. }
  120. function toggleAll(chk) {
  121. document.querySelectorAll('.row-chk').forEach(el => {
  122. el.checked = chk.checked;
  123. const id = Number(el.dataset.id);
  124. if (chk.checked) state.selected.add(id);
  125. else state.selected.delete(id);
  126. });
  127. updateSelectionLabel();
  128. }
  129. function toggleRow(el) {
  130. const id = Number(el.dataset.id);
  131. if (el.checked) state.selected.add(id);
  132. else state.selected.delete(id);
  133. updateSelectionLabel();
  134. }
  135. function updateSelectionLabel() {
  136. // selection label removed from UI
  137. }
  138. function updatePageInfo() {
  139. const el = document.getElementById('page-info');
  140. if (el) {
  141. const totalPages = Math.max(1, Math.ceil(state.total / state.perPage));
  142. el.textContent = `Page ${state.page} of ${totalPages} (${state.total} total)`;
  143. }
  144. }
  145. function prevPage() {
  146. if (state.page > 1) {
  147. state.page--;
  148. refreshActive();
  149. }
  150. }
  151. function nextPage() {
  152. const totalPages = Math.ceil(state.total / state.perPage);
  153. if (state.page < totalPages) {
  154. state.page++;
  155. refreshActive();
  156. }
  157. }
  158. function sortBy(col) {
  159. if (state.sortBy === col) {
  160. state.sortDir = state.sortDir === 'asc' ? 'desc' : 'asc';
  161. } else {
  162. state.sortBy = col;
  163. state.sortDir = 'desc';
  164. }
  165. state.page = 1;
  166. loadAlerts();
  167. }
  168. async function assignSelected() {
  169. if (!state.selected.size) {
  170. alert('No alerts selected.');
  171. return;
  172. }
  173. const ownerIdStr = prompt('Enter owner user ID to assign:');
  174. if (!ownerIdStr) return;
  175. const ownerId = parseInt(ownerIdStr, 10);
  176. if (isNaN(ownerId)) {
  177. alert('Invalid owner ID.');
  178. return;
  179. }
  180. const ids = Array.from(state.selected);
  181. try {
  182. await Promise.all(ids.map(id =>
  183. fetch(`${API_BASE}/alerts/${id}/assign`, {
  184. method: 'POST',
  185. headers: { 'Content-Type': 'application/json' },
  186. body: JSON.stringify({ owner_id: ownerId }),
  187. })
  188. ));
  189. state.selected.clear();
  190. loadAlerts();
  191. } catch (err) {
  192. alert(`Assignment failed: ${err.message}`);
  193. }
  194. }
  195. function exportCsv() {
  196. const params = new URLSearchParams({
  197. sort_by: state.sortBy,
  198. sort_dir: state.sortDir,
  199. });
  200. if (state.filterTitle) params.set('filter_title', state.filterTitle);
  201. if (state.filterOwner) params.set('filter_owner', state.filterOwner);
  202. window.location.href = `${API_BASE}/alerts/export-csv?${params}`;
  203. }
  204. let activeTab = 'alerts';
  205. function switchTab(tab) {
  206. activeTab = tab;
  207. document.getElementById('tab-alerts').classList.toggle('active', tab === 'alerts');
  208. document.getElementById('tab-cases').classList.toggle('active', tab === 'cases');
  209. state.page = 1;
  210. activeTab === 'alerts' ? loadAlerts() : loadCases();
  211. }
  212. async function loadCases() {
  213. const params = new URLSearchParams({
  214. page: state.page,
  215. per_page: state.perPage,
  216. sort_by: state.sortBy,
  217. sort_dir: state.sortDir,
  218. });
  219. const body = document.getElementById('alerts-body');
  220. body.innerHTML = '<tr><td colspan="12" class="text-center text-muted py-4">Loading...</td></tr>';
  221. try {
  222. const res = await fetch(`${API_BASE}/cases?${params}`);
  223. if (!res.ok) throw new Error(`HTTP ${res.status}`);
  224. const json = await res.json();
  225. const casesData = json.data?.cases ?? {};
  226. const cases = casesData.data ?? [];
  227. state.total = casesData.total ?? cases.length;
  228. renderCasesTable(cases);
  229. updatePageInfo();
  230. } catch (err) {
  231. body.innerHTML = `<tr><td colspan="12" class="text-center text-danger py-4">Error: ${err.message}</td></tr>`;
  232. }
  233. }
  234. function renderCasesTable(cases) {
  235. const body = document.getElementById('alerts-body');
  236. if (!cases.length) {
  237. body.innerHTML = '<tr><td colspan="12" class="text-center text-muted py-4">No cases found.</td></tr>';
  238. return;
  239. }
  240. body.innerHTML = cases.map(c => {
  241. const owner = (c.owner || {}).user_name || '—';
  242. const stateName = (c.state || {}).state_name || '—';
  243. const closed = c.close_date ? fmtDate(c.close_date) : '—';
  244. return `<tr>
  245. <td></td>
  246. <td>${statusDot({status_name: stateName})}</td>
  247. <td>${owner}</td>
  248. <td>${c.case_id ?? ''}</td>
  249. <td>${c.case_name || '—'}</td>
  250. <td>${c.case_soc_id || '—'}</td>
  251. <td>—</td>
  252. <td>${fmtDate(c.open_date)}</td>
  253. <td>${renderKpiBar(c.kpi)}</td>
  254. <td>—</td>
  255. <td>${closed}</td>
  256. <td>—</td>
  257. </tr>`;
  258. }).join('');
  259. }
  260. function openAlertDetail(alertId, event) {
  261. // Don't navigate when clicking the checkbox
  262. if (event && event.target.type === 'checkbox') return;
  263. window.location.href = `/alerts?alert_ids=${alertId}&cid=1`;
  264. }
  265. function refreshActive() {
  266. activeTab === 'alerts' ? loadAlerts() : loadCases();
  267. }
  268. // Auto-load on page ready
  269. document.addEventListener('DOMContentLoaded', loadAlerts);
  270. setInterval(refreshActive, 60_000);
  271. // Expose for inline onclick handlers
  272. window.debouncedLoad = debouncedLoad;
  273. window.loadAlerts = loadAlerts;
  274. window.toggleAll = toggleAll;
  275. window.toggleRow = toggleRow;
  276. window.assignSelected = assignSelected;
  277. window.exportCsv = exportCsv;
  278. window.prevPage = prevPage;
  279. window.nextPage = nextPage;
  280. window.sortBy = sortBy;
  281. window.switchTab = switchTab;
  282. window.openAlertDetail = openAlertDetail;