diff --git a/.Net Capstone Project/dashboard27082026.js b/.Net Capstone Project/dashboard27082026.js
new file mode 100644
index 0000000..8bedeac
--- /dev/null
+++ b/.Net Capstone Project/dashboard27082026.js
@@ -0,0 +1,802 @@
+let currentPatientId = null;
+let patientsCache = [];
+let patientSearchTerm = '';
+
+function showSpinner(show) {
+ document.getElementById('spinnerOverlay').style.display = show ? 'flex' : 'none';
+}
+
+function showToast(message, variant = 'success') {
+ const container = document.getElementById('toastContainer');
+ const el = document.createElement('div');
+ el.className = `toast align-items-center text-bg-${variant} border-0 show mb-2`;
+ el.innerHTML = `
`;
+ container.appendChild(el);
+ setTimeout(() => el.remove(), 5000);
+}
+
+function escapeHtml(str) {
+ const div = document.createElement('div');
+ div.textContent = str ?? '';
+ return div.innerHTML;
+}
+
+function fmtDate(dateStr) {
+ if (!dateStr) return '—';
+ const d = new Date(dateStr);
+ return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
+}
+
+// ---------- Dynamic resource forms ----------
+// Field config per FHIR resource type. Drives the generic #addResourceModal
+// so patients, conditions, observations, allergies, medications and
+// encounters can all be added from the UI and hit the CRUD API — nothing
+// here is limited to the three seeded demo patients.
+const RESOURCE_FORMS = {
+ condition: {
+ title: 'Add Condition',
+ fields: [
+ { key: 'code', label: 'Code (ICD-10, e.g. I10)', type: 'text', required: true },
+ { key: 'display', label: 'Display name', type: 'text', required: true },
+ { key: 'clinicalStatus', label: 'Clinical status', type: 'select', options: ['active', 'inactive', 'resolved', 'remission'], default: 'active' },
+ { key: 'onsetDate', label: 'Onset date', type: 'date', required: true }
+ ],
+ add: (patientId, payload) => Api.addCondition(patientId, payload)
+ },
+ observation: {
+ title: 'Add Observation',
+ fields: [
+ { key: 'code', label: 'Code (LOINC, e.g. 8480-6)', type: 'text', required: true },
+ { key: 'display', label: 'Display name', type: 'text', required: true },
+ { key: 'value', label: 'Value', type: 'number', required: true },
+ { key: 'unit', label: 'Unit', type: 'text', required: true },
+ { key: 'effectiveDate', label: 'Effective date/time', type: 'datetime-local', required: true }
+ ],
+ add: (patientId, payload) => Api.addObservation(patientId, payload)
+ },
+ allergy: {
+ title: 'Add Allergy / Intolerance',
+ fields: [
+ { key: 'substance', label: 'Substance', type: 'text', required: true },
+ { key: 'reaction', label: 'Reaction', type: 'text' },
+ { key: 'criticality', label: 'Criticality', type: 'select', options: ['low', 'high', 'unable-to-assess'], default: 'low' }
+ ],
+ add: (patientId, payload) => Api.addAllergy(patientId, payload)
+ },
+ medication: {
+ title: 'Add Medication',
+ fields: [
+ { key: 'medicationName', label: 'Medication name', type: 'text', required: true },
+ { key: 'dosage', label: 'Dosage', type: 'text' },
+ { key: 'status', label: 'Status', type: 'select', options: ['active', 'completed', 'cancelled', 'on-hold', 'stopped'], default: 'active' },
+ { key: 'authoredOn', label: 'Authored on', type: 'date' }
+ ],
+ add: (patientId, payload) => Api.addMedication(patientId, payload)
+ },
+ encounter: {
+ title: 'Add Encounter',
+ fields: [
+ { key: 'encounterType', label: 'Encounter type', type: 'text', required: true, placeholder: 'e.g. Outpatient' },
+ { key: 'reason', label: 'Reason', type: 'text' },
+ { key: 'encounterDate', label: 'Date/time', type: 'datetime-local', required: true },
+ { key: 'status', label: 'Status', type: 'select', options: ['planned', 'in-progress', 'finished', 'cancelled'], default: 'in-progress' }
+ ],
+ add: (patientId, payload) => Api.addEncounter(patientId, payload)
+ }
+};
+
+let activeResourceType = null;
+let newPatientModal, addResourceModal;
+
+function openResourceModal(type) {
+ if (!currentPatientId) {
+ showToast('Select a patient first.', 'danger');
+ return;
+ }
+ activeResourceType = type;
+ const config = RESOURCE_FORMS[type];
+ document.getElementById('addResourceModalTitle').textContent = config.title;
+
+ const body = document.getElementById('addResourceModalBody');
+ body.innerHTML = config.fields.map(f => {
+ const id = `res_${f.key}`;
+ if (f.type === 'select') {
+ return `
+
+
+
`;
+ }
+ return `
+
+
+
`;
+ }).join('');
+
+ addResourceModal.show();
+}
+
+// ---------- Dashboard stats + resource browser ----------
+// Aggregates every patient's chart + care plans into flat arrays (each item
+// tagged with its owning patient) so the top stat cards can show real
+// counts, and clicking one can browse that resource across ALL patients —
+// not just the one currently selected.
+let resourceCache = { conditions: [], observations: [], allergies: [], medications: [], encounters: [], careplans: [] };
+let resourceBrowserModal;
+
+async function loadDashboardStats() {
+ resourceCache = { conditions: [], observations: [], allergies: [], medications: [], encounters: [], careplans: [] };
+
+ try {
+ const [chartResults, carePlanResults] = await Promise.all([
+ Promise.all(patientsCache.map(p => Api.getChart(p.id).catch(() => null))),
+ Promise.all(patientsCache.map(p => Api.getCarePlans(p.id).catch(() => [])))
+ ]);
+
+ patientsCache.forEach((p, idx) => {
+ const chart = chartResults[idx];
+ if (chart) {
+ chart.conditions.forEach(c => resourceCache.conditions.push({ ...c, patientId: p.id, patientName: p.fullName }));
+ chart.observations.forEach(o => resourceCache.observations.push({ ...o, patientId: p.id, patientName: p.fullName }));
+ chart.allergies.forEach(a => resourceCache.allergies.push({ ...a, patientId: p.id, patientName: p.fullName }));
+ chart.medications.forEach(m => resourceCache.medications.push({ ...m, patientId: p.id, patientName: p.fullName }));
+ chart.encounters.forEach(e => resourceCache.encounters.push({ ...e, patientId: p.id, patientName: p.fullName }));
+ }
+ (carePlanResults[idx] || []).forEach(cp => resourceCache.careplans.push({ ...cp, patientId: p.id, patientName: p.fullName }));
+ });
+ } catch {
+ // Stats are a nice-to-have on top of the core dashboard — don't block
+ // the rest of the page if aggregation fails for some reason.
+ }
+
+ updateStatCardsForContext();
+}
+
+// Returns the patients matching the current search box text (or everyone,
+// if the box is empty). Shared by the sidebar list and the stat cards so
+// they always agree on "what's currently in view".
+function getFilteredPatients() {
+ const term = patientSearchTerm.trim().toLowerCase();
+ if (!term) return patientsCache;
+ return patientsCache.filter(p =>
+ (p.fullName || '').toLowerCase().includes(term) ||
+ (p.id || '').toLowerCase().includes(term) ||
+ (p.medicalRecordNumber || '').toLowerCase().includes(term));
+}
+
+// Drives the top stat cards. Three states:
+// 1. A patient is selected (currentPatientId set) -> counts for just them.
+// 2. Nothing selected but the search box has text -> counts for the
+// patients currently matching the search.
+// 3. Neither -> counts across everyone.
+function updateStatCardsForContext() {
+ const setCount = (id, value) => { const el = document.getElementById(id); if (el) el.textContent = value; };
+ const scopeLabel = document.getElementById('statsScopeLabel');
+
+ let activePatients;
+ let scopeText = null;
+
+ if (currentPatientId) {
+ const selected = patientsCache.find(p => p.id === currentPatientId);
+ activePatients = selected ? [selected] : [];
+ if (selected) scopeText = `Showing data for ${escapeHtml(selected.fullName)}`;
+ } else {
+ activePatients = getFilteredPatients();
+ if (patientSearchTerm.trim()) {
+ scopeText = `Showing ${activePatients.length} of ${patientsCache.length} patients matching "${escapeHtml(patientSearchTerm.trim())}"`;
+ }
+ }
+
+ const activeIds = new Set(activePatients.map(p => p.id));
+
+ setCount('stat_patients', activePatients.length);
+ setCount('stat_conditions', resourceCache.conditions.filter(c => activeIds.has(c.patientId)).length);
+ setCount('stat_medications', resourceCache.medications.filter(m => activeIds.has(m.patientId)).length);
+ setCount('stat_allergies', resourceCache.allergies.filter(a => activeIds.has(a.patientId)).length);
+ setCount('stat_observations', resourceCache.observations.filter(o => activeIds.has(o.patientId)).length);
+ setCount('stat_encounters', resourceCache.encounters.filter(e => activeIds.has(e.patientId)).length);
+ setCount('stat_careplans', resourceCache.careplans.filter(cp => activeIds.has(cp.patientId)).length);
+
+ if (scopeLabel) {
+ if (scopeText) {
+ scopeLabel.innerHTML = `${scopeText} · Show all patients`;
+ scopeLabel.classList.remove('d-none');
+ } else {
+ scopeLabel.innerHTML = '';
+ scopeLabel.classList.add('d-none');
+ }
+ }
+}
+
+function patientChipHtml(patientId, name) {
+ return `👤 ${escapeHtml(name)}`;
+}
+
+const RESOURCE_BROWSER_CONFIG = {
+ patients: {
+ title: 'All Patients',
+ columns: ['Name', 'Age', 'Gender', 'MRN'],
+ rows: () => patientsCache.map(p => [
+ patientChipHtml(p.id, p.fullName), p.age, escapeHtml(p.gender), escapeHtml(p.medicalRecordNumber)
+ ])
+ },
+ conditions: {
+ title: 'All Conditions',
+ columns: ['Patient', 'Display', 'Code', 'Status', 'Onset'],
+ rows: () => resourceCache.conditions.map(c => [
+ patientChipHtml(c.patientId, c.patientName), escapeHtml(c.display), escapeHtml(c.code),
+ escapeHtml(c.clinicalStatus), fmtDate(c.onsetDate)
+ ])
+ },
+ medications: {
+ title: 'All Medications',
+ columns: ['Patient', 'Medication', 'Dosage', 'Status', 'Authored'],
+ rows: () => resourceCache.medications.map(m => [
+ patientChipHtml(m.patientId, m.patientName), escapeHtml(m.medicationName), escapeHtml(m.dosage),
+ escapeHtml(m.status), fmtDate(m.authoredOn)
+ ])
+ },
+ allergies: {
+ title: 'All Allergies',
+ columns: ['Patient', 'Substance', 'Reaction', 'Criticality'],
+ rows: () => resourceCache.allergies.map(a => [
+ patientChipHtml(a.patientId, a.patientName), escapeHtml(a.substance), escapeHtml(a.reaction), escapeHtml(a.criticality)
+ ])
+ },
+ observations: {
+ title: 'All Observations',
+ columns: ['Patient', 'Display', 'Value', 'Date'],
+ rows: () => resourceCache.observations.map(o => [
+ patientChipHtml(o.patientId, o.patientName), escapeHtml(o.display), `${o.value} ${escapeHtml(o.unit)}`, fmtDate(o.effectiveDate)
+ ])
+ },
+ encounters: {
+ title: 'All Encounters',
+ columns: ['Patient', 'Type', 'Reason', 'Date', 'Status'],
+ rows: () => resourceCache.encounters.map(e => [
+ patientChipHtml(e.patientId, e.patientName), escapeHtml(e.encounterType), escapeHtml(e.reason),
+ fmtDate(e.encounterDate), escapeHtml(e.status)
+ ])
+ },
+ careplans: {
+ title: 'All Care Plans',
+ columns: ['Patient', 'Title', 'Triggered By', 'Status', 'Created'],
+ rows: () => resourceCache.careplans.map(cp => [
+ patientChipHtml(cp.patientId, cp.patientName), escapeHtml(cp.title), escapeHtml(cp.triggeredByRule),
+ escapeHtml(cp.status), fmtDate(cp.createdDate)
+ ])
+ }
+};
+
+function openResourceBrowser(resourceType) {
+ const config = RESOURCE_BROWSER_CONFIG[resourceType];
+ if (!config) return;
+
+ document.getElementById('resourceBrowserTitle').textContent = config.title;
+ document.getElementById('resourceBrowserHead').innerHTML = config.columns.map(c => `${c} | `).join('');
+
+ const rows = config.rows();
+ const body = document.getElementById('resourceBrowserBody');
+ const emptyState = document.getElementById('resourceBrowserEmpty');
+
+ if (rows.length === 0) {
+ body.innerHTML = '';
+ emptyState.classList.remove('d-none');
+ } else {
+ emptyState.classList.add('d-none');
+ body.innerHTML = rows.map(cells => `${cells.map(c => `| ${c} | `).join('')}
`).join('');
+ }
+
+ resourceBrowserModal.show();
+}
+
+async function submitResourceForm(e) {
+ e.preventDefault();
+ const config = RESOURCE_FORMS[activeResourceType];
+ if (!config) return;
+
+ const payload = {};
+ for (const f of config.fields) {
+ const el = document.getElementById(`res_${f.key}`);
+ let val = el.value;
+ if (f.type === 'number') val = val === '' ? null : parseFloat(val);
+ payload[f.key] = val === '' ? null : val;
+ }
+
+ try {
+ showSpinner(true);
+ await config.add(currentPatientId, payload);
+ addResourceModal.hide();
+ showToast(`${config.title.replace('Add ', '')} added.`);
+ await loadChart(currentPatientId);
+ loadDashboardStats();
+ } catch (err) {
+ showToast(err.message, 'danger');
+ } finally {
+ showSpinner(false);
+ }
+}
+
+async function deleteResource(type, id) {
+ if (!currentPatientId || !confirm('Delete this item?')) return;
+ const deleteFns = {
+ condition: Api.deleteCondition,
+ observation: Api.deleteObservation,
+ allergy: Api.deleteAllergy,
+ medication: Api.deleteMedication,
+ encounter: Api.deleteEncounter
+ };
+ try {
+ showSpinner(true);
+ await deleteFns[type](currentPatientId, id);
+ showToast('Deleted.');
+ await loadChart(currentPatientId);
+ loadDashboardStats();
+ } catch (err) {
+ showToast(err.message, 'danger');
+ } finally {
+ showSpinner(false);
+ }
+}
+
+function setupDynamicDataControls() {
+ newPatientModal = new bootstrap.Modal(document.getElementById('newPatientModal'));
+ addResourceModal = new bootstrap.Modal(document.getElementById('addResourceModal'));
+ resourceBrowserModal = new bootstrap.Modal(document.getElementById('resourceBrowserModal'));
+
+ document.getElementById('newPatientBtn').addEventListener('click', () => newPatientModal.show());
+
+ // Live search: filters the cached patient list by name, patient ID, or
+ // MRN as the user types — no server round-trip needed.
+ document.getElementById('patientSearchInput')?.addEventListener('input', (e) => {
+ patientSearchTerm = e.target.value;
+ renderPatientList();
+ updateStatCardsForContext();
+ });
+
+ document.getElementById('newPatientForm').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const payload = {
+ firstName: document.getElementById('np_firstName').value,
+ lastName: document.getElementById('np_lastName').value,
+ dateOfBirth: document.getElementById('np_dob').value,
+ gender: document.getElementById('np_gender').value,
+ medicalRecordNumber: document.getElementById('np_mrn').value || null
+ };
+ try {
+ showSpinner(true);
+ const created = await Api.createPatient(payload);
+ newPatientModal.hide();
+ document.getElementById('newPatientForm').reset();
+ showToast('Patient created.');
+ await loadPatientList();
+ loadDashboardStats();
+ selectPatient(created.patient.id);
+ } catch (err) {
+ showToast(err.message, 'danger');
+ } finally {
+ showSpinner(false);
+ }
+ });
+
+ document.getElementById('addResourceForm').addEventListener('submit', submitResourceForm);
+
+ // Stat cards -> open the "browse this resource across all patients" modal.
+ document.getElementById('statsRow').addEventListener('click', (e) => {
+ const card = e.target.closest('.stat-card');
+ if (card) openResourceBrowser(card.dataset.resource);
+ });
+
+ // "Show all patients" link inside the stats scope label — clears both
+ // the search box and the selected patient, reverting the stat cards to
+ // totals across everyone.
+ document.addEventListener('click', (e) => {
+ if (e.target.id !== 'clearStatsScopeLink') return;
+ e.preventDefault();
+ patientSearchTerm = '';
+ const input = document.getElementById('patientSearchInput');
+ if (input) input.value = '';
+ currentPatientId = null;
+ sessionStorage.removeItem('cip_patientId');
+ document.getElementById('patientContent')?.classList.add('d-none');
+ document.getElementById('noPatientState')?.classList.remove('d-none');
+ renderPatientList();
+ updateStatCardsForContext();
+ });
+
+ // Inside the resource browser: clicking a patient chip jumps straight to
+ // that patient's Chart tab.
+ document.getElementById('resourceBrowserBody').addEventListener('click', (e) => {
+ const chip = e.target.closest('[data-jump-patient]');
+ if (!chip) return;
+ resourceBrowserModal.hide();
+ selectPatient(chip.dataset.jumpPatient);
+ document.querySelectorAll('#dashboardTabs .nav-link').forEach(b => b.classList.remove('active'));
+ document.querySelector('#dashboardTabs .nav-link[data-tab="chart"]')?.classList.add('active');
+ document.querySelectorAll('.tab-pane').forEach(p => p.classList.add('d-none'));
+ document.getElementById('tab-chart')?.classList.remove('d-none');
+ });
+
+ // Event delegation: "+ Add" buttons in the chart tab, and per-item delete
+ // buttons rendered inside the chart lists (see loadChart()).
+ document.getElementById('tab-chart').addEventListener('click', (e) => {
+ const addBtn = e.target.closest('[data-add]');
+ if (addBtn) { openResourceModal(addBtn.dataset.add); return; }
+
+ const delBtn = e.target.closest('[data-delete-type]');
+ if (delBtn) { deleteResource(delBtn.dataset.deleteType, parseInt(delBtn.dataset.deleteId, 10)); }
+ });
+}
+
+// ---------- Init ----------
+document.addEventListener('DOMContentLoaded', async () => {
+ const token = sessionStorage.getItem('cip_token');
+ if (!token) {
+ window.location.href = 'login.html';
+ return;
+ }
+
+ document.getElementById('clinicianLabel').textContent =
+ `👤 ${sessionStorage.getItem('cip_clinician') || 'Clinician'}`;
+
+ document.getElementById('logoutBtn').addEventListener('click', () => {
+ sessionStorage.clear();
+ window.location.href = 'index.html';
+ });
+
+ setupTabs();
+ setupRiskForm();
+ setupDynamicDataControls();
+ document.getElementById('evaluateCdsBtn').addEventListener('click', evaluateCds);
+ document.getElementById('refreshCarePlansBtn').addEventListener('click', () => loadCarePlans(currentPatientId));
+
+ await loadPatientList();
+ loadDashboardStats();
+
+ const launchPatientId = sessionStorage.getItem('cip_patientId');
+ if (launchPatientId) {
+ selectPatient(launchPatientId);
+ }
+});
+
+function setupTabs() {
+ document.querySelectorAll('#dashboardTabs .nav-link').forEach(btn => {
+ btn.addEventListener('click', () => {
+ document.querySelectorAll('#dashboardTabs .nav-link').forEach(b => b.classList.remove('active'));
+ btn.classList.add('active');
+ document.querySelectorAll('.tab-pane').forEach(p => p.classList.add('d-none'));
+ document.getElementById(`tab-${btn.dataset.tab}`).classList.remove('d-none');
+ });
+ });
+}
+
+// ---------- Patients ----------
+async function loadPatientList() {
+ try {
+ showSpinner(true);
+ patientsCache = await Api.getPatients();
+ renderPatientList();
+ } catch (err) {
+ showToast(err.message, 'danger');
+ } finally {
+ showSpinner(false);
+ }
+}
+
+function renderPatientList() {
+ const container = document.getElementById('patientList');
+ const emptyState = document.getElementById('patientSearchEmpty');
+ const emptyTerm = document.getElementById('patientSearchEmptyTerm');
+
+ const term = patientSearchTerm.trim();
+ const filtered = getFilteredPatients();
+
+ if (filtered.length === 0 && term) {
+ container.innerHTML = '';
+ container.classList.add('d-none');
+ if (emptyTerm) emptyTerm.textContent = patientSearchTerm.trim();
+ emptyState?.classList.remove('d-none');
+ return;
+ }
+ container.classList.remove('d-none');
+ emptyState?.classList.add('d-none');
+
+ container.innerHTML = filtered.map(p => `
+
+
+
${escapeHtml(p.fullName)}
+
${p.age} yrs · ${escapeHtml(p.gender)} · ${escapeHtml(p.medicalRecordNumber)}
+
ID: ${escapeHtml(p.id)}
+
+
+
+ `).join('');
+
+ container.querySelectorAll('.patient-tile').forEach(tile => {
+ tile.addEventListener('click', (e) => {
+ if (e.target.closest('[data-delete-patient]')) return;
+ selectPatient(tile.dataset.id);
+ });
+ });
+
+ container.querySelectorAll('[data-delete-patient]').forEach(btn => {
+ btn.addEventListener('click', async (e) => {
+ e.stopPropagation();
+ const id = btn.dataset.deletePatient;
+ if (!confirm('Delete this patient and all their chart data?')) return;
+ try {
+ showSpinner(true);
+ await Api.deletePatient(id);
+ showToast('Patient deleted.');
+ if (currentPatientId === id) {
+ currentPatientId = null;
+ sessionStorage.removeItem('cip_patientId');
+ document.getElementById('patientContent').classList.add('d-none');
+ document.getElementById('noPatientState').classList.remove('d-none');
+ }
+ await loadPatientList();
+ loadDashboardStats();
+ } catch (err) {
+ showToast(err.message, 'danger');
+ } finally {
+ showSpinner(false);
+ }
+ });
+ });
+}
+
+async function selectPatient(id) {
+ currentPatientId = id;
+ sessionStorage.setItem('cip_patientId', id);
+ renderPatientList();
+ updateStatCardsForContext();
+
+ document.getElementById('noPatientState').classList.add('d-none');
+ document.getElementById('patientContent').classList.remove('d-none');
+
+ resetRiskTab();
+ resetCdsTab();
+
+ await loadChart(id);
+ await loadCarePlans(id);
+}
+
+// ---------- Chart ----------
+async function loadChart(id) {
+ try {
+ showSpinner(true);
+ const chart = await Api.getChart(id);
+
+ document.getElementById('patientName').textContent = chart.fullName;
+ document.getElementById('patientMeta').textContent =
+ `${chart.age} yrs · ${chart.gender} · ${chart.medicalRecordNumber} · Patient ID: ${chart.patientId}`;
+
+ document.getElementById('conditionsList').innerHTML = chart.conditions.length
+ ? chart.conditions.map(c => `
+
+
+
${escapeHtml(c.display)}
+
${escapeHtml(c.code)} · ${escapeHtml(c.clinicalStatus)} · onset ${fmtDate(c.onsetDate)}
+
+
+
`).join('')
+ : 'No active conditions on file.
';
+
+ document.getElementById('allergiesList').innerHTML = chart.allergies.length
+ ? chart.allergies.map(a => `
+
+
+
${escapeHtml(a.substance)}
+
Reaction: ${escapeHtml(a.reaction)} · Criticality: ${escapeHtml(a.criticality)}
+
+
+
`).join('')
+ : 'No known allergies (NKA).
';
+
+ document.getElementById('medicationsList').innerHTML = chart.medications.length
+ ? chart.medications.map(m => `
+
+
+
${escapeHtml(m.medicationName)}
+
${escapeHtml(m.dosage)} · ${escapeHtml(m.status)} · authored ${fmtDate(m.authoredOn)}
+
+
+
`).join('')
+ : 'No active medications.
';
+
+ document.getElementById('observationsList').innerHTML = chart.observations.length
+ ? chart.observations.map(o => `
+
+
+
${escapeHtml(o.display)}: ${o.value} ${escapeHtml(o.unit)}
+
${fmtDate(o.effectiveDate)}
+
+
+
`).join('')
+ : 'No recent observations.
';
+
+ document.getElementById('encountersList').innerHTML = chart.encounters.length
+ ? chart.encounters.map(e => `
+
+
+
${escapeHtml(e.encounterType)} — ${escapeHtml(e.reason)}
+
${fmtDate(e.encounterDate)} · ${escapeHtml(e.status)}
+
+
+
`).join('')
+ : 'No encounters on file.
';
+
+ document.getElementById('validationNotes').innerHTML = chart.validationNotes.length
+ ? chart.validationNotes.map(n => `✓ ${escapeHtml(n)}
`).join('')
+ : 'No normalization actions were required.
';
+
+ } catch (err) {
+ showToast(err.message, 'danger');
+ } finally {
+ showSpinner(false);
+ }
+}
+
+// ---------- Risk ----------
+function resetRiskTab() {
+ document.getElementById('riskForm').reset();
+ document.getElementById('q_pain_val').textContent = '0';
+ document.getElementById('riskResultEmpty').classList.remove('d-none');
+ document.getElementById('riskResultBody').classList.add('d-none');
+}
+
+function setupRiskForm() {
+ const painInput = document.getElementById('q_pain');
+ painInput.addEventListener('input', () => {
+ document.getElementById('q_pain_val').textContent = painInput.value;
+ });
+
+ document.getElementById('riskForm').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ if (!currentPatientId) return;
+
+ const questionnaire = {
+ smoker: document.getElementById('q_smoker').checked,
+ familyHistoryHeartDisease: document.getElementById('q_family').checked,
+ priorHospitalization: document.getElementById('q_hosp').checked,
+ bmi: document.getElementById('q_bmi').value ? parseFloat(document.getElementById('q_bmi').value) : null,
+ painScore: parseInt(document.getElementById('q_pain').value || '0', 10)
+ };
+
+ try {
+ showSpinner(true);
+ const result = await Api.assessRisk(currentPatientId, questionnaire);
+ renderRiskResult(result);
+ showToast('Risk assessment complete.');
+ } catch (err) {
+ showToast(err.message, 'danger');
+ } finally {
+ showSpinner(false);
+ }
+ });
+}
+
+function renderRiskResult(result) {
+ document.getElementById('riskResultEmpty').classList.add('d-none');
+ document.getElementById('riskResultBody').classList.remove('d-none');
+
+ const gauge = document.getElementById('scoreGauge');
+ gauge.textContent = result.score;
+ gauge.style.background = result.riskLevel === 'High'
+ ? 'radial-gradient(circle, #e74c3c, #c0392b)'
+ : 'radial-gradient(circle, #27ae60, #1e8449)';
+
+ const badge = document.getElementById('riskLevelBadge');
+ badge.textContent = `${result.riskLevel} Risk`;
+ badge.className = `risk-badge ${result.riskLevel}`;
+
+ document.getElementById('riskFactorsList').innerHTML = result.contributingFactors
+ .map(f => `${escapeHtml(f)}`).join('');
+}
+
+// ---------- CDS ----------
+function resetCdsTab() {
+ document.getElementById('cdsAlertsContainer').innerHTML =
+ 'Click "Evaluate Encounter" to screen for drug-allergy conflicts and guideline gaps.
';
+ document.getElementById('alertCountBadge').classList.add('d-none');
+}
+
+async function evaluateCds() {
+ if (!currentPatientId) return;
+ try {
+ showSpinner(true);
+ const result = await Api.evaluateCds(currentPatientId);
+ renderCdsAlerts(result);
+ } catch (err) {
+ showToast(err.message, 'danger');
+ } finally {
+ showSpinner(false);
+ }
+}
+
+function renderCdsAlerts(result) {
+ const container = document.getElementById('cdsAlertsContainer');
+ const badge = document.getElementById('alertCountBadge');
+
+ if (!result.hasAlerts) {
+ container.innerHTML = '✅
Nothing triggered — no CDS alerts for this encounter.
';
+ badge.classList.add('d-none');
+ return;
+ }
+
+ badge.textContent = result.alerts.length;
+ badge.classList.remove('d-none');
+
+ container.innerHTML = result.alerts.map(a => `
+
+
+ ${escapeHtml(a.severity)}
+ ${escapeHtml(a.ruleId)}
+
+
${escapeHtml(a.summary)}
+
${escapeHtml(a.recommendation)}
+
+
+ `).join('');
+
+ container.querySelectorAll('.create-careplan-btn').forEach(btn => {
+ btn.addEventListener('click', () => createCarePlanFromAlert(parseInt(btn.dataset.alertId, 10)));
+ });
+}
+
+async function createCarePlanFromAlert(alertId) {
+ try {
+ showSpinner(true);
+ await Api.createCarePlan({
+ patientId: currentPatientId,
+ alertId,
+ createServiceRequest: true,
+ priority: 'urgent'
+ });
+ showToast('Care Plan and Service Request created and written back to the FHIR store.');
+
+ document.querySelector(`.alert-card[data-alert-id="${alertId}"]`)?.remove();
+ const badge = document.getElementById('alertCountBadge');
+ const remaining = document.querySelectorAll('.alert-card').length;
+ if (remaining === 0) {
+ resetCdsTab();
+ document.getElementById('cdsAlertsContainer').innerHTML =
+ '✅
All alerts for this encounter have been actioned.
';
+ } else {
+ badge.textContent = remaining;
+ }
+
+ await loadCarePlans(currentPatientId);
+ } catch (err) {
+ showToast(err.message, 'danger');
+ } finally {
+ showSpinner(false);
+ }
+}
+
+// ---------- Care Plans ----------
+async function loadCarePlans(patientId) {
+ if (!patientId) return;
+ try {
+ const carePlans = await Api.getCarePlans(patientId);
+ const tbody = document.getElementById('carePlansTableBody');
+
+ tbody.innerHTML = carePlans.length
+ ? carePlans.map(cp => `
+
+ | ${escapeHtml(cp.title)} |
+ ${escapeHtml(cp.triggeredByRule)} |
+ ${escapeHtml(cp.status)} |
+ ${fmtDate(cp.createdDate)} |
+
`).join('')
+ : '| No care plans yet. |
';
+ } catch (err) {
+ showToast(err.message, 'danger');
+ }
+}