DotNet_Capstone_Project/.Net Capstone Project/cdsalerts updated.html
2026-08-27 13:56:53 +00:00

368 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CDS Alerts — Clinical Insight Pro</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="css/styles.css">
<style>
.cds-tile-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; }
.cds-badge {
display: inline-block; padding: 0.28rem 0.7rem; border-radius: 999px;
font-size: 0.76rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.4px;
}
.cds-badge.critical, .cds-badge.high { background: #fdecea; color: #c0392b; }
.cds-badge.warning, .cds-badge.moderate { background: #fff6dd; color: #a9750c; }
.cds-badge.info, .cds-badge.low { background: #e8f2fb; color: #2e6fa8; }
.cds-priority-wrap { display: flex; align-items: center; gap: 0.5rem; min-width: 110px; }
.cds-priority-bar { flex: 1; height: 7px; border-radius: 999px; background: #eef1f1; overflow: hidden; }
.cds-priority-fill { height: 100%; border-radius: 999px; }
.cds-priority-fill.critical { background: #d64545; }
.cds-priority-fill.warning { background: #e0a83d; }
.cds-priority-fill.info { background: #4a90c4; }
.cds-priority-pct { font-size: 0.78rem; font-weight: 700; color: var(--ci-muted); width: 34px; text-align: right; }
.cds-overall-wrap { display: flex; align-items: center; gap: 10px; margin: 0.25rem 0 1.1rem; }
.cds-overall-bar { flex: 1; height: 9px; border-radius: 999px; background: #eef1f1; overflow: hidden; }
.cds-overall-fill { height: 100%; border-radius: 999px; }
.cds-overall-fill.high { background: #d64545; }
.cds-overall-fill.moderate { background: #e0a83d; }
.cds-overall-fill.low { background: #27ae60; }
.cds-overall-num { font-size: 1.15rem; font-weight: 700; min-width: 56px; text-align: right; }
.cds-actioned-pill { font-size: 0.72rem; font-weight: 600; padding: 0.15rem 0.55rem; border-radius: 999px; background: #eafaf1; color: #1e8449; }
.cds-actioned-pill.pending { background: #f4f4f4; color: #7a7a7a; }
.cds-alert-card { border: 1px solid #e6e9e8; border-radius: 12px; padding: 1rem 1.1rem; margin-bottom: 0.85rem; background: #fff; }
.cds-row-critical { border-left: 3px solid #d64545; }
.cds-row-warning { border-left: 3px solid #e0a83d; }
.cds-row-info { border-left: 3px solid #4a90c4; }
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-ci">
<div class="container-fluid px-4">
<a class="navbar-brand" href="dashboard.html">🩺 Clinical Insight Pro</a>
<div class="ms-auto d-flex align-items-center gap-3">
<span class="navbar-text" id="clinicianLabel">Clinician</span>
<button class="btn btn-outline-light btn-sm" id="logoutBtn">Sign out</button>
</div>
</div>
</nav>
<div class="app-shell">
<aside class="app-sidebar" id="appSidebar"></aside>
<main class="app-main">
<div class="resource-page-header">
<h4>🔔 CDS Alerts</h4>
<span class="text-muted-ci small" id="cds_countLabel"></span>
</div>
<div class="card-ci p-3 mb-3 d-flex flex-row align-items-center gap-3 flex-wrap">
<input type="text" class="form-control resource-search-bar" id="cds_searchInput" placeholder="Search by patient name or patient ID…" autocomplete="off">
<a href="cdsalert-add.html" class="btn btn-ci-primary ms-auto">+ Add Alert</a>
</div>
<div id="cds_body" class="cds-tile-grid"></div>
<div id="cds_empty" class="text-muted-ci text-center py-5 d-none">No CDS alerts on file.</div>
</main>
</div>
<div class="toast-container position-fixed bottom-0 end-0 p-3" id="toastContainer"></div>
<div class="modal fade" id="cds_editModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="cds_editModalTitle">Edit Alert</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="cds_form">
<div class="modal-body row g-2">
<div class="col-12">
<label class="form-label small">Severity</label>
<select class="form-select" id="cds_severity">
<option value="info">Info</option>
<option value="warning" selected>Warning</option>
<option value="critical">Critical</option>
</select>
</div>
<div class="col-12">
<label class="form-label small">Summary</label>
<textarea class="form-control" id="cds_summary" rows="2" required></textarea>
</div>
<div class="col-12">
<label class="form-label small">Recommendation</label>
<textarea class="form-control" id="cds_recommendation" rows="2" required></textarea>
</div>
<div class="col-12 form-check mt-2">
<input type="checkbox" class="form-check-input" id="cds_actioned">
<label class="form-check-label small" for="cds_actioned">Already actioned</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-ci-primary">Save</button>
</div>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="js/config.js"></script>
<script src="js/sidebar.js"></script>
<script src="js/api.js"></script>
<script>
const CDS_TILE_COLORS = ['primary', 'success', 'info', 'warning', 'danger', 'secondary'];
const CDS_PRIORITY = { critical: 100, warning: 60, info: 30 };
const CDS_RULE_WEIGHTS = { critical: 35, warning: 20, info: 8 };
let cdsSearchTerm = '';
let cdsEditModal, cdsEditingId = null, cdsEditingPatientId = null;
let cdsViewModalInstance = null;
function cdsEscapeHtml(s) {
if (s === null || s === undefined) return '';
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
}
function cdsFmtDate(value) {
if (!value) return '—';
const d = new Date(value);
if (isNaN(d)) return '—';
return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
}
function cdsShowToast(message, variant = 'success') {
const container = document.getElementById('toastContainer');
const el = document.createElement('div');
el.className = `toast align-items-center text-bg-${variant === 'danger' ? 'danger' : 'success'} border-0 show mb-2`;
el.innerHTML = `<div class="d-flex"><div class="toast-body">${cdsEscapeHtml(message)}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" onclick="this.closest('.toast').remove()"></button></div>`;
container.appendChild(el);
setTimeout(() => el.remove(), 5000);
}
function cdsComputeOverallRisk(alerts) {
let score = 0;
alerts.forEach(a => { score += CDS_RULE_WEIGHTS[(a.severity || '').toLowerCase()] || 0; });
score = Math.min(score, 100);
const level = score >= 70 ? 'high' : score >= 40 ? 'moderate' : 'low';
return { score, level };
}
function cdsEnsureViewModal() {
let el = document.getElementById('cds_viewModal');
if (el) return el;
el = document.createElement('div');
el.className = 'modal fade';
el.id = 'cds_viewModal';
el.tabIndex = -1;
el.innerHTML = `
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="cds_viewModalTitle"></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="cds_viewModalBody"></div>
</div>
</div>`;
document.body.appendChild(el);
cdsViewModalInstance = new bootstrap.Modal(el);
return el;
}
function cdsOpenViewModal(patientId, allRows) {
const items = allRows.filter(r => r.patientId === patientId)
.sort((a, b) => (CDS_PRIORITY[(b.severity||'').toLowerCase()]||0) - (CDS_PRIORITY[(a.severity||'').toLowerCase()]||0));
if (items.length === 0) return;
cdsEnsureViewModal();
document.getElementById('cds_viewModalTitle').textContent = items[0].patientName;
const overall = cdsComputeOverallRisk(items);
const critCount = items.filter(a => (a.severity||'').toLowerCase() === 'critical').length;
const warnCount = items.filter(a => (a.severity||'').toLowerCase() === 'warning').length;
const infoCount = items.filter(a => (a.severity||'').toLowerCase() === 'info').length;
const summaryHtml = `
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
<span class="cds-badge ${overall.level}">${overall.level} CDS risk</span>
<span class="small text-muted-ci">${critCount} critical (+${CDS_RULE_WEIGHTS.critical} each) · ${warnCount} warning (+${CDS_RULE_WEIGHTS.warning} each) · ${infoCount} info (+${CDS_RULE_WEIGHTS.info} each)</span>
</div>
<div class="cds-overall-wrap">
<div class="cds-overall-bar"><div class="cds-overall-fill ${overall.level}" style="width:${overall.score}%"></div></div>
<span class="cds-overall-num">${overall.score}%</span>
</div>
</div>`;
const cardsHtml = items.map(r => {
const sev = (r.severity || 'warning').toLowerCase();
const pct = CDS_PRIORITY[sev] ?? 50;
return `
<div class="cds-alert-card cds-row-${sev}">
<div class="d-flex justify-content-between align-items-start flex-wrap gap-2">
<span class="cds-badge ${sev}">${cdsEscapeHtml(sev)}</span>
<span class="cds-actioned-pill ${r.actioned ? '' : 'pending'}">${r.actioned ? 'Actioned' : 'Pending'}</span>
</div>
<div class="cds-priority-wrap mt-2">
<div class="cds-priority-bar"><div class="cds-priority-fill ${sev}" style="width:${pct}%"></div></div>
<span class="cds-priority-pct">${pct}%</span>
</div>
<div class="fw-semibold mt-2">${cdsEscapeHtml(r.summary)}</div>
<div class="small text-muted-ci mt-1">${cdsEscapeHtml(r.recommendation)}</div>
<div class="small text-muted-ci mt-2">Generated ${cdsFmtDate(r.generatedDate)}</div>
<div class="mt-2 text-end">
<button class="btn btn-sm btn-outline-secondary me-1" data-edit-row="${r.id}" data-edit-patient="${r.patientId}">Edit</button>
<button class="btn btn-sm btn-outline-danger" data-delete-row="${r.id}" data-delete-patient="${r.patientId}">Delete</button>
</div>
</div>`;
}).join('');
document.getElementById('cds_viewModalBody').innerHTML = summaryHtml + cardsHtml;
cdsViewModalInstance.show();
}
async function cdsLoadGrid() {
const container = document.getElementById('cds_body');
const empty = document.getElementById('cds_empty');
let rows;
try {
rows = await Api.listAllCdsAlerts(cdsSearchTerm.trim());
} catch (err) {
cdsShowToast(err.message, 'danger');
return;
}
const patientCount = new Set(rows.map(r => r.patientId)).size;
document.getElementById('cds_countLabel').textContent = `${rows.length} alert${rows.length === 1 ? '' : 's'} · ${patientCount} patient${patientCount === 1 ? '' : 's'}`;
if (rows.length === 0) {
container.innerHTML = '';
empty.classList.remove('d-none');
return;
}
empty.classList.add('d-none');
const groups = new Map();
rows.forEach(r => {
if (!groups.has(r.patientId)) groups.set(r.patientId, { patientId: r.patientId, patientName: r.patientName, items: [] });
groups.get(r.patientId).items.push(r);
});
container.innerHTML = [...groups.values()].map((g, i) => {
const color = CDS_TILE_COLORS[i % CDS_TILE_COLORS.length];
const initials = g.patientName.split(' ').map(n => n[0]).filter(Boolean).slice(0, 2).join('').toUpperCase();
const overall = cdsComputeOverallRisk(g.items);
return `
<div class="card shadow-sm border-0" style="cursor:pointer" data-view-patient="${g.patientId}">
<div class="card-body text-center py-3">
<div class="rounded-circle bg-${color} text-white d-inline-flex align-items-center justify-content-center mb-2" style="width:44px;height:44px;font-size:14px">${cdsEscapeHtml(initials)}</div>
<div class="fw-semibold small">${cdsEscapeHtml(g.patientName)}</div>
<span class="cds-badge ${overall.level} mt-1 d-inline-block">${overall.level} risk · ${overall.score}%</span>
<div class="mt-2">
<button type="button" class="btn btn-sm btn-outline-${color}" data-view-patient="${g.patientId}">View (${g.items.length})</button>
</div>
</div>
</div>`;
}).join('');
container._cdsRows = rows;
}
function cdsOpenEdit(row) {
cdsEditingId = row.id;
cdsEditingPatientId = row.patientId;
document.getElementById('cds_editModalTitle').textContent = `Edit Alert — ${row.patientName}`;
document.getElementById('cds_severity').value = (row.severity || 'warning').toLowerCase();
document.getElementById('cds_summary').value = row.summary || '';
document.getElementById('cds_recommendation').value = row.recommendation || '';
document.getElementById('cds_actioned').checked = !!row.actioned;
cdsEditModal.show();
}
async function cdsSubmit(e) {
e.preventDefault();
const payload = {
severity: document.getElementById('cds_severity').value,
summary: document.getElementById('cds_summary').value,
recommendation: document.getElementById('cds_recommendation').value,
actioned: document.getElementById('cds_actioned').checked
};
try {
await Api.updateCdsAlert(cdsEditingPatientId, cdsEditingId, payload);
cdsShowToast('Alert updated.');
cdsEditModal.hide();
await cdsLoadGrid();
if (cdsViewModalInstance) cdsViewModalInstance.hide();
} catch (err) {
cdsShowToast(err.message, 'danger');
}
}
async function cdsDelete(row) {
if (!confirm(`Delete this alert for ${row.patientName}?`)) return;
try {
await Api.deleteCdsAlert(row.patientId, row.id);
cdsShowToast('Deleted.');
await cdsLoadGrid();
const container = document.getElementById('cds_body');
const stillHasRows = (container._cdsRows || []).some(r => r.patientId === row.patientId);
if (stillHasRows) cdsOpenViewModal(row.patientId, container._cdsRows);
else if (cdsViewModalInstance) cdsViewModalInstance.hide();
} catch (err) {
cdsShowToast(err.message, 'danger');
}
}
document.addEventListener('DOMContentLoaded', () => {
if (!sessionStorage.getItem('cip_token')) { window.location.href = 'login.html'; return; }
renderSidebar('cdsalerts');
document.getElementById('clinicianLabel').textContent = `👤 ${sessionStorage.getItem('cip_clinician') || 'Clinician'}`;
document.getElementById('logoutBtn').addEventListener('click', () => { sessionStorage.clear(); window.location.href = 'login.html'; });
cdsEditModal = new bootstrap.Modal(document.getElementById('cds_editModal'));
document.getElementById('cds_form').addEventListener('submit', cdsSubmit);
document.getElementById('cds_searchInput').addEventListener('input', (e) => {
cdsSearchTerm = e.target.value;
cdsLoadGrid();
});
document.getElementById('cds_body').addEventListener('click', (e) => {
const viewEl = e.target.closest('[data-view-patient]');
if (!viewEl) return;
const container = document.getElementById('cds_body');
cdsOpenViewModal(viewEl.dataset.viewPatient, container._cdsRows || []);
});
cdsEnsureViewModal().addEventListener('click', (e) => {
const editBtn = e.target.closest('[data-edit-row]');
const delBtn = e.target.closest('[data-delete-row]');
const container = document.getElementById('cds_body');
const rows = container._cdsRows || [];
if (editBtn) {
const row = rows.find(r => String(r.id) === editBtn.dataset.editRow && r.patientId === editBtn.dataset.editPatient);
if (row) { cdsViewModalInstance.hide(); cdsOpenEdit(row); }
} else if (delBtn) {
const row = rows.find(r => String(r.id) === delBtn.dataset.deleteRow && r.patientId === delBtn.dataset.deletePatient);
if (row) cdsDelete(row);
}
});
cdsLoadGrid();
});
</script>
</body>
</html>