Upload files to ".Net Capstone Project"
This commit is contained in:
parent
50f53dcf7b
commit
81c0622c9c
317
.Net Capstone Project/MedplumFhirServicevv1.cs
Normal file
317
.Net Capstone Project/MedplumFhirServicevv1.cs
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
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<MedplumFhirService> _logger;
|
||||||
|
|
||||||
|
public MedplumFhirService(
|
||||||
|
ApplicationDbContext db,
|
||||||
|
ILogger<MedplumFhirService> logger)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<List<JsonElement>> 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<List<PatientSummaryDto>> 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<PatientChartDto> 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<string> 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<List<Models.Patient>> 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<Models.Patient>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Practitioner two-way sync (offline no-ops, mirrors Patient above) ----
|
||||||
|
public Task<string> CreatePractitionerInMedplumAsync(Models.Practitioner practitioner)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"CreatePractitionerInMedplumAsync called while running in offline/local mode " +
|
||||||
|
"(Fhir:UseLiveMedplum=false) — practitioner {Id} was saved locally only, not pushed to Medplum. " +
|
||||||
|
"Set MEDPLUM_USE_LIVE=true in .env to enable Medplum sync.", practitioner.Id);
|
||||||
|
return Task.FromResult(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task UpdatePractitionerInMedplumAsync(Models.Practitioner practitioner)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"UpdatePractitionerInMedplumAsync called while running in offline/local mode " +
|
||||||
|
"(Fhir:UseLiveMedplum=false) — practitioner {Id} was updated locally only, not pushed to Medplum.", practitioner.Id);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DeletePractitionerInMedplumAsync(string medplumId)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"DeletePractitionerInMedplumAsync called while running in offline/local mode " +
|
||||||
|
"(Fhir:UseLiveMedplum=false) — nothing was deleted in Medplum.");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<List<Models.Practitioner>> PullAllPractitionersFromMedplumAsync()
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"PullAllPractitionersFromMedplumAsync 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<Models.Practitioner>());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user