2026-02-20 15:28:41 -05:00
{{template "base.html" .}}
{{define "nav"}}
2026-02-20 23:59:46 -05:00
< 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 >
2026-02-20 15:28:41 -05:00
{{end}}
{{define "content"}}
2026-02-20 17:46:34 -05:00
{{/* ---- Flash Messages ---- */}}
2026-02-20 17:17:26 -05:00
{{if .Synced}}
2026-02-20 23:59:46 -05:00
< div class = "flash-message mb-6" >
2026-02-20 17:46:34 -05:00
< 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 >
2026-02-20 17:17:26 -05:00
< div >
2026-02-20 23:59:46 -05:00
< strong class = "font-medium text-main" > Sync complete< / strong >
< span class = "text-sm text-muted" > Data has been refreshed from the EHR.< / span >
2026-02-20 17:17:26 -05:00
< / div >
< / div >
2026-02-20 23:59:46 -05:00
< button onclick = "this.parentElement.style.display='none';" class = "btn btn-icon text-muted" > × < / button >
2026-02-20 17:17:26 -05:00
< / div >
{{end}}
2026-02-20 23:59:46 -05:00
{{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}}
2026-02-20 17:46:34 -05:00
< / div >
2026-02-20 23:59:46 -05:00
< 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 >
2026-02-20 17:46:34 -05:00
< / div >
2026-02-20 23:59:46 -05:00
< 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}}
2026-02-20 17:46:34 -05:00
< / div >
< / div >
2026-02-20 23:59:46 -05:00
< 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 >
2026-03-01 21:02:10 -05:00
{{if .RimidiMatch}}
{{if .RimidiPatientKey}}
< a href = "http://localhost:2222/providers/v2/diab/patient/labs/{{.RimidiPatientKey}}/" target = "_blank" class = "btn" style = "background-color:#e91e63;color:white;border:none;" title = "Launch patient in Rimidi" >
< 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 = "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" / > < polyline points = "15 3 21 3 21 9" / > < line x1 = "10" y1 = "14" x2 = "21" y2 = "3" / > < / svg >
Launch in Rimidi
< / a >
{{else}}
< button class = "btn" style = "background-color:#e91e63;color:white;border:none;opacity:0.6;cursor:not-allowed;" title = "Loading patient key..." disabled >
< 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 = "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" / > < polyline points = "15 3 21 3 21 9" / > < line x1 = "10" y1 = "14" x2 = "21" y2 = "3" / > < / svg >
Launch in Rimidi
< / button >
{{end}}
< button class = "btn btn-outline" type = "button" onclick = "unlinkPatient('{{.Patient.FHIRID}}')" title = "Unlink this patient from Rimidi" style = "color:var(--color-danger);border-color:var(--color-danger);" >
< 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 = "M18 6L6 18" / > < path d = "M6 6l12 12" / > < / svg >
Unlink
< / button >
{{else}}
2026-02-24 00:26:55 -05:00
< button class = "btn btn-outline" type = "button" onclick = "findInRimidi('{{.Patient.FHIRID}}')" title = "Search for this patient in the Rimidi system" >
< 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" > < circle cx = "11" cy = "11" r = "8" / > < line x1 = "21" y1 = "21" x2 = "16.65" y2 = "16.65" / > < / svg >
Find in Rimidi
< / button >
2026-03-01 21:02:10 -05:00
{{end}}
2026-02-20 23:59:46 -05:00
< span class = "text-xs text-muted" >
Last Synced: {{if .LatestSync}}{{formatDateTime .LatestSync.SyncedAt}}{{else}}Never{{end}}
< / span >
< / div >
2026-02-20 15:28:41 -05:00
< / div >
2026-02-20 23:59:46 -05:00
< / div >
2026-02-20 15:28:41 -05:00
2026-02-20 23:59:46 -05:00
< div class = "dashboard-layout" >
{{/* ---- Main Content Column ---- */}}
< div class = "dashboard-main" >
2026-02-20 17:46:34 -05:00
2026-02-20 23:59:46 -05:00
{{/* 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 >
2026-02-20 17:46:34 -05:00
< / div >
2026-02-20 23:59:46 -05:00
< div class = "panel panel-padded stat-card" >
< div class = "stat-value text-success" > {{.Summary.ActiveMedCount}}< / div >
< div class = "stat-label" > Active Medications< / div >
2026-02-20 17:46:34 -05:00
< / div >
2026-02-20 23:59:46 -05:00
< 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 >
2026-02-20 17:46:34 -05:00
< / div >
< / div >
2026-02-20 17:17:26 -05:00
2026-02-20 23:59:46 -05:00
{{/* 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 >
2026-03-01 21:02:10 -05:00
{{if .RimidiMatch}}
< button class = "tab-btn cgm-tab" onclick = "openTab(event, 'cgm')" > CGM< / button >
{{end}}
2026-02-20 23:59:46 -05:00
< 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 >
2026-02-20 17:46:34 -05:00
< / div >
< / div >
2026-02-20 15:28:41 -05:00
2026-02-20 23:59:46 -05:00
< 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 >
2026-02-20 17:17:26 -05:00
2026-02-20 23:59:46 -05:00
< 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 >
2026-02-20 17:17:26 -05:00
2026-03-01 21:02:10 -05:00
{{/* ---- CGM Tab ---- */}}
{{if .RimidiMatch}}
< div id = "cgm" class = "tab-content" >
< div id = "cgmTabContent" style = "min-height:400px;" > < / div >
< / div >
{{end}}
2026-02-20 23:59:46 -05:00
{{/* ---- 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 >
2026-02-20 15:28:41 -05:00
2026-02-20 23:59:46 -05:00
{{/* ---- Labs Tab ---- */}}
< div id = "labs" class = "tab-content" >
{{$labs := filterObsByCategory .Observations "laboratory"}}
{{if $labs}}
< div class = "table-wrapper" >
2026-02-20 17:46:34 -05:00
< table >
< thead >
2026-02-20 23:59:46 -05:00
< tr > < th > Date< / th > < th > Test< / th > < th > Result< / th > < th > Ref Range< / th > < th > Flag< / th > < / tr >
2026-02-20 17:46:34 -05:00
< / thead >
< tbody >
2026-02-20 23:59:46 -05:00
{{range $labs}}
< tr { { if isAbnormal . Interpretation } } class = "row-abnormal" { { end } } >
< td > {{formatDate .EffectiveDate}}< / td >
2026-02-20 17:46:34 -05:00
< td class = "font-medium" > {{orDash .CodeText}}< / td >
< td >
2026-02-20 23:59:46 -05:00
{{if .ValueQuantity}}< strong > {{printf "%.4g" (derefFloat64 .ValueQuantity)}}< / strong > {{.ValueUnit}}
{{else if .ValueString}}{{.ValueString}}
2026-02-20 17:46:34 -05:00
{{else}}—{{end}}
< / td >
2026-02-20 23:59:46 -05:00
< 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}}]
2026-02-20 17:46:34 -05:00
{{else}}—{{end}}
< / td >
2026-02-20 23:59:46 -05:00
< td > {{if isAbnormal .Interpretation}}< span class = "badge badge-danger" > {{.Interpretation}}< / span > {{else if .Interpretation}}< span class = "badge badge-neutral" > {{.Interpretation}}< / span > {{end}}< / td >
2026-02-20 17:46:34 -05:00
< / tr >
{{end}}
< / tbody >
< / table >
< / div >
2026-02-20 23:59:46 -05:00
{{else}}
< p class = "empty-state" > No lab results found.< / p >
{{end}}
2026-02-20 17:46:34 -05:00
< / div >
2026-02-20 15:28:41 -05:00
2026-02-20 23:59:46 -05:00
{{/* ---- 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 >
2026-02-20 17:46:34 -05:00
{{end}}
2026-02-20 23:59:46 -05:00
< / 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 >
2026-02-20 17:46:34 -05:00
{{end}}
2026-02-20 23:59:46 -05:00
< / 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 >
2026-02-20 17:46:34 -05:00
< / div >
2026-02-20 15:28:41 -05:00
< / div >
2026-02-20 23:59:46 -05:00
< / div >
2026-02-20 17:46:34 -05:00
2026-02-20 23:59:46 -05:00
{{/* ---- 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}}
2026-02-20 17:46:34 -05:00
< / div >
2026-02-20 15:28:41 -05:00
< / div >
2026-02-20 23:59:46 -05:00
{{end}}
2026-02-20 17:46:34 -05:00
2026-02-20 23:59:46 -05:00
{{/* 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}}
2026-02-20 17:46:34 -05:00
< / div >
2026-02-20 15:28:41 -05:00
< / div >
2026-02-20 17:46:34 -05:00
2026-02-20 23:59:46 -05:00
{{/* 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 >
2026-02-20 15:28:41 -05:00
< / div >
2026-02-20 17:46:34 -05:00
2026-02-20 15:28:41 -05:00
< / div >
< / div >
2026-02-20 17:46:34 -05:00
< / div >
2026-02-20 23:59:46 -05:00
{{end}}
2026-02-20 15:28:41 -05:00
{{end}}
{{define "scripts"}}
< script >
function openTab(evt, tabName) {
2026-02-20 23:59:46 -05:00
var tabcontent = document.getElementsByClassName("tab-content");
for (var i = 0; i < tabcontent.length ; i + + ) {
2026-02-20 17:46:34 -05:00
tabcontent[i].classList.remove("active");
2026-02-20 15:28:41 -05:00
}
2026-02-20 23:59:46 -05:00
var tablinks = document.getElementsByClassName("tab-btn");
for (var i = 0; i < tablinks.length ; i + + ) {
2026-02-20 17:46:34 -05:00
tablinks[i].classList.remove("active");
2026-02-20 15:28:41 -05:00
}
2026-02-20 17:46:34 -05:00
var target = document.getElementById(tabName);
setTimeout(() => target.classList.add("active"), 10);
evt.currentTarget.classList.add("active");
2026-03-01 21:02:10 -05:00
// Load CGM data when CGM tab is opened
if (tabName === 'cgm') {
var cgmContent = document.getElementById('cgmTabContent');
if (cgmContent & & !cgmContent.dataset.loaded) {
var patientFHIRID = '{{.Patient.FHIRID}}';
loadCGMData(patientFHIRID);
}
}
2026-02-20 15:28:41 -05:00
}
2026-02-20 17:17:26 -05:00
function onSyncClick() {
var btn = document.getElementById("syncBtn");
2026-02-20 17:46:34 -05:00
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);
2026-02-20 17:17:26 -05:00
}
2026-02-20 23:59:46 -05:00
// Helper for splitting scopes string
function split(s, sep) { return s.split(sep); }
2026-02-24 00:26:55 -05:00
/* ── "Find in Rimidi" match modal ────────────────────────────────── */
function findInRimidi(patientFHIRID) {
var overlay = document.getElementById('rimidiMatchOverlay');
var body = document.getElementById('rimidiMatchBody');
overlay.style.display = 'flex';
body.innerHTML =
'< div style = "text-align:center;padding:40px;" > ' +
'< div class = "animate-spin" style = "width:24px;height:24px;border:3px solid #ddd;border-top-color:#0d6efd;border-radius:50%;display:inline-block;" > < / div > ' +
'< p style = "margin-top:12px;color:var(--color-text-muted);" > Searching Rimidi…< / p > < / div > ';
var url = '/api/patient-match-proxy';
if (patientFHIRID) {
url += '?patient_id=' + encodeURIComponent(patientFHIRID);
}
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})
.then(function(resp) {
if (!resp.ok) {
return resp.json().then(function(d) { throw new Error(d.error || 'HTTP ' + resp.status); });
}
return resp.json();
})
.then(function(data) {
2026-03-01 21:02:10 -05:00
renderRimidiMatchResults(body, data, patientFHIRID);
2026-02-24 00:26:55 -05:00
})
.catch(function(err) {
body.innerHTML =
'< div style = "padding:20px;color:var(--color-danger);background:var(--color-danger-bg);border-radius:8px;margin:20px;" > ' +
'< strong > Error:< / strong > ' + escapeHtml(err.message) + '< / div > ';
});
}
function closeRimidiMatch() {
document.getElementById('rimidiMatchOverlay').style.display = 'none';
}
function escapeHtml(s) {
var d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
2026-03-01 21:02:10 -05:00
function renderRimidiMatchResults(container, data, patientFHIRID) {
2026-02-24 00:26:55 -05:00
var matches = data.matches || [];
var local = data.local_patient || {};
2026-03-01 21:02:10 -05:00
patientFHIRID = patientFHIRID || '';
2026-02-24 00:26:55 -05:00
if (matches.length === 0) {
container.innerHTML =
2026-03-01 21:02:10 -05:00
'< div style = "padding:40px;text-align:center;color:var(--text-muted);" > ' +
'< svg xmlns = "http://www.w3.org/2000/svg" width = "48" height = "48" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2" stroke-linecap = "round" stroke-linejoin = "round" style = "margin:0 auto 16px;opacity:0.5;" > < circle cx = "11" cy = "11" r = "8" / > < line x1 = "21" y1 = "21" x2 = "16.65" y2 = "16.65" / > < / svg > ' +
'< p style = "margin:0;font-size:16px;" > No matching patients found in the Rimidi system.< / p > < / div > ';
2026-02-24 00:26:55 -05:00
return;
}
2026-03-01 21:02:10 -05:00
var html = '< div style = "padding:24px;" > ';
html += '< div style = "margin-bottom:24px;padding:16px;background:var(--accent-blue-bg);border-radius:var(--radius-md);border:1px solid #BAE6FD;" > ';
html += '< p style = "margin:0;color:var(--accent-blue);font-size:14px;font-weight:500;" > ' +
'Found < strong style = "font-weight:600;" > ' + matches.length + '< / strong > potential match' +
2026-02-24 00:26:55 -05:00
(matches.length > 1 ? 'es' : '') + ' in Rimidi.< / p > ';
2026-03-01 21:02:10 -05:00
html += '< / div > ';
2026-02-24 00:26:55 -05:00
var fieldLabels = {
first_name: 'First Name', last_name: 'Last Name',
email: 'Email', dob: 'Date of Birth', sex: 'Sex'
};
var fieldOrder = ['first_name', 'last_name', 'email', 'dob', 'sex'];
for (var i = 0; i < matches.length ; i + + ) {
var m = matches[i];
var f = m.fields || {};
2026-03-01 21:02:10 -05:00
var borderColor = m.score >= 4 ? 'var(--success)' : (m.score >= 3 ? 'var(--warning)' : 'var(--accent-blue)');
2026-02-24 00:26:55 -05:00
var badgeClass = m.score >= 4 ? 'badge-success' : (m.score >= 3 ? 'badge-warning' : 'badge-blue');
2026-03-01 21:02:10 -05:00
var matchQuality = m.score >= 4 ? 'High' : (m.score >= 3 ? 'Medium' : 'Low');
2026-02-24 00:26:55 -05:00
2026-03-01 21:02:10 -05:00
html += '< div class = "panel" style = "margin-bottom:20px;border-left:5px solid ' + borderColor + ';padding:0;overflow:hidden;box-shadow:var(--shadow-subtle);" > ';
html += '< div style = "display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid var(--border-hard);background:var(--bg-panel);" > ';
html += '< div style = "display:flex;align-items:center;gap:12px;" > ';
html += '< strong style = "font-size:16px;font-weight:600;color:var(--text-main);" > Match #' + (i + 1) + '< / strong > ';
html += '< span class = "badge ' + badgeClass + '" style = "font-size:12px;padding:4px 10px;" > ' + m.score + '/5 fields match< / span > ';
html += '< span style = "font-size:12px;color:var(--text-muted);" > (' + matchQuality + ' confidence)< / span > ';
html += '< / div > ';
2026-02-24 00:26:55 -05:00
html += '< / div > ';
2026-03-01 21:02:10 -05:00
html += '< div style = "overflow-x:auto;" > ';
html += '< table style = "width:100%;border-collapse:separate;border-spacing:0;" > ';
html += '< thead > < tr style = "background:var(--bg-hover);" > ';
html += '< th style = "padding:12px 16px;text-align:left;width:20%;font-size:11px;text-transform:uppercase;color:var(--text-muted);font-weight:600;letter-spacing:0.05em;border-bottom:2px solid var(--border-hard);" > Field< / th > ';
html += '< th style = "padding:12px 16px;text-align:left;width:32%;font-size:11px;text-transform:uppercase;color:var(--text-muted);font-weight:600;letter-spacing:0.05em;border-bottom:2px solid var(--border-hard);" > HRS (Local)< / th > ';
html += '< th style = "padding:12px 16px;text-align:left;width:32%;font-size:11px;text-transform:uppercase;color:var(--text-muted);font-weight:600;letter-spacing:0.05em;border-bottom:2px solid var(--border-hard);" > Rimidi (Remote)< / th > ';
html += '< th style = "padding:12px 16px;text-align:center;width:16%;font-size:11px;text-transform:uppercase;color:var(--text-muted);font-weight:600;letter-spacing:0.05em;border-bottom:2px solid var(--border-hard);" > Status< / th > ';
2026-02-24 00:26:55 -05:00
html += '< / tr > < / thead > < tbody > ';
for (var j = 0; j < fieldOrder.length ; j + + ) {
var key = fieldOrder[j];
var fld = f[key] || {};
var localVal = local[key] || '—';
var remoteVal = fld.value || '—';
var isMatch = fld.match === true;
2026-03-01 21:02:10 -05:00
// Enhanced visual indicators
var matchIcon = isMatch
? '< svg xmlns = "http://www.w3.org/2000/svg" width = "20" height = "20" viewBox = "0 0 24 24" fill = "none" stroke = "var(--success)" stroke-width = "2.5" stroke-linecap = "round" stroke-linejoin = "round" style = "display:inline-block;vertical-align:middle;" > < polyline points = "20 6 9 17 4 12" / > < / svg > '
: '< svg xmlns = "http://www.w3.org/2000/svg" width = "20" height = "20" viewBox = "0 0 24 24" fill = "none" stroke = "var(--danger)" stroke-width = "2.5" stroke-linecap = "round" stroke-linejoin = "round" style = "display:inline-block;vertical-align:middle;" > < line x1 = "18" y1 = "6" x2 = "6" y2 = "18" / > < line x1 = "6" y1 = "6" x2 = "18" y2 = "18" / > < / svg > ';
var rowStyle = isMatch
? 'background:var(--success-bg);border-left:3px solid var(--success);'
: 'background:var(--warning-bg);border-left:3px solid var(--warning);';
var matchBadge = isMatch
? '< span style = "display:inline-flex;align-items:center;gap:4px;padding:4px 8px;background:var(--success-bg);color:var(--success);border:1px solid #A7F3D0;border-radius:var(--radius-sm);font-size:11px;font-weight:600;" > Match< / span > '
: '< span style = "display:inline-flex;align-items:center;gap:4px;padding:4px 8px;background:var(--danger-bg);color:var(--danger);border:1px solid #FECDD3;border-radius:var(--radius-sm);font-size:11px;font-weight:600;" > Mismatch< / span > ';
html += '< tr style = "' + rowStyle + 'border-top:1px solid var(--border-soft);transition:background 0.2s;" > ';
html += '< td style = "padding:14px 16px;font-weight:600;color:var(--text-main);font-size:13px;" > ' + fieldLabels[key] + '< / td > ';
html += '< td style = "padding:14px 16px;color:var(--text-main);font-size:13px;font-family:var(--font-mono);background:rgba(255,255,255,0.5);" > ' + escapeHtml(localVal) + '< / td > ';
html += '< td style = "padding:14px 16px;color:var(--text-main);font-size:13px;font-family:var(--font-mono);background:rgba(255,255,255,0.5);" > ' + escapeHtml(remoteVal) + '< / td > ';
html += '< td style = "padding:14px 16px;text-align:center;vertical-align:middle;" > ' + matchBadge + '< / td > ';
2026-02-24 00:26:55 -05:00
html += '< / tr > ';
}
2026-03-01 21:02:10 -05:00
html += '< / tbody > < / table > ';
html += '< / div > ';
// Add Confirm Match button
var patientPK = m.patient_pk || m.patient_ref;
html += '< div style = "padding:16px 20px;border-top:1px solid var(--border-hard);text-align:right;background:var(--bg-panel);" > ';
html += '< button class = "btn btn-primary" id = "confirm-btn-' + i + '" onclick = "confirmMatch(\'' + escapeHtml(patientFHIRID) + '\', \'' + escapeHtml(m.patient_ref) + '\', \'' + escapeHtml(patientPK) + '\', ' + i + ')" style = "font-weight:600;" > ';
html += '< 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" style = "vertical-align:text-bottom;margin-right:6px;" > < polyline points = "20 6 9 17 4 12" / > < / svg > ';
html += 'Confirm Match';
html += '< / button > ';
html += '< / div > ';
html += '< / div > ';
2026-02-24 00:26:55 -05:00
}
html += '< / div > ';
container.innerHTML = html;
}
2026-03-01 21:02:10 -05:00
function confirmMatch(patientFHIRID, rimidiPatientRef, rimidiPatientPK, matchIndex) {
var btn = document.getElementById('confirm-btn-' + matchIndex);
if (!btn) return;
var originalText = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '< span style = "display:inline-block;width:14px;height:14px;border:2px solid #fff;border-top-color:transparent;border-radius:50%;animation:spin 0.6s linear infinite;margin-right:6px;" > < / span > Confirming...';
fetch('/api/patient-match/confirm?patient_id=' + encodeURIComponent(patientFHIRID), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
rimidi_patient_ref: rimidiPatientRef,
rimidi_patient_pk: rimidiPatientPK
})
})
.then(function(resp) {
if (!resp.ok) {
return resp.json().then(function(d) { throw new Error(d.error || 'HTTP ' + resp.status); });
}
return resp.json();
})
.then(function(data) {
if (data.success) {
// Show success message
var body = document.getElementById('rimidiMatchBody');
body.innerHTML =
'< div style = "padding:40px;text-align:center;" > ' +
'< div style = "color:var(--color-success);font-size:48px;margin-bottom:16px;" > ✓< / div > ' +
'< h3 style = "margin:0 0 8px 0;" > Match Confirmed!< / h3 > ' +
'< p style = "color:var(--color-text-muted);margin-bottom:24px;" > The patient match has been successfully confirmed.< / p > ' +
'< button class = "btn btn-primary" onclick = "closeRimidiMatch(); window.location.reload();" > Close & Refresh< / button > ' +
'< / div > ';
} else {
throw new Error(data.message || 'Confirmation failed');
}
})
.catch(function(err) {
btn.disabled = false;
btn.innerHTML = originalText;
alert('Error confirming match: ' + err.message);
});
}
2026-02-20 15:28:41 -05:00
< / script >
2026-02-24 00:26:55 -05:00
<!-- Rimidi Patient Match Modal Overlay -->
2026-03-01 21:02:10 -05:00
< div id = "rimidiMatchOverlay" style = "display:none;position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.65);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);align-items:center;justify-content:center;" >
< div style = "background:#FFFFFF;border-radius:var(--radius-md);width:90%;max-width:800px;max-height:85vh;display:flex;flex-direction:column;box-shadow:0 25px 80px rgba(0,0,0,0.4),0 8px 16px rgba(0,0,0,0.2);border:1px solid var(--border-soft);overflow:hidden;" >
< div style = "display:flex;justify-content:space-between;align-items:center;padding:20px 24px;border-bottom:1px solid var(--border-hard);background:var(--bg-panel);" >
< h3 style = "margin:0;font-size:18px;font-weight:600;color:var(--text-main);display:flex;align-items:center;gap:8px;" >
< 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" > < circle cx = "11" cy = "11" r = "8" / > < line x1 = "21" y1 = "21" x2 = "16.65" y2 = "16.65" / > < / svg >
Find Patient in Rimidi
< / h3 >
< button onclick = "closeRimidiMatch()" style = "background:none;border:none;font-size:28px;cursor:pointer;color:var(--text-muted);padding:0;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:var(--radius-sm);transition:all 0.2s;" onmouseover = "this.style.background='var(--bg-hover)';this.style.color='var(--text-main)';" onmouseout = "this.style.background='none';this.style.color='var(--text-muted)';" title = "Close" > × < / button >
< / div >
< div id = "rimidiMatchBody" style = "overflow-y:auto;flex:1;background:var(--bg-panel);" >
< / div >
< div style = "padding:16px 24px;border-top:1px solid var(--border-hard);text-align:right;background:var(--bg-panel);" >
< button class = "btn btn-secondary" onclick = "closeRimidiMatch()" > Close< / button >
< / div >
< / div >
< / div >
<!-- CGM Preview Modal Overlay -->
< div id = "cgmPreviewOverlay" style = "display:none;position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,0.5);align-items:center;justify-content:center;" >
< div style = "background:var(--color-bg-main);border-radius:12px;width:95%;max-width:1200px;max-height:90vh;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,0.3);" >
2026-02-24 00:26:55 -05:00
< div style = "display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid var(--color-border-soft);" >
< h3 style = "margin:0;font-size:18px;" >
2026-03-01 21:02:10 -05:00
< 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" style = "vertical-align:text-bottom;margin-right:6px;" > < path d = "M3 3v18h18" / > < path d = "M7 12h10M7 8h10M7 16h4" / > < / svg >
CGM Data from Rimidi
2026-02-24 00:26:55 -05:00
< / h3 >
2026-03-01 21:02:10 -05:00
< button onclick = "closeCGMPreview()" style = "background:none;border:none;font-size:24px;cursor:pointer;color:var(--color-text-muted);padding:0 4px;" > × < / button >
2026-02-24 00:26:55 -05:00
< / div >
2026-03-01 21:02:10 -05:00
< div id = "cgmPreviewBody" style = "overflow-y:auto;flex:1;padding:20px;" >
2026-02-24 00:26:55 -05:00
< / div >
< div style = "padding:12px 20px;border-top:1px solid var(--color-border-soft);text-align:right;" >
2026-03-01 21:02:10 -05:00
< button class = "btn btn-secondary" onclick = "closeCGMPreview()" > Close< / button >
2026-02-24 00:26:55 -05:00
< / div >
< / div >
< / div >
2026-03-01 21:02:10 -05:00
< script src = "https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js" > < / script >
< script src = "https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js" > < / script >
< script >
/* ── CGM Preview Functions ──────────────────────────────────────── */
var cgmChart = null;
function loadCGMData(patientFHIRID) {
var container = document.getElementById('cgmTabContent');
if (!container) return;
// Mark as loading to prevent duplicate loads
if (container.dataset.loaded === 'loading') return;
container.dataset.loaded = 'loading';
container.innerHTML =
'< div style = "text-align:center;padding:40px;" > ' +
'< div class = "animate-spin" style = "width:24px;height:24px;border:3px solid #ddd;border-top-color:#0d6efd;border-radius:50%;display:inline-block;" > < / div > ' +
'< p style = "margin-top:12px;color:var(--color-text-muted);" > Loading CGM data…< / p > < / div > ';
var url = '/api/cgm-preview?patient_id=' + encodeURIComponent(patientFHIRID);
fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
})
.then(function(resp) {
if (!resp.ok) {
return resp.json().then(function(d) { throw new Error(d.error || 'HTTP ' + resp.status); });
}
return resp.json();
})
.then(function(data) {
container.dataset.loaded = 'true';
renderCGMPreview(container, data);
})
.catch(function(err) {
container.dataset.loaded = 'error';
container.innerHTML =
'< div style = "padding:20px;color:var(--color-danger);background:var(--color-danger-bg);border-radius:8px;margin:20px;" > ' +
'< strong > Error:< / strong > ' + escapeHtml(err.message) + '< / div > ';
});
}
// Keep the old function for backward compatibility (in case it's called from elsewhere)
function loadCGMPreview(patientFHIRID) {
loadCGMData(patientFHIRID);
}
function closeCGMPreview() {
document.getElementById('cgmPreviewOverlay').style.display = 'none';
if (cgmChart) {
cgmChart.destroy();
cgmChart = null;
}
}
function unlinkPatient(patientFHIRID) {
if (!confirm('Are you sure you want to unlink this patient from Rimidi? This will remove the connection and you will need to match again to view CGM data.')) {
return;
}
var url = '/api/patient-match/unlink?patient_id=' + encodeURIComponent(patientFHIRID);
fetch(url, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' }
})
.then(function(resp) {
if (!resp.ok) {
return resp.json().then(function(d) { throw new Error(d.error || 'HTTP ' + resp.status); });
}
return resp.json();
})
.then(function(data) {
if (data.success) {
// Reload the page to update the UI
window.location.reload();
} else {
throw new Error(data.message || 'Unlink failed');
}
})
.catch(function(err) {
alert('Error unlinking patient: ' + err.message);
});
}
function renderCGMPreview(container, data) {
var html = '< div style = "display:flex;flex-direction:column;gap:32px;" > ';
// Key Metrics - Modern Grid Layout
if (data.metrics) {
html += '< div style = "display:grid;grid-template-columns:repeat(auto-fit, minmax(180px, 1fr));gap:20px;margin-bottom:8px;" > ';
if (data.metrics.average_glucose !== undefined) {
html += '< div style = "padding:20px;background:linear-gradient(135deg, #818cf8 0%, #a78bfa 100%);border-radius:12px;color:white;box-shadow:0 4px 12px rgba(129,140,248,0.2);" > ';
html += '< div style = "font-size:12px;opacity:0.9;margin-bottom:8px;text-transform:uppercase;letter-spacing:0.5px;" > Average Glucose< / div > ';
html += '< div style = "font-size:32px;font-weight:700;line-height:1.2;" > ' + escapeHtml(data.metrics.average_glucose.toFixed(1)) + '< span style = "font-size:18px;font-weight:400;opacity:0.9;" > mg/dL< / span > < / div > ';
html += '< / div > ';
}
if (data.metrics.gmi !== undefined) {
html += '< div style = "padding:20px;background:linear-gradient(135deg, #f9a8d4 0%, #fb7185 100%);border-radius:12px;color:white;box-shadow:0 4px 12px rgba(249,168,212,0.2);" > ';
html += '< div style = "font-size:12px;opacity:0.9;margin-bottom:8px;text-transform:uppercase;letter-spacing:0.5px;" > GMI< / div > ';
html += '< div style = "font-size:32px;font-weight:700;line-height:1.2;" > ' + escapeHtml(data.metrics.gmi.toFixed(1)) + '< span style = "font-size:18px;font-weight:400;opacity:0.9;" > %< / span > < / div > ';
html += '< / div > ';
}
if (data.metrics.time_in_range) {
var tir = data.metrics.time_in_range;
html += '< div style = "padding:20px;background:linear-gradient(135deg, #60a5fa 0%, #38bdf8 100%);border-radius:12px;color:white;box-shadow:0 4px 12px rgba(96,165,250,0.2);" > ';
html += '< div style = "font-size:12px;opacity:0.9;margin-bottom:8px;text-transform:uppercase;letter-spacing:0.5px;" > Time in Range< / div > ';
html += '< div style = "font-size:32px;font-weight:700;line-height:1.2;" > ' + escapeHtml((tir.target || 0).toFixed(1)) + '< span style = "font-size:18px;font-weight:400;opacity:0.9;" > %< / span > < / div > ';
html += '< / div > ';
}
if (data.gri_score & & data.gri_score !== '-' & & data.gri_score !== 'NA') {
html += '< div style = "padding:20px;background:linear-gradient(135deg, #fb9ab8 0%, #fde68a 100%);border-radius:12px;color:white;box-shadow:0 4px 12px rgba(251,154,184,0.2);" > ';
html += '< div style = "font-size:12px;opacity:0.9;margin-bottom:8px;text-transform:uppercase;letter-spacing:0.5px;" > GRI Score< / div > ';
html += '< div style = "font-size:32px;font-weight:700;line-height:1.2;" > ' + escapeHtml(data.gri_score) + '< / div > ';
html += '< / div > ';
}
html += '< / div > ';
// Device Info - Inline with subtle styling
if (data.device_info) {
html += '< div style = "display:flex;align-items:center;gap:16px;padding:16px 20px;background:var(--bg-panel);border-radius:8px;border:1px solid var(--color-border-soft);" > ';
html += '< div style = "flex:1;" > ';
html += '< div style = "font-size:12px;color:var(--color-text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px;" > Device< / div > ';
html += '< div style = "font-size:16px;font-weight:600;color:var(--text-main);" > ' + escapeHtml(data.device_info.type || 'Unknown') + '< / div > ';
html += '< / div > ';
html += '< div style = "padding-left:20px;border-left:1px solid var(--color-border-soft);" > ';
html += '< div style = "font-size:12px;color:var(--color-text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px;" > Status< / div > ';
html += '< div style = "display:flex;align-items:center;gap:6px;" > ';
var statusColor = data.device_info.enabled ? 'var(--color-success)' : 'var(--color-text-muted)';
html += '< span style = "width:8px;height:8px;border-radius:50%;background:' + statusColor + ';" > < / span > ';
html += '< span style = "font-size:14px;font-weight:500;color:var(--text-main);" > ' + (data.device_info.enabled ? 'Active' : 'Inactive') + '< / span > ';
html += '< / div > ';
html += '< / div > ';
html += '< / div > ';
}
// Time in Range Breakdown - Modern Visual Bars
if (data.metrics.time_in_range) {
var tir = data.metrics.time_in_range;
html += '< div style = "padding:24px;background:var(--bg-panel);border-radius:12px;border:1px solid var(--color-border-soft);" > ';
html += '< h3 style = "margin:0 0 20px 0;font-size:16px;font-weight:600;color:var(--text-main);" > Time in Range Breakdown< / h3 > ';
html += '< div style = "display:flex;flex-direction:column;gap:12px;" > ';
// Very Low
if (tir.very_low !== undefined & & tir.very_low > 0) {
html += '< div > ';
html += '< div style = "display:flex;justify-content:space-between;margin-bottom:6px;" > ';
html += '< span style = "font-size:13px;font-weight:500;color:#dc2626;" > Very Low (< 54)< / span > ';
html += '< span style = "font-size:13px;font-weight:600;color:var(--text-main);" > ' + (tir.very_low || 0).toFixed(1) + '%< / span > ';
html += '< / div > ';
html += '< div style = "height:8px;background:#fee2e2;border-radius:4px;overflow:hidden;" > ';
html += '< div style = "height:100%;width:' + (tir.very_low || 0) + '%;background:#dc2626;transition:width 0.3s ease;" > < / div > ';
html += '< / div > ';
html += '< / div > ';
}
// Low
if (tir.low !== undefined & & tir.low > 0) {
html += '< div > ';
html += '< div style = "display:flex;justify-content:space-between;margin-bottom:6px;" > ';
html += '< span style = "font-size:13px;font-weight:500;color:#f59e0b;" > Low (54-70)< / span > ';
html += '< span style = "font-size:13px;font-weight:600;color:var(--text-main);" > ' + (tir.low || 0).toFixed(1) + '%< / span > ';
html += '< / div > ';
html += '< div style = "height:8px;background:#fef3c7;border-radius:4px;overflow:hidden;" > ';
html += '< div style = "height:100%;width:' + (tir.low || 0) + '%;background:#f59e0b;transition:width 0.3s ease;" > < / div > ';
html += '< / div > ';
html += '< / div > ';
}
// Target
if (tir.target !== undefined & & tir.target > 0) {
html += '< div > ';
html += '< div style = "display:flex;justify-content:space-between;margin-bottom:6px;" > ';
html += '< span style = "font-size:13px;font-weight:500;color:#10b981;" > Target (70-180)< / span > ';
html += '< span style = "font-size:13px;font-weight:600;color:var(--text-main);" > ' + (tir.target || 0).toFixed(1) + '%< / span > ';
html += '< / div > ';
html += '< div style = "height:8px;background:#d1fae5;border-radius:4px;overflow:hidden;" > ';
html += '< div style = "height:100%;width:' + (tir.target || 0) + '%;background:#10b981;transition:width 0.3s ease;" > < / div > ';
html += '< / div > ';
html += '< / div > ';
}
// High
if (tir.high !== undefined & & tir.high > 0) {
html += '< div > ';
html += '< div style = "display:flex;justify-content:space-between;margin-bottom:6px;" > ';
html += '< span style = "font-size:13px;font-weight:500;color:#f59e0b;" > High (180-250)< / span > ';
html += '< span style = "font-size:13px;font-weight:600;color:var(--text-main);" > ' + (tir.high || 0).toFixed(1) + '%< / span > ';
html += '< / div > ';
html += '< div style = "height:8px;background:#fef3c7;border-radius:4px;overflow:hidden;" > ';
html += '< div style = "height:100%;width:' + (tir.high || 0) + '%;background:#f59e0b;transition:width 0.3s ease;" > < / div > ';
html += '< / div > ';
html += '< / div > ';
}
// Very High
if (tir.very_high !== undefined & & tir.very_high > 0) {
html += '< div > ';
html += '< div style = "display:flex;justify-content:space-between;margin-bottom:6px;" > ';
html += '< span style = "font-size:13px;font-weight:500;color:#dc2626;" > Very High (> 250)< / span > ';
html += '< span style = "font-size:13px;font-weight:600;color:var(--text-main);" > ' + (tir.very_high || 0).toFixed(1) + '%< / span > ';
html += '< / div > ';
html += '< div style = "height:8px;background:#fee2e2;border-radius:4px;overflow:hidden;" > ';
html += '< div style = "height:100%;width:' + (tir.very_high || 0) + '%;background:#dc2626;transition:width 0.3s ease;" > < / div > ';
html += '< / div > ';
html += '< / div > ';
}
html += '< / div > ';
html += '< / div > ';
}
}
// Chart
if (data.chart_data & & data.chart_data.length > 0) {
html += '< div style = "padding:24px;background:var(--bg-panel);border-radius:12px;border:1px solid var(--color-border-soft);" > ';
html += '< h3 style = "margin:0 0 20px 0;font-size:16px;font-weight:600;color:var(--text-main);" > Glucose Trends (Last 14 Days)< / h3 > ';
html += '< div style = "position:relative;height:350px;" > ';
html += '< canvas id = "cgmChart" > < / canvas > ';
html += '< / div > ';
html += '< / div > ';
}
// Reports - Carousel Format
if (data.reports & & data.reports.length > 0) {
html += '< div style = "padding:24px;background:var(--bg-panel);border-radius:12px;border:1px solid var(--color-border-soft);" > ';
html += '< h3 style = "margin:0 0 24px 0;font-size:16px;font-weight:600;color:var(--text-main);" > Reports (' + data.reports.length + ')< / h3 > ';
// Carousel Container
html += '< div id = "cgmReportsCarousel" style = "position:relative;width:100%;overflow:hidden;" > ';
// Carousel Wrapper
html += '< div id = "cgmReportsCarouselWrapper" style = "display:flex;width:' + (data.reports.length * 100) + '%;transition:transform 0.3s ease-in-out;" > ';
// Generate report slides
for (var i = 0; i < data.reports.length ; i + + ) {
var report = data.reports[i];
var reportType = report.type || 'Unknown';
var reportTypeLabel = reportType.charAt(0).toUpperCase() + reportType.slice(1);
var dateRange = '';
if (report.start_date & & report.end_date) {
dateRange = report.start_date + ' - ' + report.end_date;
} else if (report.start_date) {
dateRange = report.start_date;
} else if (report.end_date) {
dateRange = report.end_date;
}
// Report Slide - each slide is 100% of container width
var slideWidthPercent = 100 / data.reports.length;
html += '< div class = "cgm-report-slide" data-slide-index = "' + i + '" style = "width:' + slideWidthPercent + '%;flex-shrink:0;display:flex;flex-direction:column;padding:24px;box-sizing:border-box;" > ';
// Report Header
html += '< div style = "margin-bottom:16px;" > ';
html += '< div style = "display:flex;align-items:center;gap:12px;margin-bottom:8px;" > ';
html += '< span style = "font-weight:600;font-size:18px;" > ' + escapeHtml(reportTypeLabel) + ' Report< / span > ';
html += '< / div > ';
if (dateRange) {
html += '< span style = "color:var(--color-text-muted);font-size:14px;" > ' + escapeHtml(dateRange) + '< / span > ';
}
html += '< / div > ';
// Preview Image
if (report.image_url) {
html += '< div style = "flex:1;display:flex;align-items:center;justify-content:center;margin-bottom:20px;background:#f8f9fa;border-radius:8px;padding:16px;min-height:400px;" > ';
html += '< img src = "' + escapeHtml(report.image_url) + '" alt = "' + escapeHtml(reportTypeLabel) + ' Report Preview" style = "max-width:100%;max-height:500px;object-fit:contain;border-radius:4px;box-shadow:0 2px 8px rgba(0,0,0,0.1);" > ';
html += '< / div > ';
} else {
html += '< div style = "flex:1;display:flex;align-items:center;justify-content:center;margin-bottom:20px;background:#f8f9fa;border-radius:8px;padding:16px;min-height:400px;color:var(--color-text-muted);" > ';
html += '< p > No preview image available< / p > ';
html += '< / div > ';
}
// PDF Download Link
if (report.pdf_url) {
html += '< div style = "text-align:center;padding-top:12px;border-top:1px solid var(--color-border-soft);" > ';
html += '< a href = "' + escapeHtml(report.pdf_url) + '" target = "_blank" class = "btn btn-primary" style = "text-decoration:none;display:inline-block;" > ';
html += '< 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" style = "vertical-align:text-bottom;margin-right:6px;" > < path d = "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" / > < polyline points = "7 10 12 15 17 10" / > < line x1 = "12" y1 = "15" x2 = "12" y2 = "3" / > < / svg > ';
html += 'Download PDF';
html += '< / a > ';
html += '< / div > ';
}
html += '< / div > '; // End report slide
}
html += '< / div > '; // End carousel wrapper
// Navigation Arrows
if (data.reports.length > 1) {
html += '< button id = "cgmCarouselPrev" onclick = "navigateCGMCarousel(-1)" style = "position:absolute;left:10px;top:50%;transform:translateY(-50%);background:rgba(255,255,255,0.9);border:1px solid var(--color-border-soft);border-radius:50%;width:40px;height:40px;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px rgba(0,0,0,0.1);z-index:10;transition:all 0.2s;" onmouseover = "this.style.background=\'white\';this.style.boxShadow=\'0 4px 12px rgba(0,0,0,0.15)\';" onmouseout = "this.style.background=\'rgba(255,255,255,0.9)\';this.style.boxShadow=\'0 2px 8px rgba(0,0,0,0.1)\';" > ';
html += '< 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" > < polyline points = "15 18 9 12 15 6" / > < / svg > ';
html += '< / button > ';
html += '< button id = "cgmCarouselNext" onclick = "navigateCGMCarousel(1)" style = "position:absolute;right:10px;top:50%;transform:translateY(-50%);background:rgba(255,255,255,0.9);border:1px solid var(--color-border-soft);border-radius:50%;width:40px;height:40px;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px rgba(0,0,0,0.1);z-index:10;transition:all 0.2s;" onmouseover = "this.style.background=\'white\';this.style.boxShadow=\'0 4px 12px rgba(0,0,0,0.15)\';" onmouseout = "this.style.background=\'rgba(255,255,255,0.9)\';this.style.boxShadow=\'0 2px 8px rgba(0,0,0,0.1)\';" > ';
html += '< 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" > < polyline points = "9 18 15 12 9 6" / > < / svg > ';
html += '< / button > ';
}
// Pagination Dots
if (data.reports.length > 1) {
html += '< div style = "display:flex;justify-content:center;gap:8px;margin-top:16px;" > ';
for (var i = 0; i < data.reports.length ; i + + ) {
var activeClass = i === 0 ? 'active' : '';
html += '< button class = "cgm-carousel-dot ' + activeClass + '" onclick = "goToCGMReport(' + i + ')" data-dot-index = "' + i + '" style = "width:10px;height:10px;border-radius:50%;border:none;background:' + (i === 0 ? 'var(--color-brand)' : 'var(--color-border-soft)') + ';cursor:pointer;transition:all 0.2s;padding:0;" > < / button > ';
}
html += '< / div > ';
}
html += '< / div > '; // End carousel container
html += '< / div > '; // End panel
}
html += '< / div > ';
container.innerHTML = html;
// Initialize carousel after rendering (with delay to ensure DOM is ready)
setTimeout(function() {
initCGMCarousel();
// Handle window resize to recalculate carousel
var resizeTimeout;
window.addEventListener('resize', function() {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(function() {
updateCGMCarouselPosition();
}, 250);
});
}, 300);
// Render chart if data exists
if (data.chart_data & & data.chart_data.length > 0 & & typeof Chart !== 'undefined') {
setTimeout(function() {
renderCGMChart(data.chart_data, data.thresholds);
}, 100);
}
}
function renderCGMChart(chartData, thresholds) {
var ctx = document.getElementById('cgmChart');
if (!ctx) return;
// Destroy existing chart
if (cgmChart) {
cgmChart.destroy();
}
// Prepare data
var labels = [];
var values = [];
for (var i = 0; i < chartData.length ; i + + ) {
var point = chartData[i];
labels.push(point.timestamp);
values.push(point.value);
}
// Thresholds
var lowThreshold = (thresholds & & thresholds.low) || 70;
var highThreshold = (thresholds & & thresholds.high) || 180;
cgmChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Glucose (mg/dL)',
data: values,
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.1,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: false,
suggestedMin: 50,
suggestedMax: 300
},
x: {
type: 'time',
time: {
unit: 'day'
}
}
},
plugins: {
annotation: {
annotations: {
lowLine: {
type: 'line',
yMin: lowThreshold,
yMax: lowThreshold,
borderColor: 'rgb(239, 68, 68)',
borderWidth: 2,
borderDash: [5, 5],
label: {
content: 'Low: ' + lowThreshold + ' mg/dL',
enabled: true
}
},
highLine: {
type: 'line',
yMin: highThreshold,
yMax: highThreshold,
borderColor: 'rgb(239, 68, 68)',
borderWidth: 2,
borderDash: [5, 5],
label: {
content: 'High: ' + highThreshold + ' mg/dL',
enabled: true
}
}
}
}
}
}
});
}
/* ── CGM Reports Carousel Navigation ──────────────────────────────────────── */
var cgmCarouselCurrentIndex = 0;
var cgmCarouselTotalSlides = 0;
function initCGMCarousel() {
var wrapper = document.getElementById('cgmReportsCarouselWrapper');
if (!wrapper) {
console.log('CGM Carousel: Wrapper not found');
return;
}
var slides = wrapper.querySelectorAll('.cgm-report-slide');
cgmCarouselTotalSlides = slides.length;
cgmCarouselCurrentIndex = 0;
console.log('CGM Carousel: Initialized with', cgmCarouselTotalSlides, 'slides');
if (cgmCarouselTotalSlides > 0) {
updateCGMCarouselPosition();
updateCGMCarouselDots();
}
}
function navigateCGMCarousel(direction) {
var wrapper = document.getElementById('cgmReportsCarouselWrapper');
if (!wrapper) return;
var slides = wrapper.querySelectorAll('.cgm-report-slide');
if (slides.length === 0) return;
cgmCarouselCurrentIndex += direction;
// Wrap around
if (cgmCarouselCurrentIndex < 0 ) {
cgmCarouselCurrentIndex = slides.length - 1;
} else if (cgmCarouselCurrentIndex >= slides.length) {
cgmCarouselCurrentIndex = 0;
}
updateCGMCarouselPosition();
updateCGMCarouselDots();
}
function goToCGMReport(index) {
var wrapper = document.getElementById('cgmReportsCarouselWrapper');
if (!wrapper) return;
var slides = wrapper.querySelectorAll('.cgm-report-slide');
if (index < 0 | | index > = slides.length) return;
cgmCarouselCurrentIndex = index;
updateCGMCarouselPosition();
updateCGMCarouselDots();
}
function updateCGMCarouselPosition() {
var wrapper = document.getElementById('cgmReportsCarouselWrapper');
if (!wrapper) {
console.log('CGM Carousel: Wrapper not found in updateCGMCarouselPosition');
return;
}
var slides = wrapper.querySelectorAll('.cgm-report-slide');
if (slides.length === 0) {
console.log('CGM Carousel: No slides found');
return;
}
// Get the container (parent of wrapper) to calculate pixel offset
var container = wrapper.parentElement;
if (!container) {
console.log('CGM Carousel: Container not found');
return;
}
var containerWidth = container.offsetWidth || container.clientWidth;
if (containerWidth === 0) {
console.log('CGM Carousel: Container width is 0, retrying...');
setTimeout(function() {
updateCGMCarouselPosition();
}, 100);
return;
}
// Calculate offset in pixels: move by container width * index
// Since each slide is 100% of container width, we move by container width for each slide
var offsetPixels = -cgmCarouselCurrentIndex * containerWidth;
wrapper.style.transform = 'translateX(' + offsetPixels + 'px)';
console.log('CGM Carousel: Moved to slide', cgmCarouselCurrentIndex, 'offset:', offsetPixels + 'px', 'container width:', containerWidth);
}
function updateCGMCarouselDots() {
var dots = document.querySelectorAll('.cgm-carousel-dot');
for (var i = 0; i < dots.length ; i + + ) {
if (i === cgmCarouselCurrentIndex) {
dots[i].style.background = 'var(--color-brand)';
dots[i].classList.add('active');
} else {
dots[i].style.background = 'var(--color-border-soft)';
dots[i].classList.remove('active');
}
}
}
< / script >
2026-02-20 15:28:41 -05:00
{{end}}