Files
FHIR-Sandbox/app/handlers/dashboard.go

196 lines
6.2 KiB
Go
Raw Normal View History

package handlers
import (
"log"
"net/http"
2026-02-20 23:59:46 -05:00
"time"
"github.com/AmanTahiliani/FHIR-Sandbox/app/fhir"
"github.com/AmanTahiliani/FHIR-Sandbox/app/middleware"
"github.com/AmanTahiliani/FHIR-Sandbox/app/models"
)
2026-02-20 23:59:46 -05:00
// ClinicalSummary holds pre-computed summary values for the Summary tab.
type ClinicalSummary struct {
LatestVitals []models.Observation
ActiveCondCount int
ActiveMedCount int
AbnormalLabCount int
AbnormalLabsRecent []models.Observation
}
// buildClinicalSummary computes the clinical summary from existing in-memory data.
// No additional DB or FHIR calls are made.
func buildClinicalSummary(obs []models.Observation, conds []models.Condition, meds []models.MedicationRequest) ClinicalSummary {
// Latest value per vital code.
latestVitals := latestObsPerCode(filterObsByCategory(obs, "vital-signs"))
activeConds := 0
for _, c := range conds {
if c.ClinicalStatus == "active" {
activeConds++
}
}
activeMeds := 0
for _, m := range meds {
if m.Status == "active" {
activeMeds++
}
}
// Abnormal labs within the last 30 days.
cutoff := time.Now().AddDate(0, 0, -30).Format("2006-01-02")
var abnormalLabs []models.Observation
for _, o := range obs {
if isAbnormalInterp(o.Interpretation) && o.EffectiveDate >= cutoff {
abnormalLabs = append(abnormalLabs, o)
}
}
return ClinicalSummary{
LatestVitals: latestVitals,
ActiveCondCount: activeConds,
ActiveMedCount: activeMeds,
AbnormalLabCount: len(abnormalLabs),
AbnormalLabsRecent: abnormalLabs,
}
}
// HandleDashboard renders the stable patient dashboard.
2026-02-20 17:17:26 -05:00
// On first load (no prior sync), automatically triggers a sync to populate data.
// All other clinical data is read from the local database; no live FHIR calls are
// made here. Use POST /dashboard/sync to refresh data from the EHR.
//
// GET /dashboard
func (h *Handler) HandleDashboard(w http.ResponseWriter, r *http.Request) {
sess := middleware.SessionFromContext(r.Context())
practitionerUser := middleware.UserFromContext(r.Context())
if sess == nil || practitionerUser == nil {
h.handleUnauthorized(w, r)
return
}
ehrURL := sess.EHRURL
patientID := sess.PatientFHIRID
2026-02-20 17:17:26 -05:00
// Allow overriding the patient context via a query parameter.
if overrideID := r.URL.Query().Get("patient_id"); overrideID != "" {
patientID = overrideID
}
// Check if this is the first load and trigger auto-sync
latestSync, err := h.store.LatestSync(patientID, ehrURL)
if err != nil {
log.Printf("handlers: dashboard LatestSync Patient/%s: %v", patientID, err)
}
if latestSync == nil {
// First load — redirect to sync endpoint for auto-sync
syncURL := "/dashboard/sync"
if overrideID := r.URL.Query().Get("patient_id"); overrideID != "" {
syncURL += "?patient_id=" + overrideID
}
http.Redirect(w, r, syncURL, http.StatusSeeOther)
return
}
// Fetch patient demographics from the FHIR server. This is a cheap single
// resource call and keeps the patient card always current.
fhirClient := fhir.NewClient(ehrURL, sess.AccessToken)
patient, err := fhirClient.GetPatient(patientID)
if err != nil {
log.Printf("handlers: dashboard fetch Patient/%s failed: %v", patientID, err)
h.renderError(w, http.StatusBadGateway, "Failed to fetch patient details from the FHIR server.")
return
}
patientUser := fhir.ExtractUserFromPatient(patient, ehrURL)
// Read clinical data from the local database.
observations, err := h.store.ListObservations(patientID, ehrURL)
if err != nil {
log.Printf("handlers: dashboard ListObservations Patient/%s: %v", patientID, err)
}
conditions, err := h.store.ListConditions(patientID, ehrURL)
if err != nil {
log.Printf("handlers: dashboard ListConditions Patient/%s: %v", patientID, err)
}
docRefs, err := h.store.ListDocumentReferences(patientID, ehrURL)
if err != nil {
log.Printf("handlers: dashboard ListDocumentReferences Patient/%s: %v", patientID, err)
}
2026-02-20 17:17:26 -05:00
medications, err := h.store.ListMedicationRequests(patientID, ehrURL)
if err != nil {
2026-02-20 17:17:26 -05:00
log.Printf("handlers: dashboard ListMedicationRequests Patient/%s: %v", patientID, err)
}
2026-02-20 17:17:26 -05:00
allergies, err := h.store.ListAllergyIntolerances(patientID, ehrURL)
if err != nil {
log.Printf("handlers: dashboard ListAllergyIntolerances Patient/%s: %v", patientID, err)
}
2026-02-20 23:59:46 -05:00
immunizations, err := h.store.ListImmunizations(patientID, ehrURL)
if err != nil {
log.Printf("handlers: dashboard ListImmunizations Patient/%s: %v", patientID, err)
}
procedures, err := h.store.ListProcedures(patientID, ehrURL)
if err != nil {
log.Printf("handlers: dashboard ListProcedures Patient/%s: %v", patientID, err)
}
encounters, err := h.store.ListEncounters(patientID, ehrURL)
if err != nil {
log.Printf("handlers: dashboard ListEncounters Patient/%s: %v", patientID, err)
}
summary := buildClinicalSummary(observations, conditions, medications)
2026-02-20 17:17:26 -05:00
synced := r.URL.Query().Get("synced") == "true"
h.render(w, "dashboard.html", dashboardData{
Patient: patientUser,
Practitioner: practitionerUser,
RawPatient: patient,
Observations: observations,
Conditions: conditions,
DocumentReferences: docRefs,
2026-02-20 17:17:26 -05:00
Medications: medications,
Allergies: allergies,
2026-02-20 23:59:46 -05:00
Immunizations: immunizations,
Procedures: procedures,
Encounters: encounters,
Summary: summary,
LatestSync: latestSync,
Session: sess,
2026-02-20 17:17:26 -05:00
Synced: synced,
})
}
// dashboardData is the view model passed to the dashboard template.
type dashboardData struct {
Patient *models.User
Practitioner *models.User
RawPatient *fhir.Patient
Observations []models.Observation
Conditions []models.Condition
DocumentReferences []models.DocumentReference
2026-02-20 17:17:26 -05:00
Medications []models.MedicationRequest
Allergies []models.AllergyIntolerance
2026-02-20 23:59:46 -05:00
Immunizations []models.Immunization
Procedures []models.Procedure
Encounters []models.Encounter
Summary ClinicalSummary
LatestSync *models.PatientSync
Session *models.Session
2026-02-20 17:17:26 -05:00
Synced bool
}
// handleUnauthorized redirects to root for dashboard requests.
func (h *Handler) handleUnauthorized(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusSeeOther)
}