Files
FHIR-Sandbox/app/templates/dashboard.html
2026-02-20 23:59:46 -05:00

474 lines
22 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{template "base.html" .}}
{{define "nav"}}
<div class="flex items-center gap-4">
<a href="/patients" class="nav-link">All Patients</a>
<a href="/" class="nav-link">Home</a>
{{if .Practitioner}}
<div class="practitioner-mini-card">
<div class="avatar avatar-sm">
{{if .Practitioner.FirstName}}{{slice .Practitioner.FirstName 0 1}}{{end}}{{if .Practitioner.LastName}}{{slice .Practitioner.LastName 0 1}}{{end}}
</div>
<span class="text-xs font-medium">Dr. {{.Practitioner.LastName}}</span>
</div>
{{end}}
<form action="/logout" method="POST" class="flex items-center">
<button class="btn btn-danger btn-sm" type="submit">Logout</button>
</form>
</div>
{{end}}
{{define "content"}}
{{/* ---- Flash Messages ---- */}}
{{if .Synced}}
<div class="flash-message mb-6">
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
<div>
<strong class="font-medium text-main">Sync complete</strong>
<span class="text-sm text-muted">Data has been refreshed from the EHR.</span>
</div>
</div>
<button onclick="this.parentElement.style.display='none';" class="btn btn-icon text-muted">&times;</button>
</div>
{{end}}
{{if .Patient}}
<div class="flex flex-col gap-6 pt-4">
{{/* ---- Top Header: Patient Banner ---- */}}
<div class="panel panel-padded flex items-center justify-between border-l-brand">
<div class="flex items-center gap-6 w-full">
<div class="avatar avatar-lg">
{{if .Patient.FirstName}}{{slice .Patient.FirstName 0 1}}{{end}}{{if .Patient.LastName}}{{slice .Patient.LastName 0 1}}{{end}}
</div>
<div class="flex flex-col gap-1 w-full">
<div class="flex items-center gap-3">
<h1 class="page-title m-0">
{{.Patient.LastName}}, {{.Patient.FirstName}} {{.Patient.MiddleName}}
</h1>
<span class="mrn-badge">MRN: {{orDash .Patient.MRN}}</span>
</div>
<div class="patient-meta-strip">
<span class="meta-item"><strong>DOB:</strong> {{formatDate .Patient.DOB}} ({{calculateAge .Patient.DOB}} yrs)</span>
<span class="meta-item"><strong>Gender:</strong> {{titleCase .Patient.Gender}}</span>
{{with .Patient.Email}}<span class="meta-item"><strong>Email:</strong> {{.}}</span>{{end}}
{{with primaryPhone .RawPatient}}<span class="meta-item"><strong>Phone:</strong> {{.}}</span>{{end}}
</div>
</div>
<div class="flex flex-col items-end gap-2 shrink-0">
<form action="/dashboard/sync?patient_id={{.Patient.FHIRID}}" method="POST" class="flex items-center">
<button class="btn btn-primary" type="submit" id="syncBtn" onclick="onSyncClick()">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 4v6h-6"/><path d="M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
Refresh Chart
</button>
</form>
<span class="text-xs text-muted">
Last Synced: {{if .LatestSync}}{{formatDateTime .LatestSync.SyncedAt}}{{else}}Never{{end}}
</span>
</div>
</div>
</div>
<div class="dashboard-layout">
{{/* ---- Main Content Column ---- */}}
<div class="dashboard-main">
{{/* Clinical Summary Stats */}}
<div class="grid grid-cols-3 gap-4 lg:grid-cols-1">
<div class="panel panel-padded stat-card">
<div class="stat-value text-blue">{{.Summary.ActiveCondCount}}</div>
<div class="stat-label">Active Conditions</div>
</div>
<div class="panel panel-padded stat-card">
<div class="stat-value text-success">{{.Summary.ActiveMedCount}}</div>
<div class="stat-label">Active Medications</div>
</div>
<div class="panel panel-padded stat-card {{if gt .Summary.AbnormalLabCount 0}}danger{{end}}">
<div class="stat-value {{if gt .Summary.AbnormalLabCount 0}}text-danger{{end}}">{{.Summary.AbnormalLabCount}}</div>
<div class="stat-label">Abnormal Labs (30d)</div>
</div>
</div>
{{/* Clinical Data Tabs */}}
<div class="panel p-0 overflow-hidden">
<div class="bg-panel px-4 pt-4 border-b border-hard">
<div class="tabs">
<button class="tab-btn active" onclick="openTab(event, 'summary')">Summary</button>
<button class="tab-btn" onclick="openTab(event, 'vitals')">Vitals ({{len (latestObPerCode (filterObsByCategory .Observations "vital-signs"))}})</button>
<button class="tab-btn" onclick="openTab(event, 'labs')">Labs ({{len (filterObsByCategory .Observations "laboratory")}})</button>
<button class="tab-btn" onclick="openTab(event, 'conditions')">Conditions ({{len .Conditions}})</button>
<button class="tab-btn" onclick="openTab(event, 'medications')">Meds ({{len .Medications}})</button>
<button class="tab-btn" onclick="openTab(event, 'allergies')">Allergies ({{len .Allergies}})</button>
<button class="tab-btn" onclick="openTab(event, 'notes')">Notes ({{len .DocumentReferences}})</button>
<button class="tab-btn" onclick="openTab(event, 'more')" title="More Data">...</button>
</div>
</div>
<div class="panel-padded min-h-[400px]">
{{/* ---- Summary Tab ---- */}}
<div id="summary" class="tab-content active">
<div class="flex flex-col gap-8">
<div class="flex flex-col gap-4">
<h3 class="text-xs text-muted font-medium uppercase tracking-wider">Latest Vital Signs</h3>
{{if .Summary.LatestVitals}}
<div class="vitals-strip">
{{range .Summary.LatestVitals}}
<div class="vital-item {{if isAbnormal .Interpretation}}vital-abnormal{{end}}">
<span class="vital-label" title="{{.CodeText}}">{{orDash .CodeText}}</span>
<span class="vital-value">
{{if .ValueQuantity}}{{printf "%.4g" (derefFloat64 .ValueQuantity)}} <small>{{.ValueUnit}}</small>
{{else if .ValueString}}{{.ValueString}}
{{else}}—{{end}}
</span>
<span class="vital-date">{{formatDate .EffectiveDate}}</span>
</div>
{{end}}
</div>
{{else}}
<p class="empty-state">No vital signs recorded.</p>
{{end}}
</div>
<div class="flex flex-col gap-4">
<h3 class="text-xs text-muted font-medium uppercase tracking-wider">Recent Abnormal Labs</h3>
{{if .Summary.AbnormalLabsRecent}}
<div class="table-wrapper">
<table>
<thead>
<tr><th>Date</th><th>Test</th><th>Result</th><th>Flag</th></tr>
</thead>
<tbody>
{{range .Summary.AbnormalLabsRecent}}
<tr class="row-abnormal">
<td>{{formatDate .EffectiveDate}}</td>
<td class="font-medium">{{.CodeText}}</td>
<td>{{if .ValueQuantity}}{{printf "%.4g" (derefFloat64 .ValueQuantity)}} {{.ValueUnit}}{{else}}{{.ValueString}}{{end}}</td>
<td><span class="badge badge-danger">{{.Interpretation}}</span></td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<p class="empty-state">No abnormal labs in the last 30 days.</p>
{{end}}
</div>
</div>
</div>
{{/* ---- Vitals Tab ---- */}}
<div id="vitals" class="tab-content">
{{$vitals := latestObPerCode (filterObsByCategory .Observations "vital-signs")}}
{{if $vitals}}
<div class="vitals-grid">
{{range $vitals}}
<div class="panel panel-padded p-5 {{if isAbnormal .Interpretation}}vital-abnormal{{end}}">
<div class="flex justify-between items-start mb-2">
<span class="text-xs font-semibold text-muted">{{orDash .CodeText}}</span>
{{if isAbnormal .Interpretation}}<span class="badge badge-danger">{{.Interpretation}}</span>{{end}}
</div>
<div class="text-2xl font-semibold mb-4">
{{if .ValueQuantity}}{{printf "%.4g" (derefFloat64 .ValueQuantity)}} <span class="text-sm font-medium text-muted">{{.ValueUnit}}</span>
{{else if .ValueString}}{{.ValueString}}
{{else}}—{{end}}
</div>
<div class="flex flex-col gap-1 text-xs text-muted">
<span>{{formatDate .EffectiveDate}}</span>
{{if or .ReferenceRangeLow .ReferenceRangeHigh}}
<span>Ref: {{if .ReferenceRangeLow}}{{printf "%.4g" (derefFloat64 .ReferenceRangeLow)}}{{else}}—{{end}}-{{if .ReferenceRangeHigh}}{{printf "%.4g" (derefFloat64 .ReferenceRangeHigh)}}{{else}}—{{end}}</span>
{{end}}
</div>
</div>
{{end}}
</div>
{{else}}
<p class="empty-state">No vital signs found.</p>
{{end}}
</div>
{{/* ---- Labs Tab ---- */}}
<div id="labs" class="tab-content">
{{$labs := filterObsByCategory .Observations "laboratory"}}
{{if $labs}}
<div class="table-wrapper">
<table>
<thead>
<tr><th>Date</th><th>Test</th><th>Result</th><th>Ref Range</th><th>Flag</th></tr>
</thead>
<tbody>
{{range $labs}}
<tr {{if isAbnormal .Interpretation}}class="row-abnormal"{{end}}>
<td>{{formatDate .EffectiveDate}}</td>
<td class="font-medium">{{orDash .CodeText}}</td>
<td>
{{if .ValueQuantity}}<strong>{{printf "%.4g" (derefFloat64 .ValueQuantity)}}</strong> {{.ValueUnit}}
{{else if .ValueString}}{{.ValueString}}
{{else}}—{{end}}
</td>
<td class="text-xs text-muted">
{{if or .ReferenceRangeLow .ReferenceRangeHigh}}
[{{if .ReferenceRangeLow}}{{printf "%.4g" (derefFloat64 .ReferenceRangeLow)}}{{else}}—{{end}} {{if .ReferenceRangeHigh}}{{printf "%.4g" (derefFloat64 .ReferenceRangeHigh)}}{{else}}—{{end}}]
{{else}}—{{end}}
</td>
<td>{{if isAbnormal .Interpretation}}<span class="badge badge-danger">{{.Interpretation}}</span>{{else if .Interpretation}}<span class="badge badge-neutral">{{.Interpretation}}</span>{{end}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<p class="empty-state">No lab results found.</p>
{{end}}
</div>
{{/* ---- Conditions Tab ---- */}}
<div id="conditions" class="tab-content">
{{if .Conditions}}
<div class="table-wrapper">
<table>
<thead>
<tr><th>Onset</th><th>Condition</th><th>Status</th><th>Verification</th></tr>
</thead>
<tbody>
{{range .Conditions}}
<tr>
<td>{{formatDate (or .OnsetDate .RecordedDate)}}</td>
<td class="font-medium">{{orDash .CodeText}}</td>
<td>{{if eq .ClinicalStatus "active"}}<span class="badge badge-success">Active</span>{{else}}<span class="badge badge-neutral">{{.ClinicalStatus}}</span>{{end}}</td>
<td>{{titleCase .VerificationStatus}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<p class="empty-state">No conditions found.</p>
{{end}}
</div>
{{/* ---- Medications Tab ---- */}}
<div id="medications" class="tab-content">
{{if .Medications}}
<div class="table-wrapper">
<table>
<thead>
<tr><th>Authored</th><th>Medication</th><th>Dosage</th><th>Status</th></tr>
</thead>
<tbody>
{{range .Medications}}
<tr>
<td>{{formatDate .AuthoredOn}}</td>
<td class="font-medium">{{orDash .MedCodeText}}</td>
<td>{{orDash .DosageText}}</td>
<td>{{if eq .Status "active"}}<span class="badge badge-success">Active</span>{{else}}<span class="badge badge-neutral">{{.Status}}</span>{{end}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<p class="empty-state">No medications found.</p>
{{end}}
</div>
{{/* ---- Allergies Tab ---- */}}
<div id="allergies" class="tab-content">
{{if .Allergies}}
<div class="table-wrapper">
<table>
<thead>
<tr><th>Allergen</th><th>Reaction</th><th>Severity</th><th>Status</th></tr>
</thead>
<tbody>
{{range .Allergies}}
<tr>
<td class="font-medium">{{orDash .CodeText}}</td>
<td>{{orDash .ReactionManifestation}}</td>
<td>{{if eq .ReactionSeverity "severe"}}<span class="badge badge-danger">Severe</span>{{else if .ReactionSeverity}}<span class="badge badge-warning">{{.ReactionSeverity}}</span>{{else}}—{{end}}</td>
<td>{{titleCase .ClinicalStatus}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<p class="empty-state">No allergies found.</p>
{{end}}
</div>
{{/* ---- Notes Tab ---- */}}
<div id="notes" class="tab-content">
{{if .DocumentReferences}}
<div class="notes-list">
{{range .DocumentReferences}}
<div class="panel panel-padded note-item">
<div class="note-header">
<span class="note-date">{{formatDate .Date}}</span>
<span class="note-type">{{orDash .TypeText}}</span>
{{if .ContentURL}}<a href="{{.ContentURL}}" target="_blank" class="btn btn-outline btn-sm">View Document</a>{{end}}
</div>
<p class="note-desc">{{orDash .Description}}</p>
</div>
{{end}}
</div>
{{else}}
<p class="empty-state">No clinical notes found.</p>
{{end}}
</div>
{{/* ---- More Tab (Procedures, Immunizations, Encounters) ---- */}}
<div id="more" class="tab-content">
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-2">
<h4 class="font-semibold text-sm text-main">Recent Procedures</h4>
{{if .Procedures}}
<ul class="flex flex-col gap-1 text-sm text-muted">
{{range (slice .Procedures 0 (min (len .Procedures) 5))}}
<li><strong class="text-main">{{formatDate .PerformedDate}}:</strong> {{.CodeText}}</li>
{{end}}
</ul>
{{else}}<p class="text-muted text-xs">None recorded</p>{{end}}
</div>
<div class="flex flex-col gap-2">
<h4 class="font-semibold text-sm text-main">Recent Immunizations</h4>
{{if .Immunizations}}
<ul class="flex flex-col gap-1 text-sm text-muted">
{{range (slice .Immunizations 0 (min (len .Immunizations) 5))}}
<li><strong class="text-main">{{formatDate .OccurrenceDate}}:</strong> {{.VaccineText}}</li>
{{end}}
</ul>
{{else}}<p class="text-muted text-xs">None recorded</p>{{end}}
</div>
<div class="flex flex-col gap-2">
<h4 class="font-semibold text-sm text-main">Recent Encounters</h4>
{{if .Encounters}}
<ul class="flex flex-col gap-1 text-sm text-muted">
{{range (slice .Encounters 0 (min (len .Encounters) 5))}}
<li><strong class="text-main">{{formatDate .PeriodStart}}:</strong> {{or .TypeText "Visit"}}</li>
{{end}}
</ul>
{{else}}<p class="text-muted text-xs">None recorded</p>{{end}}
</div>
</div>
</div>
</div>
</div>
</div>
{{/* ---- Sidebar Column ---- */}}
<div class="dashboard-sidebar">
{{/* High Priority: Alerts */}}
{{if hasCriticalAllergies .Allergies}}
<div class="panel panel-padded alert-card">
<div class="flex items-center gap-2 mb-4">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-danger"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
<h3 class="font-semibold text-danger">Critical Alerts</h3>
</div>
<div class="flex flex-col gap-3">
{{range .Allergies}}
{{if eq .Criticality "high"}}
<div class="alert-item">
<span class="alert-code">{{.CodeText}} Allergy</span>
{{with .ReactionManifestation}}<span class="alert-detail">{{.}}{{with $.ReactionSeverity}} ({{.}}){{end}}</span>{{end}}
</div>
{{end}}
{{end}}
</div>
</div>
{{end}}
{{/* Patient Details (Expanded) */}}
<div class="panel panel-padded">
<div class="mb-4">
<h3 class="font-semibold text-main">Patient Information</h3>
</div>
<div class="kv-list">
<div class="kv-row">
<span class="kv-label">Full Name</span>
<span class="kv-value font-medium">{{.Patient.FirstName}} {{.Patient.MiddleName}} {{.Patient.LastName}}</span>
</div>
<div class="kv-row">
<span class="kv-label">FHIR ID</span>
<span class="kv-value text-xs font-mono">{{.Patient.FHIRID}}</span>
</div>
{{with primaryAddress .RawPatient}}
<div class="kv-row">
<span class="kv-label">Address</span>
<span class="kv-value">{{.}}</span>
</div>
{{end}}
{{with usRace .RawPatient}}
<div class="kv-row">
<span class="kv-label">Race</span>
<span class="kv-value">{{.}}</span>
</div>
{{end}}
{{with usEthnicity .RawPatient}}
<div class="kv-row">
<span class="kv-label">Ethnicity</span>
<span class="kv-value">{{.}}</span>
</div>
{{end}}
</div>
</div>
{{/* Low Priority: SMART Inspector */}}
<div class="panel panel-padded inspector-card">
<details>
<summary class="text-xs text-muted font-medium uppercase tracking-wider cursor-pointer outline-none">System Inspector</summary>
<div class="inspector-content mt-4">
<div class="inspect-item">
<span class="label">EHR Server</span>
<code class="text-xs text-muted break-all" title="{{.Session.EHRURL}}">{{.Session.EHRURL}}</code>
</div>
<div class="inspect-item">
<span class="label">Granted Scopes</span>
<div class="scopes-cloud">
{{range (split .Session.Scope " ")}}
<span class="scope-tag">{{.}}</span>
{{end}}
</div>
</div>
</div>
</details>
</div>
</div>
</div>
</div>
{{end}}
{{end}}
{{define "scripts"}}
<script>
function openTab(evt, tabName) {
var tabcontent = document.getElementsByClassName("tab-content");
for (var i = 0; i < tabcontent.length; i++) {
tabcontent[i].classList.remove("active");
}
var tablinks = document.getElementsByClassName("tab-btn");
for (var i = 0; i < tablinks.length; i++) {
tablinks[i].classList.remove("active");
}
var target = document.getElementById(tabName);
setTimeout(() => target.classList.add("active"), 10);
evt.currentTarget.classList.add("active");
}
function onSyncClick() {
var btn = document.getElementById("syncBtn");
setTimeout(function() {
btn.disabled = true;
btn.innerHTML = `<svg class="animate-spin" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg> Syncing...`;
}, 50);
}
// Helper for splitting scopes string
function split(s, sep) { return s.split(sep); }
</script>
{{end}}