Cdsert updated
This commit is contained in:
parent
3c628b58b0
commit
6143b192d8
367
.Net Capstone Project/cdsalerts updated.html
Normal file
367
.Net Capstone Project/cdsalerts updated.html
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
<!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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
Loading…
Reference in New Issue
Block a user