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

436 lines
16 KiB
HTML
Raw 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"}}
<a href="/patients" class="nav-link">All Patients</a>
<a href="/" class="nav-link">Home</a>
<form action="/logout" method="POST" style="display:inline">
<button class="btn btn-danger" type="submit">Logout</button>
</form>
{{end}}
{{define "content"}}
{{/* ---- Flash Messages ---- */}}
{{if .Synced}}
<div class="flash-message flash-success">
<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>Sync complete</strong>
<span class="text-sm">Data has been refreshed from the EHR.</span>
</div>
</div>
<button onclick="this.parentElement.style.display='none';" style="background:none;border:none;cursor:pointer;font-size:1.2rem;color:inherit;">&times;</button>
</div>
{{end}}
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
{{/* ---- Left Column: Patient Context ---- */}}
<div class="md:col-span-1">
{{/* Practitioner Card */}}
{{if .Practitioner}}
<div class="card">
<div class="card-header">
<h3 class="card-title">Clinician</h3>
<span class="badge badge-neutral">Active Session</span>
</div>
<div class="flex items-center gap-4 mb-4">
<div class="avatar avatar-practitioner">
{{if .Practitioner.FirstName}}{{slice .Practitioner.FirstName 0 1}}{{end}}{{if .Practitioner.LastName}}{{slice .Practitioner.LastName 0 1}}{{end}}
</div>
<div>
<h4 class="font-bold">
{{if .Practitioner.FirstName}}{{.Practitioner.FirstName}} {{end}}
{{if .Practitioner.MiddleName}}{{.Practitioner.MiddleName}} {{end}}
{{if .Practitioner.LastName}}{{.Practitioner.LastName}}{{end}}
</h4>
<span class="text-sm text-muted">
{{if eq .Practitioner.Role "practitioner"}}Health Care Practitioner{{else}}Patient{{end}}
</span>
</div>
</div>
</div>
{{end}}
{{/* Patient Card */}}
{{if .Patient}}
<div class="card">
<div class="card-header">
<h3 class="card-title">Patient</h3>
<span class="badge badge-success">Active Context</span>
</div>
<div class="flex items-center gap-4 mb-4">
<div class="avatar avatar-patient">
{{if .Patient.FirstName}}{{slice .Patient.FirstName 0 1}}{{end}}{{if .Patient.LastName}}{{slice .Patient.LastName 0 1}}{{end}}
</div>
<div>
<h2 class="font-bold text-lg">
{{if .Patient.FirstName}}{{.Patient.FirstName}} {{end}}
{{if .Patient.MiddleName}}{{.Patient.MiddleName}} {{end}}
{{if .Patient.LastName}}{{.Patient.LastName}}{{end}}
</h2>
<span class="text-sm text-muted">ID: {{.Patient.FHIRID}}</span>
</div>
</div>
<div class="details-list" style="grid-template-columns: 1fr;">
<div class="detail-item">
<span class="detail-label">DOB</span>
<span class="detail-value">{{formatDate .Patient.DOB}}</span>
</div>
<div class="detail-item">
<span class="detail-label">Gender</span>
<span class="detail-value">{{titleCase .Patient.Gender}}</span>
</div>
<div class="detail-item">
<span class="detail-label">Email</span>
<span class="detail-value">{{orDash .Patient.Email}}</span>
</div>
</div>
</div>
{{/* Alerts Card */}}
{{if hasCriticalAllergies .Allergies}}
<div class="card" style="border-left: 4px solid var(--danger-color);">
<div class="card-header" style="border-bottom:none; padding-bottom:0; margin-bottom:0;">
<h3 class="card-title text-danger">Critical Allergies</h3>
</div>
<div class="mt-4">
{{range .Allergies}}
{{if eq .Criticality "high"}}
<div class="flex justify-between items-center mb-2">
<span class="font-medium text-danger">{{.CodeText}}</span>
<span class="badge badge-danger">High</span>
</div>
{{end}}
{{end}}
</div>
</div>
{{end}}
{{/* SMART Inspector */}}
<div class="card">
<details>
<summary>SMART Inspector</summary>
<div class="mt-4">
<div class="detail-item mb-4">
<span class="detail-label">FHIR Base URL</span>
<code class="code-block">{{.Session.EHRURL}}</code>
</div>
<div class="detail-item mb-4">
<span class="detail-label">Scopes</span>
<code class="code-block">{{.Session.Scope}}</code>
</div>
<div class="detail-item">
<span class="detail-label">Access Token</span>
<code class="code-block">{{.Session.AccessToken}}</code>
</div>
</div>
</details>
</div>
{{end}}
</div>
{{/* ---- Right Column: Clinical Data ---- */}}
<div class="md:col-span-2" style="grid-column: span 2;"> <!-- Force span 2 if grid lines up -->
{{if .Patient}}
<div class="card">
<div class="card-header">
<h3 class="card-title">Clinical Record</h3>
<div class="flex items-center gap-4">
<span class="text-sm text-muted">
{{if .LatestSync}}Synced: {{formatDateTime .LatestSync.SyncedAt}}{{else}}Never synced{{end}}
</span>
<form action="/dashboard/sync?patient_id={{.Patient.FHIRID}}" method="POST" style="display:inline;">
<button class="btn btn-primary btn-sm" type="submit" id="syncBtn" onclick="onSyncClick()">
<svg 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="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>
Sync
</button>
</form>
</div>
</div>
<div class="tabs">
<button class="tab-btn active" onclick="openTab(event, 'observations')">Vitals & Labs <span class="badge badge-neutral ml-2">{{len .Observations}}</span></button>
<button class="tab-btn" onclick="openTab(event, 'conditions')">Conditions <span class="badge badge-neutral ml-2">{{len .Conditions}}</span></button>
<button class="tab-btn" onclick="openTab(event, 'medications')">Meds <span class="badge badge-neutral ml-2">{{len .Medications}}</span></button>
<button class="tab-btn" onclick="openTab(event, 'allergies')">Allergies <span class="badge badge-neutral ml-2">{{len .Allergies}}</span></button>
<button class="tab-btn" onclick="openTab(event, 'notes')">Notes <span class="badge badge-neutral ml-2">{{len .DocumentReferences}}</span></button>
</div>
{{/* Observations */}}
<div id="observations" class="tab-content active">
{{if .Observations}}
{{range $cat, $obs := groupByCategory .Observations}}
<div class="mb-4">
<h4 class="font-bold text-muted uppercase text-xs mb-2 tracking-wide">{{titleCase $cat}}</h4>
<div class="table-container">
<table>
<thead>
<tr>
<th>Date</th>
<th>Code</th>
<th>Value</th>
<th>Interpretation</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{{range $obs}}
<tr>
<td>{{orDash .EffectiveDate}}</td>
<td class="font-medium">{{orDash .CodeText}}</td>
<td>
{{if .ValueQuantity}}
<strong>{{printf "%.4g" (derefFloat64 .ValueQuantity)}}</strong> {{.ValueUnit}}
{{if or .ReferenceRangeLow .ReferenceRangeHigh}}
<div class="text-xs text-muted">
Ref: [{{if .ReferenceRangeLow}}{{printf "%.4g" (derefFloat64 .ReferenceRangeLow)}}{{else}}—{{end}} {{if .ReferenceRangeHigh}}{{printf "%.4g" (derefFloat64 .ReferenceRangeHigh)}}{{else}}—{{end}}]
</div>
{{end}}
{{else if .ValueString}}
{{.ValueString}}
{{else}}—{{end}}
</td>
<td>
{{if .Interpretation}}
<span class="badge badge-info">{{.Interpretation}}</span>
{{else}}—{{end}}
</td>
<td><span class="badge badge-neutral">{{.Status}}</span></td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
{{end}}
{{else}}
<div class="text-center py-8 text-muted">No observations found.</div>
{{end}}
</div>
{{/* Conditions */}}
<div id="conditions" class="tab-content">
{{if .Conditions}}
<div class="flex gap-2 mb-4">
<button onclick="filterConditions('all')" class="btn btn-outline btn-sm active">All</button>
<button onclick="filterConditions('active')" class="btn btn-outline btn-sm">Active</button>
<button onclick="filterConditions('resolved')" class="btn btn-outline btn-sm">Resolved</button>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>Date</th>
<th>Condition</th>
<th>Status</th>
<th>Verification</th>
</tr>
</thead>
<tbody>
{{range .Conditions}}
<tr class="condition-row" data-status="{{.ClinicalStatus}}">
<td>{{orDash .RecordedDate}}</td>
<td class="font-medium">{{orDash .CodeText}}</td>
<td>
{{if eq .ClinicalStatus "active"}}
<span class="badge badge-success">Active</span>
{{else if eq .ClinicalStatus "resolved"}}
<span class="badge badge-neutral">Resolved</span>
{{else}}
<span class="badge badge-warning">{{.ClinicalStatus}}</span>
{{end}}
</td>
<td>{{titleCase .VerificationStatus}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<div class="text-center py-8 text-muted">No conditions found.</div>
{{end}}
</div>
{{/* Medications */}}
<div id="medications" class="tab-content">
{{if .Medications}}
<div class="table-container">
<table>
<thead>
<tr>
<th>Authored</th>
<th>Medication</th>
<th>Dosage</th>
<th>Status</th>
<th>Requester</th>
</tr>
</thead>
<tbody>
{{range .Medications}}
<tr>
<td>{{orDash .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>
<td>{{orDash .RequesterDisplay}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<div class="text-center py-8 text-muted">No medications found.</div>
{{end}}
</div>
{{/* Allergies */}}
<div id="allergies" class="tab-content">
{{if .Allergies}}
<div class="table-container">
<table>
<thead>
<tr>
<th>Date</th>
<th>Allergen</th>
<th>Type</th>
<th>Criticality</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{{range .Allergies}}
<tr>
<td>{{orDash .RecordedDate}}</td>
<td class="font-medium">{{orDash .CodeText}}</td>
<td>{{titleCase .Type}}</td>
<td>
{{if eq .Criticality "high"}}
<span class="badge badge-danger">High</span>
{{else if eq .Criticality "medium"}}
<span class="badge badge-warning">Medium</span>
{{else}}
<span class="badge badge-neutral">{{orDash .Criticality}}</span>
{{end}}
</td>
<td>{{titleCase .ClinicalStatus}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<div class="text-center py-8 text-muted">No allergies found.</div>
{{end}}
</div>
{{/* Notes */}}
<div id="notes" class="tab-content">
{{if .DocumentReferences}}
<div class="table-container">
<table>
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>Description</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{{range .DocumentReferences}}
<tr>
<td>{{orDash .Date}}</td>
<td class="font-medium">{{orDash .TypeText}}</td>
<td>{{orDash .Description}}</td>
<td><span class="badge badge-neutral">{{orDash .Status}}</span></td>
<td>
{{if .ContentURL}}
<a href="{{.ContentURL}}" target="_blank" class="btn btn-outline btn-sm" style="padding: 2px 8px; font-size: 0.75rem;">View</a>
{{else}}
<span class="text-muted text-xs">Inline</span>
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<div class="text-center py-8 text-muted">No clinical notes found.</div>
{{end}}
</div>
</div>
{{end}} {{/* End if .Patient */}}
</div>
</div>
{{end}}
{{define "scripts"}}
<script>
function openTab(evt, tabName) {
var i, tabcontent, tablinks;
tabcontent = document.getElementsByClassName("tab-content");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].classList.remove("active");
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tab-btn");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].classList.remove("active");
}
var target = document.getElementById(tabName);
target.style.display = "block";
// Small timeout to allow display:block to apply before adding active class for animation
setTimeout(() => target.classList.add("active"), 10);
evt.currentTarget.classList.add("active");
}
function filterConditions(status) {
var rows = document.getElementsByClassName("condition-row");
// Update buttons
var btns = document.querySelectorAll('#conditions .btn-outline');
btns.forEach(b => b.classList.remove('active'));
event.target.classList.add('active');
for (var i = 0; i < rows.length; i++) {
var rowStatus = rows[i].getAttribute("data-status");
if (status === "all" || rowStatus === status) {
rows[i].style.display = "";
} else {
rows[i].style.display = "none";
}
}
}
function onSyncClick() {
var btn = document.getElementById("syncBtn");
// Delay disabling to ensure form submission triggers
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);
}
</script>
<style>
.animate-spin { animation: spin 1s linear infinite; }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
</style>
{{end}}