mirror of
https://github.com/AmanTahiliani/FHIR-Sandbox.git
synced 2026-08-07 19:56:17 -04:00
Improved FHIR handling
This commit is contained in:
@@ -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 ""
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
40
app/handlers/patients.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user