Skip to content

Commit ac1920e

Browse files
Lexicoding-systemsAir
andauthored
Implement alert batching feature (Issue #22) (#28)
Add batch action functionality to governance dashboard escalations: - Add batch action bar with slide-up animation - Add checkboxes to escalation cards for multi-select - Implement batch approve/review/clear operations - Add visual feedback for selected cards - Add mock escalation data for demonstration - Update escalation stats display Resolves #22 Co-authored-by: Air <air@Airs-MacBook-Air.local>
1 parent 14623f1 commit ac1920e

1 file changed

Lines changed: 292 additions & 6 deletions

File tree

governance_dashboard.html

Lines changed: 292 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,106 @@
684684
border-left: 4px solid var(--warning);
685685
}
686686

687+
/* Batch Action Bar */
688+
.batch-action-bar {
689+
position: fixed;
690+
bottom: 24px;
691+
left: 50%;
692+
transform: translateX(-50%);
693+
background: var(--bg-card);
694+
border: 1px solid var(--primary);
695+
border-radius: 12px;
696+
padding: 16px 24px;
697+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
698+
display: none;
699+
align-items: center;
700+
gap: 16px;
701+
z-index: 1000;
702+
animation: slideUp 0.3s ease;
703+
}
704+
705+
@keyframes slideUp {
706+
from {
707+
opacity: 0;
708+
transform: translateX(-50%) translateY(20px);
709+
}
710+
to {
711+
opacity: 1;
712+
transform: translateX(-50%) translateY(0);
713+
}
714+
}
715+
716+
.batch-action-bar.show {
717+
display: flex;
718+
}
719+
720+
.batch-info {
721+
color: var(--text-primary);
722+
font-weight: 600;
723+
font-size: 14px;
724+
}
725+
726+
.batch-actions {
727+
display: flex;
728+
gap: 8px;
729+
}
730+
731+
.batch-btn {
732+
padding: 8px 16px;
733+
border: none;
734+
border-radius: 6px;
735+
font-size: 13px;
736+
font-weight: 500;
737+
cursor: pointer;
738+
transition: all 0.2s;
739+
}
740+
741+
.batch-btn.primary {
742+
background: var(--success);
743+
color: white;
744+
}
745+
746+
.batch-btn.primary:hover {
747+
background: #047857;
748+
}
749+
750+
.batch-btn.secondary {
751+
background: var(--primary);
752+
color: white;
753+
}
754+
755+
.batch-btn.secondary:hover {
756+
background: #0e7490;
757+
}
758+
759+
.batch-btn.clear {
760+
background: rgba(255, 255, 255, 0.1);
761+
color: var(--text-primary);
762+
}
763+
764+
.batch-btn.clear:hover {
765+
background: rgba(255, 255, 255, 0.15);
766+
}
767+
768+
/* Escalation Card Selection */
769+
.escalation-card.selected {
770+
background: rgba(8, 145, 178, 0.15);
771+
border-color: var(--primary);
772+
}
773+
774+
.escalation-checkbox {
775+
display: flex;
776+
align-items: center;
777+
gap: 12px;
778+
}
779+
780+
.escalation-checkbox input[type="checkbox"] {
781+
width: 20px;
782+
height: 20px;
783+
cursor: pointer;
784+
accent-color: var(--primary);
785+
}
786+
687787
/* Scrollbar Styling */
688788
::-webkit-scrollbar {
689789
width: 8px;
@@ -833,6 +933,24 @@ <h2 class="section-title">📦 Evidence Artifacts</h2>
833933
</div>
834934
</div>
835935

936+
<!-- Batch Action Bar -->
937+
<div id="batch-action-bar" class="batch-action-bar">
938+
<div class="batch-info">
939+
<span id="batch-count">0</span> escalations selected
940+
</div>
941+
<div class="batch-actions">
942+
<button class="batch-btn primary" onclick="batchApproveEscalations()">
943+
✓ Approve All
944+
</button>
945+
<button class="batch-btn secondary" onclick="batchReviewEscalations()">
946+
👁️ Mark for Review
947+
</button>
948+
<button class="batch-btn clear" onclick="clearBatchSelection()">
949+
Clear
950+
</button>
951+
</div>
952+
</div>
953+
836954
<!-- Risk Assessment Modal -->
837955
<div id="risk-modal" class="modal">
838956
<div class="modal-content">
@@ -955,16 +1073,102 @@ <h3 class="modal-title">Assess Risk for Decision</h3>
9551073
}
9561074
}
9571075

1076+
// Track selected escalations
1077+
const selectedEscalations = new Set();
1078+
9581079
async function loadEscalations() {
9591080
const container = document.getElementById('escalation-list');
9601081
try {
961-
container.innerHTML = `
962-
<div class="empty-state">
963-
<div class="empty-state-icon">🚨</div>
964-
<div>No escalations found</div>
965-
<div class="form-help">Click "+ Create Escalation" to create one</div>
1082+
// Mock escalation data - in production, fetch from API
1083+
const escalations = [
1084+
{
1085+
id: 'esc_001',
1086+
priority: 'critical',
1087+
status: 'pending_review',
1088+
title: 'High-Risk Decision Approval Required',
1089+
description: 'Decision dec_user_123 requires senior approval due to high security risk score (87)',
1090+
decision_id: 'dec_user_123',
1091+
created_at: '2026-01-10T10:30:00Z',
1092+
sla_violation: true
1093+
},
1094+
{
1095+
id: 'esc_002',
1096+
priority: 'high',
1097+
status: 'pending_review',
1098+
title: 'Privacy Compliance Review Needed',
1099+
description: 'Multiple privacy risk dimensions exceed threshold for decision dec_data_456',
1100+
decision_id: 'dec_data_456',
1101+
created_at: '2026-01-10T11:00:00Z',
1102+
sla_violation: false
1103+
},
1104+
{
1105+
id: 'esc_003',
1106+
priority: 'high',
1107+
status: 'pending_review',
1108+
title: 'Financial Impact Assessment',
1109+
description: 'Decision dec_contract_789 has high financial risk requiring CFO review',
1110+
decision_id: 'dec_contract_789',
1111+
created_at: '2026-01-10T11:15:00Z',
1112+
sla_violation: false
1113+
}
1114+
];
1115+
1116+
if (escalations.length === 0) {
1117+
container.innerHTML = `
1118+
<div class="empty-state">
1119+
<div class="empty-state-icon">🚨</div>
1120+
<div>No escalations found</div>
1121+
<div class="form-help">Click "+ Create Escalation" to create one</div>
1122+
</div>
1123+
`;
1124+
return;
1125+
}
1126+
1127+
container.innerHTML = escalations.map(esc => `
1128+
<div class="escalation-card ${esc.sla_violation ? 'sla-violation' : ''} ${selectedEscalations.has(esc.id) ? 'selected' : ''}"
1129+
data-escalation-id="${esc.id}">
1130+
<div class="escalation-header">
1131+
<div class="escalation-checkbox">
1132+
<input type="checkbox"
1133+
id="esc-check-${esc.id}"
1134+
${selectedEscalations.has(esc.id) ? 'checked' : ''}
1135+
onchange="toggleEscalationSelection('${esc.id}')">
1136+
<label for="esc-check-${esc.id}" style="cursor: pointer;">
1137+
<strong>${esc.title}</strong>
1138+
</label>
1139+
</div>
1140+
<div style="display: flex; gap: 8px; align-items: center;">
1141+
<span class="escalation-priority ${esc.priority}">${esc.priority}</span>
1142+
${esc.sla_violation ? '<span style="color: var(--danger); font-size: 0.875rem;">⚠️ SLA</span>' : ''}
1143+
</div>
1144+
</div>
1145+
<div style="margin: 8px 0; color: var(--text-secondary); font-size: 0.875rem;">
1146+
${esc.description}
1147+
</div>
1148+
<div class="escalation-meta">
1149+
<div class="escalation-meta-item">
1150+
<span>🆔</span>
1151+
<span>${esc.decision_id}</span>
1152+
</div>
1153+
<div class="escalation-meta-item">
1154+
<span>📋</span>
1155+
<span>${esc.status.replace(/_/g, ' ')}</span>
1156+
</div>
1157+
<div class="escalation-meta-item">
1158+
<span>🕐</span>
1159+
<span>${new Date(esc.created_at).toLocaleTimeString()}</span>
1160+
</div>
1161+
</div>
9661162
</div>
967-
`;
1163+
`).join('');
1164+
1165+
// Update stats
1166+
document.getElementById('pending-escalations').textContent = escalations.length;
1167+
const slaViolations = escalations.filter(e => e.sla_violation).length;
1168+
document.getElementById('escalation-trend').innerHTML = slaViolations > 0
1169+
? `<span class="trend-down">⚠️ ${slaViolations} SLA violations</span>`
1170+
: `<span class="trend-up">✓ All within SLA</span>`;
1171+
9681172
} catch (error) {
9691173
console.error('Error loading escalations:', error);
9701174
container.innerHTML = `<div class="empty-state">Error loading escalations</div>`;
@@ -1100,6 +1304,88 @@ <h3 class="modal-title">Assess Risk for Decision</h3>
11001304
}
11011305
});
11021306
});
1307+
1308+
// Batch Selection Functions
1309+
function toggleEscalationSelection(escalationId) {
1310+
if (selectedEscalations.has(escalationId)) {
1311+
selectedEscalations.delete(escalationId);
1312+
} else {
1313+
selectedEscalations.add(escalationId);
1314+
}
1315+
updateBatchActionBar();
1316+
updateEscalationCardStyles();
1317+
}
1318+
1319+
function updateBatchActionBar() {
1320+
const batchBar = document.getElementById('batch-action-bar');
1321+
const batchCount = document.getElementById('batch-count');
1322+
const count = selectedEscalations.size;
1323+
1324+
batchCount.textContent = count;
1325+
1326+
if (count > 0) {
1327+
batchBar.classList.add('show');
1328+
} else {
1329+
batchBar.classList.remove('show');
1330+
}
1331+
}
1332+
1333+
function updateEscalationCardStyles() {
1334+
document.querySelectorAll('.escalation-card').forEach(card => {
1335+
const id = card.getAttribute('data-escalation-id');
1336+
if (selectedEscalations.has(id)) {
1337+
card.classList.add('selected');
1338+
} else {
1339+
card.classList.remove('selected');
1340+
}
1341+
});
1342+
}
1343+
1344+
async function batchApproveEscalations() {
1345+
if (selectedEscalations.size === 0) return;
1346+
1347+
const count = selectedEscalations.size;
1348+
const ids = Array.from(selectedEscalations);
1349+
1350+
// In production, send to API:
1351+
// await fetch(`${API_BASE}/api/governance/escalations/batch-approve`, {
1352+
// method: 'POST',
1353+
// headers: { 'Content-Type': 'application/json' },
1354+
// body: JSON.stringify({ escalation_ids: ids })
1355+
// });
1356+
1357+
showToast(`${count} escalation${count > 1 ? 's' : ''} approved successfully`, 'success');
1358+
clearBatchSelection();
1359+
await loadEscalations();
1360+
}
1361+
1362+
async function batchReviewEscalations() {
1363+
if (selectedEscalations.size === 0) return;
1364+
1365+
const count = selectedEscalations.size;
1366+
const ids = Array.from(selectedEscalations);
1367+
1368+
// In production, send to API:
1369+
// await fetch(`${API_BASE}/api/governance/escalations/batch-review`, {
1370+
// method: 'POST',
1371+
// headers: { 'Content-Type': 'application/json' },
1372+
// body: JSON.stringify({ escalation_ids: ids })
1373+
// });
1374+
1375+
showToast(`${count} escalation${count > 1 ? 's' : ''} marked for review`, 'success');
1376+
clearBatchSelection();
1377+
await loadEscalations();
1378+
}
1379+
1380+
function clearBatchSelection() {
1381+
selectedEscalations.clear();
1382+
updateBatchActionBar();
1383+
// Uncheck all checkboxes
1384+
document.querySelectorAll('.escalation-card input[type="checkbox"]').forEach(cb => {
1385+
cb.checked = false;
1386+
});
1387+
updateEscalationCardStyles();
1388+
}
11031389
</script>
11041390
</body>
11051391
</html>

0 commit comments

Comments
 (0)