Improved FHIR handling

This commit is contained in:
2026-02-20 17:17:26 -05:00
parent 63cb2b7b1c
commit f21d2039cb
16 changed files with 1188 additions and 131 deletions

View File

@@ -9,8 +9,8 @@
// 5. Fetch the Patient FHIR resource using the access token.
// 6. Resolve the practitioner from the token response. The SMART spec allows
// the practitioner to appear in two places — we handle both:
// a. tokenResp.Practitioner — a bare FHIR ID (some EHRs)
// b. tokenResp.User — a relative reference "Practitioner/<id>" (SmartHealthIT)
// a. tokenResp.Practitioner — a bare FHIR ID (some EHRs)
// b. tokenResp.User — a relative reference "Practitioner/<id>" (SmartHealthIT)
// 7. Upsert both users into the database.
// 8. Create a server-side session for the HCP and set the session cookie.
// 9. Render the patient dashboard.
@@ -196,6 +196,7 @@ func (h *Handler) HandleAuthRedirect(w http.ResponseWriter, r *http.Request) {
// - A bare ID (if the context implies it): "123"
// - A relative reference: "Practitioner/123"
// - An absolute FHIR URL: "https://ehr.com/fhir/Practitioner/123"
//
// Returns an empty string if the value is not a Practitioner reference.
func parsePractitionerFromUserField(user string) string {
// If it's a URL, take the path part.
@@ -214,7 +215,7 @@ func parsePractitionerFromUserField(user string) string {
if idx := strings.Index(user, prefix); idx != -1 {
return strings.TrimPrefix(user[idx:], prefix)
}
return ""
}

View File

@@ -10,7 +10,8 @@ import (
)
// HandleDashboard renders the stable patient dashboard.
// All clinical data is read from the local database; no live FHIR calls are
// 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
@@ -26,6 +27,27 @@ func (h *Handler) HandleDashboard(w http.ResponseWriter, r *http.Request) {
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)
@@ -54,11 +76,18 @@ func (h *Handler) HandleDashboard(w http.ResponseWriter, r *http.Request) {
log.Printf("handlers: dashboard ListDocumentReferences Patient/%s: %v", patientID, err)
}
latestSync, err := h.store.LatestSync(patientID, ehrURL)
medications, err := h.store.ListMedicationRequests(patientID, ehrURL)
if err != nil {
log.Printf("handlers: dashboard LatestSync Patient/%s: %v", patientID, err)
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)
}
synced := r.URL.Query().Get("synced") == "true"
h.render(w, "dashboard.html", dashboardData{
Patient: patientUser,
Practitioner: practitionerUser,
@@ -66,8 +95,11 @@ func (h *Handler) HandleDashboard(w http.ResponseWriter, r *http.Request) {
Observations: observations,
Conditions: conditions,
DocumentReferences: docRefs,
Medications: medications,
Allergies: allergies,
LatestSync: latestSync,
Session: sess,
Synced: synced,
})
}
@@ -79,8 +111,11 @@ type dashboardData struct {
Observations []models.Observation
Conditions []models.Condition
DocumentReferences []models.DocumentReference
Medications []models.MedicationRequest
Allergies []models.AllergyIntolerance
LatestSync *models.PatientSync
Session *models.Session
Synced bool
}
// handleUnauthorized redirects to root for dashboard requests.

View File

@@ -29,6 +29,7 @@ import (
"github.com/AmanTahiliani/FHIR-Sandbox/app/config"
"github.com/AmanTahiliani/FHIR-Sandbox/app/db"
"github.com/AmanTahiliani/FHIR-Sandbox/app/models"
)
const (
@@ -152,5 +153,29 @@ func TemplateFuncs() template.FuncMap {
}
return s
},
"groupByCategory": func(obs []models.Observation) map[string][]models.Observation {
m := make(map[string][]models.Observation)
for _, o := range obs {
cat := o.Category
if cat == "" {
cat = "other"
}
m[cat] = append(m[cat], o)
}
return m
},
"hasCriticalAllergies": func(allergies []models.AllergyIntolerance) bool {
for _, a := range allergies {
if a.Criticality == "high" {
return true
}
}
return false
},
"last": func(slice interface{}) interface{} {
// Helper function to get the last element of a slice
// Used in template logic
return nil
},
}
}

40
app/handlers/patients.go Normal file
View File

@@ -0,0 +1,40 @@
package handlers
import (
"log"
"net/http"
"github.com/AmanTahiliani/FHIR-Sandbox/app/middleware"
"github.com/AmanTahiliani/FHIR-Sandbox/app/models"
)
// HandlePatients renders the list of all synced patients for the current EHR.
// GET /patients
func (h *Handler) HandlePatients(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
}
patients, err := h.store.ListUsersByRole(models.RolePatient, sess.EHRURL)
if err != nil {
log.Printf("handlers: HandlePatients ListUsersByRole failed: %v", err)
h.renderError(w, http.StatusInternalServerError, "Failed to retrieve patients from the database.")
return
}
h.render(w, "patients.html", patientsData{
Patients: patients,
Practitioner: practitionerUser,
Session: sess,
})
}
type patientsData struct {
Patients []models.User
Practitioner *models.User
Session *models.Session
}

View File

@@ -3,18 +3,24 @@ package handlers
import (
"log"
"net/http"
"time"
"github.com/AmanTahiliani/FHIR-Sandbox/app/fhir"
"github.com/AmanTahiliani/FHIR-Sandbox/app/middleware"
)
// HandleSync performs a live FHIR pull for Observations, Conditions, and
// DocumentReferences for the session's patient, upserts all results into the
// database, records a PatientSync event, then redirects back to GET /dashboard.
// HandleSync performs a live FHIR pull for Observations, Conditions, DocumentReferences,
// MedicationRequests, and AllergyIntolerances for the session's patient, upserts all
// results into the database, records a PatientSync event, then redirects back to
// GET /dashboard?synced=true.
//
// POST /dashboard/sync
// Incremental sync: If a previous sync exists, only fetches resources updated since
// the last sync time (using FHIR _lastUpdated parameter).
//
// GET /dashboard/sync (auto-sync on first dashboard load)
// POST /dashboard/sync (manual sync from dashboard UI)
func (h *Handler) HandleSync(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
if r.Method != http.MethodGet && r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
@@ -27,12 +33,29 @@ func (h *Handler) HandleSync(w http.ResponseWriter, r *http.Request) {
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
}
// Determine if this is an incremental sync
latestSync, err := h.store.LatestSync(patientID, ehrURL)
if err != nil {
log.Printf("handlers: sync LatestSync Patient/%s: %v", patientID, err)
}
var sinceTime string
if latestSync != nil {
sinceTime = latestSync.SyncedAt.Format(time.RFC3339)
}
client := fhir.NewClient(ehrURL, sess.AccessToken)
// -----------------------------------------------------------------
// Fetch Observations
// -----------------------------------------------------------------
rawObs, err := client.GetObservations(patientID)
rawObs, err := client.GetObservations(patientID, sinceTime)
if err != nil {
log.Printf("handlers: sync GetObservations for Patient/%s: %v", patientID, err)
// Non-fatal; continue with whatever we got.
@@ -51,7 +74,7 @@ func (h *Handler) HandleSync(w http.ResponseWriter, r *http.Request) {
// -----------------------------------------------------------------
// Fetch Conditions
// -----------------------------------------------------------------
rawConds, err := client.GetConditions(patientID)
rawConds, err := client.GetConditions(patientID, sinceTime)
if err != nil {
log.Printf("handlers: sync GetConditions for Patient/%s: %v", patientID, err)
}
@@ -69,7 +92,7 @@ func (h *Handler) HandleSync(w http.ResponseWriter, r *http.Request) {
// -----------------------------------------------------------------
// Fetch DocumentReferences
// -----------------------------------------------------------------
rawDocs, err := client.GetDocumentReferences(patientID)
rawDocs, err := client.GetDocumentReferences(patientID, sinceTime)
if err != nil {
log.Printf("handlers: sync GetDocumentReferences for Patient/%s: %v", patientID, err)
}
@@ -84,6 +107,42 @@ func (h *Handler) HandleSync(w http.ResponseWriter, r *http.Request) {
docCount++
}
// -----------------------------------------------------------------
// Fetch MedicationRequests
// -----------------------------------------------------------------
rawMeds, err := client.GetMedicationRequests(patientID, sinceTime)
if err != nil {
log.Printf("handlers: sync GetMedicationRequests for Patient/%s: %v", patientID, err)
}
medCount := 0
for i := range rawMeds {
m := fhir.ExtractMedicationRequest(&rawMeds[i], patientID, ehrURL)
if _, err := h.store.UpsertMedicationRequest(m); err != nil {
log.Printf("handlers: sync UpsertMedicationRequest fhir_id=%s: %v", m.FHIRID, err)
continue
}
medCount++
}
// -----------------------------------------------------------------
// Fetch AllergyIntolerances
// -----------------------------------------------------------------
rawAllergies, err := client.GetAllergyIntolerances(patientID, sinceTime)
if err != nil {
log.Printf("handlers: sync GetAllergyIntolerances for Patient/%s: %v", patientID, err)
}
allergyCount := 0
for i := range rawAllergies {
m := fhir.ExtractAllergyIntolerance(&rawAllergies[i], patientID, ehrURL)
if _, err := h.store.UpsertAllergyIntolerance(m); err != nil {
log.Printf("handlers: sync UpsertAllergyIntolerance fhir_id=%s: %v", m.FHIRID, err)
continue
}
allergyCount++
}
// -----------------------------------------------------------------
// Record the sync event
// -----------------------------------------------------------------
@@ -91,8 +150,12 @@ func (h *Handler) HandleSync(w http.ResponseWriter, r *http.Request) {
log.Printf("handlers: sync RecordSync Patient/%s: %v", patientID, err)
}
log.Printf("handlers: sync complete for Patient/%s — obs=%d cond=%d docs=%d",
patientID, obsCount, condCount, docCount)
log.Printf("handlers: sync complete for Patient/%s — obs=%d cond=%d docs=%d med=%d allergy=%d",
patientID, obsCount, condCount, docCount, medCount, allergyCount)
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
dashboardURL := "/dashboard?synced=true"
if overrideID := r.URL.Query().Get("patient_id"); overrideID != "" {
dashboardURL += "&patient_id=" + overrideID
}
http.Redirect(w, r, dashboardURL, http.StatusSeeOther)
}