diff --git a/.Net Capstone Project/MedplumFhirService.cs b/.Net Capstone Project/MedplumFhirService.cs new file mode 100644 index 0000000..91a07e2 --- /dev/null +++ b/.Net Capstone Project/MedplumFhirService.cs @@ -0,0 +1,282 @@ +using ClinicalInsightsPro.API.Data; +using ClinicalInsightsPro.API.DTOs; +using ClinicalInsightsPro.API.Services.Interfaces; +using Microsoft.EntityFrameworkCore; +using System.Text.Json; + +namespace ClinicalInsightsPro.API.Services; + +public class MedplumFhirService : IMedplumFhirService +{ + private readonly ApplicationDbContext _db; + private readonly ILogger _logger; + + public MedplumFhirService( + ApplicationDbContext db, + ILogger logger) + { + _db = db; + _logger = logger; + } + + public Task> GetMedplumResourcesAsync(string resourceType, string? patientId = null) + { + // Offline/demo mode has normalized tables, not the raw Medplum store. + // The unified Medplum browser is intentionally live-only so the UI + // always shows actual FHIR JSON when the feature is enabled. + throw new InvalidOperationException( + "The Medplum resource browser requires Fhir:UseLiveMedplum=true. " + + "Set MEDPLUM_USE_LIVE=true and configure MEDPLUM_BASE_URL, " + + "MEDPLUM_TOKEN_URL, MEDPLUM_CLIENT_ID and MEDPLUM_CLIENT_SECRET."); + } + + public async Task> GetPatientListAsync() + { + var patients = await _db.Patient + .AsNoTracking() + .OrderBy(x => x.GivenName) + .ThenBy(x => x.FamilyName) + .ToListAsync(); + + return patients.Select(p => new PatientSummaryDto + { + Id = p.Id, + FullName = $"{p.GivenName} {p.FamilyName}", + Age = CalculateAge(p.BirthDate), + Gender = p.Gender, + MedicalRecordNumber = p.MedicalRecordNumber + }).ToList(); + } + + public async Task GetPatientChartAsync(string patientId) + { + var patient = await _db.Patient + .AsNoTracking() + .FirstOrDefaultAsync(p => p.Id == patientId); + + if (patient == null) + { + throw new KeyNotFoundException( + $"Patient '{patientId}' was not found."); + } + + var conditions = await _db.Condition + .AsNoTracking() + .Where(x => x.PatientId == patientId) + .OrderByDescending(x => x.RecordedDate) + .ToListAsync(); + + var observations = await _db.Observation + .AsNoTracking() + .Where(x => x.PatientId == patientId) + .OrderByDescending(x => x.EffectiveDateTime) + .ToListAsync(); + + var allergies = await _db.AllergyIntolerance + .AsNoTracking() + .Where(x => x.PatientId == patientId) + .ToListAsync(); + + var medications = await _db.MedicationRequest + .AsNoTracking() + .Where(x => x.PatientId == patientId) + .ToListAsync(); + + var encounters = await _db.Encounter + .AsNoTracking() + .Where(x => x.PatientId == patientId) + .OrderByDescending(x => x.PeriodStart) + .ToListAsync(); + + _logger.LogInformation( + "Retrieved chart for patient {PatientId}", + patientId); + + return new PatientChartDto + { + PatientId = patient.Id, + + FullName = $"{patient.GivenName} {patient.FamilyName}", + + Age = CalculateAge(patient.BirthDate), + + MedicalRecordNumber = patient.MedicalRecordNumber, + + IdentifierSystem = patient.IdentifierSystem, + IdentifierValue = patient.IdentifierValue, + + NameUse = patient.NameUse, + Prefix = patient.Prefix, + + GivenName = patient.GivenName, + FamilyName = patient.FamilyName, + + TelecomSystem = patient.TelecomSystem, + TelecomUse = patient.TelecomUse, + TelecomValue = patient.TelecomValue, + + Gender = patient.Gender, + + BloodGroup = patient.BloodGroup, + Email = patient.Email, + + BirthDate = patient.BirthDate, + + AddressUse = patient.AddressUse, + AddressType = patient.AddressType, + AddressLine = patient.AddressLine, + + City = patient.City, + State = patient.State, + PostalCode = patient.PostalCode, + + MaritalStatusSystem = patient.MaritalStatusSystem, + MaritalStatusCode = patient.MaritalStatusCode, + MaritalStatusDisplay = patient.MaritalStatusDisplay, + + ContactNameUse = patient.ContactNameUse, + ContactPrefix = patient.ContactPrefix, + + ContactTelecomSystem = patient.ContactTelecomSystem, + ContactTelecomUse = patient.ContactTelecomUse, + ContactTelecomValue = patient.ContactTelecomValue, + + ContactAddressUse = patient.ContactAddressUse, + ContactAddressType = patient.ContactAddressType, + ContactAddressLine = patient.ContactAddressLine, + + ContactCity = patient.ContactCity, + ContactState = patient.ContactState, + ContactPostalCode = patient.ContactPostalCode, + + ContactGender = patient.ContactGender, + ContactOrganizationId = patient.ContactOrganizationId, + + ContactPeriodStart = patient.ContactPeriodStart, + ContactPeriodEnd = patient.ContactPeriodEnd, + + ManagingOrganizationId = patient.ManagingOrganizationId, + + PhotoContentType = patient.PhotoContentType, + PhotoUrl = patient.PhotoUrl, + PhotoTitle = patient.PhotoTitle, + + GeneralPractitionerId = patient.GeneralPractitionerId, + + Conditions = conditions.Select(c => new ChartConditionDto + { + Id = c.Id, + Code = c.ConditionCode, + Display = c.ConditionDisplay, + ClinicalStatus = c.ClinicalStatusDisplay, + VerificationStatus = c.VerificationStatusDisplay, + Severity = c.SeverityDisplay, + OnsetDate = c.OnsetDateTime, + AbatementDate = c.AbatementDateTime, + RecordedDate = c.RecordedDate + }).ToList(), + + Allergies = allergies.Select(a => new ChartAllergyDto + { + Id = a.Id, + Type = a.Type, + ClinicalStatus = a.ClinicalStatus, + VerificationStatus = a.VerificationStatus, + Criticality = a.Criticality, + RecorderPractitionerId = a.RecorderPractitionerId + }).ToList(), + + Medications = medications.Select(m => new ChartMedicationDto + { + Id = m.Id, + MedicationCode = m.MedicationCode, + MedicationDisplay = m.MedicationDisplay, + Status = m.Status, + Intent = m.Intent, + Priority = m.Priority + }).ToList(), + + Observations = observations.Select(o => new ChartObservationDto + { + Id = o.Id, + Code = o.ObservationCode, + Display = o.ObservationDisplay, + Status = o.Status, + CategoryDisplay = o.CategoryDisplay, + InterpretationCode = o.InterpretationCode, + InterpretationDisplay = o.InterpretationDisplay, + EffectiveDate = o.EffectiveDateTime, + NoteText = o.NoteText + }).ToList(), + + Encounters = encounters.Select(e => new ChartEncounterDto + { + Id = e.Id, + Status = e.Status, + ClassCode = e.ClassCode, + ClassDisplay = e.ClassDisplay, + TypeDisplay = e.TypeDisplay, + ServiceTypeDisplay = e.ServiceTypeDisplay, + PeriodStart = e.PeriodStart, + PeriodEnd = e.PeriodEnd + }).ToList() + }; + } + + private static int CalculateAge(DateTime birthDate) + { + var today = DateTime.UtcNow; + + var age = today.Year - birthDate.Year; + + if (birthDate.Date > today.AddYears(-age)) + { + age--; + } + + return age; + } + + // ---- Two-way sync additions ---- + // This class is the OFFLINE/local implementation (used when + // Fhir:UseLiveMedplum=false) — it reads/writes only the local database + // and never talks to a real Medplum server, so there is nothing to push + // or pull here. These methods exist only so this class still satisfies + // IMedplumFhirService; they throw/no-op with a clear message instead of + // silently pretending to sync. Set MEDPLUM_USE_LIVE=true in .env to + // switch the app over to LiveMedplumFhirService, which implements the + // real sync against Medplum's FHIR API. + public Task CreatePatientInMedplumAsync(Models.Patient patient) + { + _logger.LogWarning( + "CreatePatientInMedplumAsync called while running in offline/local mode " + + "(Fhir:UseLiveMedplum=false) — patient {Id} was saved locally only, not pushed to Medplum. " + + "Set MEDPLUM_USE_LIVE=true in .env to enable Medplum sync.", patient.Id); + return Task.FromResult(string.Empty); + } + + public Task UpdatePatientInMedplumAsync(Models.Patient patient) + { + _logger.LogWarning( + "UpdatePatientInMedplumAsync called while running in offline/local mode " + + "(Fhir:UseLiveMedplum=false) — patient {Id} was updated locally only, not pushed to Medplum.", patient.Id); + return Task.CompletedTask; + } + + public Task DeletePatientInMedplumAsync(string medplumId) + { + _logger.LogWarning( + "DeletePatientInMedplumAsync called while running in offline/local mode " + + "(Fhir:UseLiveMedplum=false) — nothing was deleted in Medplum."); + return Task.CompletedTask; + } + + public Task> PullAllPatientsFromMedplumAsync() + { + _logger.LogWarning( + "PullAllPatientsFromMedplumAsync called while running in offline/local mode " + + "(Fhir:UseLiveMedplum=false) — returning an empty list. Set MEDPLUM_USE_LIVE=true in .env " + + "to pull real data from Medplum."); + return Task.FromResult(new List()); + } +} \ No newline at end of file