mirror of
https://github.com/AmanTahiliani/FHIR-Sandbox.git
synced 2026-08-07 11:53:56 -04:00
196 lines
6.2 KiB
Go
196 lines
6.2 KiB
Go
package handlers
|
|
|
|
import (
|
|
"log"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/AmanTahiliani/FHIR-Sandbox/app/fhir"
|
|
"github.com/AmanTahiliani/FHIR-Sandbox/app/middleware"
|
|
"github.com/AmanTahiliani/FHIR-Sandbox/app/models"
|
|
)
|
|
|
|
// 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.
|
|
// 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
|
|
|
|
// 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)
|
|
}
|
|
|
|
medications, err := h.store.ListMedicationRequests(patientID, ehrURL)
|
|
if err != nil {
|
|
log.Printf("handlers: dashboard ListMedicationRequests Patient/%s: %v", patientID, err)
|
|
}
|
|
|
|
allergies, err := h.store.ListAllergyIntolerances(patientID, ehrURL)
|
|
if err != nil {
|
|
log.Printf("handlers: dashboard ListAllergyIntolerances Patient/%s: %v", patientID, err)
|
|
}
|
|
|
|
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)
|
|
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,
|
|
Medications: medications,
|
|
Allergies: allergies,
|
|
Immunizations: immunizations,
|
|
Procedures: procedures,
|
|
Encounters: encounters,
|
|
Summary: summary,
|
|
LatestSync: latestSync,
|
|
Session: sess,
|
|
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
|
|
Medications []models.MedicationRequest
|
|
Allergies []models.AllergyIntolerance
|
|
Immunizations []models.Immunization
|
|
Procedures []models.Procedure
|
|
Encounters []models.Encounter
|
|
Summary ClinicalSummary
|
|
LatestSync *models.PatientSync
|
|
Session *models.Session
|
|
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)
|
|
}
|