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 = `
+
${message}
+ +
`; + 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'); + } +}