using adas_core.Application.Services.Interfaces;
using adas_core.Domain.Enums;
using adas_core.Domain.Models;
using adas_core.Domain.Models.AppSettings;
using adas_core.Domain.Models.Pumps;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
namespace adas_core.Application.Customizations.H12O.UCIN;
///
/// Implements the UCIN-specific clinical calculations that derive secondary
/// values (Complexity, Oxygenation_Index, Respiratory, IntravenousLines,
/// Medication, ERMedication, Monitor, Surgery, Temp_Gradient, TAm alert, etc.) from incoming
/// raw observations and active entries, using the catalogue of
/// codes, groups, and types configured in .
///
///
public class CalculatedObservations : ICalculatedObservations
{
private readonly List _complexityObservations =
["Respiratory", "IntravenousLines", "Medication", "Surgery", "Weight_Newborn", "Weight_Current", "Monitor"];
private readonly List _ecmo = [];
private readonly List _electroencephalogram = [];
private readonly List _highFrequencyVentilation = [];
//private readonly List _shift = [];
private readonly Tuple>[] _intraVenousLinesTypeValue =
[
Tuple.Create(IntraVenousLineTypes.Artery, 5,
new List { "Catéter arterial", "Catéter UMBILICAL arteria" }),
Tuple.Create(IntraVenousLineTypes.CentralVein, 4,
new List { "Catéter venoso CENTRAL", "Catéter UMBILICAL vena" }),
Tuple.Create(IntraVenousLineTypes.Picc, 3, new List { "Catéter PICC" }),
Tuple.Create(IntraVenousLineTypes.MiddleLine, 2,
new List { "Catéter línea media", "Catéter EPICUTÁNEO PERIFÉRICO" }),
Tuple.Create(IntraVenousLineTypes.Peripheral, 1, new List { "Catéter Venoso PERIFÉRICO" })
];
private readonly List _invasiveVentilation = [];
private readonly ILogger _logger;
private readonly List _medicationBolus = [];
//private readonly Lazy medicineService;
private readonly IMedicineService _medicineService;
private readonly List _nonInvasiveVentilation = [];
//private readonly List Oni = new List();
private readonly List _notesIndicatingMedication = [];
private readonly Lazy _observationService;
private readonly List _oniCodes = [];
private readonly List _regionalBrainSaturation = [];
private readonly List _respiratory = [];
private readonly Tuple>[] _respiratoryTypeValue =
[
// Tuple.Create(RespiratoryTypes.INO, 10, new List{ ""}),
Tuple.Create(RespiratoryTypes.Vafo, 5, new List { "V.A.F.O." }),
Tuple.Create(RespiratoryTypes.Vmc, 3, new List { "V.M.C." }),
Tuple.Create(RespiratoryTypes.Vmni, 2, new List { "V.N.I. Ciclada", "CPAP" }),
Tuple.Create(RespiratoryTypes.NasalCannulas, 1, new List { "Alto Flujo", "Bajo Flujo" }),
Tuple.Create(RespiratoryTypes.None, 0, new List { "Sin assistance respiratoria" })
];
private readonly List _rxaStatus = ["Concluido"];
private readonly List _surgery = [];
private readonly List _surgeryText = [];
private readonly List _transcutanous = [];
private readonly Lazy _treatmentService;
///
/// Initializes a new instance of the class,
/// resolving its dependencies (observation, medicine, and treatment services, plus the logger)
/// from the supplied and loading the configured code catalogues
/// (EEG, bolus medications, transcutaneous, regional brain saturation, surgery, respiratory,
/// ONi, ventilation modes, ECMO, and notes indicating medication) from .
///
/// The application's service provider used to resolve the required dependencies and configuration.
///
public CalculatedObservations(IServiceProvider serviceProvider)
{
_observationService = serviceProvider.GetRequiredService>(); //observationService;
_medicineService = serviceProvider.GetRequiredService(); //medicineService;
_treatmentService = serviceProvider.GetRequiredService>(); //treatmentService;
var apiSettings = serviceProvider.GetRequiredService>(); //apiSettings;
_logger = serviceProvider.GetRequiredService>();
var electroencephalogram = apiSettings.Value.Electroencephalogram ?? null;
electroencephalogram?.ForEach(x => _electroencephalogram.Add(x.Trim()));
var medicationBolus = apiSettings.Value.MedicationBolus ?? null;
medicationBolus?.ForEach(x => _medicationBolus.Add(x.Trim()));
var transcutaneous = apiSettings.Value.Transcutaneous ?? null;
transcutaneous?.ForEach(x => _transcutanous.Add(x.Trim()));
var regionalBrainSaturation = apiSettings.Value.RegionalBrainSaturation ?? null;
regionalBrainSaturation?.ForEach(x => _regionalBrainSaturation.Add(x.Trim()));
var surgery = apiSettings.Value.Surgery ?? null;
surgery?.ForEach(x => _surgery.Add(x.Trim()));
var surgeryText = apiSettings.Value.SurgeryText ?? null;
surgeryText?.ForEach(x => _surgeryText.Add(x.Trim()));
var respiratory = apiSettings.Value.Respiratory ?? null;
respiratory?.ForEach(x => _respiratory.Add(x.Trim()));
//inoCode = Respiratory.LastOrDefault();
var oni = apiSettings.Value.Oni ?? null;
oni?.ForEach(x => _oniCodes.Add(x.Trim()));
var highFrequencyVentilation = apiSettings.Value.HighFrequencyVentilation ?? null;
highFrequencyVentilation?.ForEach(x => _highFrequencyVentilation.Add(x.Trim()));
var invasiveVentilation = apiSettings.Value.InvasiveVentilation ?? null;
invasiveVentilation?.ForEach(x => _invasiveVentilation.Add(x.Trim()));
var nonInvasiveVentilation = apiSettings.Value.NonInvasiveVentilation ?? null;
nonInvasiveVentilation?.ForEach(x => _nonInvasiveVentilation.Add(x.Trim()));
//var shift = apiSettings.Value.Shift ?? null;
//shift?.ForEach(x => _shift.Add(x.Trim()));
var notesIndicatingMedication =
apiSettings.Value.NotesIndicatingMedication ?? null;
notesIndicatingMedication?.ForEach(x => _notesIndicatingMedication.Add(x.Trim()));
var ecmo = apiSettings.Value.Ecmo ?? null;
ecmo?.ForEach(x => _ecmo.Add(x.Trim()));
}
///
/// Applies the appropriate clinical calculations to a raw patient observation based on its
/// , code, coding system, and expiration state, producing
/// derived observations (respiratory assistance, oxygenation index, ventilation mode, TAm alert,
/// parsed weight, temperature gradient, intravenous lines, monitor, surgery expiration, complexity,
/// and hourly/temperature time-increment handling).
///
/// The concrete observation type, deriving from .
/// The observation to map or transform in-place.
///
/// Reserved for future use. When , restricts the mapping strategy to
/// name-based lookups only.
///
///
/// A that resolves to the (possibly transformed) observation, or
/// when the input observation has no name.
///
///
public async Task Map(T obs, bool onlyByName) where T : BasePatientObservation
{
// Early exit: if the observation has no name, there's nothing to map
if (string.IsNullOrEmpty(obs.Name)) return obs;
// Respiratory observations: only process SNOMED-coded respiratory metrics
if (!string.IsNullOrEmpty(obs.Code) && _respiratory.Contains(obs.Code) && obs.CodingSystem == "SNM")
{
_logger.LogDebug("Mapping observation Respiratorio {obs}", obs);
await CalculateAsistResp(obs);
}
// Oxygenation index calculation: triggered by air pressure, FiO2, or PaO2 readings
if (obs.Name is "AirPressure_Mean" or "FiO2" or "PaO2") await CalculateOxygenationIndex(obs);
// Ventilation mode evaluation: processes respiratory mode changes
if (obs.Name == "Resp_Mode")
{
_logger.LogDebug("Mapping Resp mode observation {obs}", obs);
await CalculateVentilationMode(obs);
}
// Mean arterial pressure (TAm) alerting: includes gestational age considerations
if (obs.Name is "TAm" or "Age_Gestational_Fixed" or "Age_Gestational") await CalculateTAmAlert(obs);
// Weight parsing: only for active (non-expired) patient observations
if (obs.Name is "Weight_Newborn" or "Weight_Current")
if (obs is PatientObservation { Expired: false })
obs = (T)await ParseWeight(obs);
// Temperature gradient calculation between incubator and patient
if (obs is { Name: "Temp_Incubator" } or { Name: "Temp_Patient" }) await CalculateTempGradient(obs);
// Intravenous lines observation processing with potential transformation
if (obs is { Name: "IntravenousLinesObs" })
{
var intraObs = await CalculateIntravenousLineObservation(obs);
// Apply the transformed observation if calculation produced a result
if (intraObs != null)
obs = (T)intraObs;
}
// Regional brain saturation monitoring: pCO2tc or transcutaneous O2 with specific codes
if (obs.Name is "pCO2tc" or "Transcutaneous_O2")
if (obs.Code != null && _regionalBrainSaturation.Contains(obs.Code))
await CalculateMonitor(obs);
// Surgery-related expiration check
if (obs.Name == "Surgery") await CheckSurgeryExpired(obs);
// Complexity calculations for predefined complex observation types
if (obs.Name != null && _complexityObservations.Contains(obs.Name))
{
_logger.LogDebug("Mapping observation Complexity {obs}", obs);
await CalculateComplexity(obs);
}
// Hourly accumulation observations: diuresis, drainages, hydric balance, and fluid entries
// These check for existing observations at the same time and increment values
if (obs.Name != null && (obs.Name.Equals("Diuresis_Hour") ||
obs.Name.Equals("Drainages_Hour") ||
obs.Name.Equals("Hydric_Balance") ||
obs.Name.Equals("IV_Entries_Hour") ||
obs.Name.Equals("Enteral_Entries_Hour")))
obs = (T)await CheckObsExistsAndIncrementTime(obs);
// Temperature observations: handle duplicate time entries by incrementing time
if (obs.Name != null && (obs.Name.Equals("Temp_Patient") ||
obs.Name.Equals("Temp_Incubator")
))
obs = (T)await CheckObsWithSameTimeExistsAndIncrementTime(obs);
return obs;
}
///
/// Maps a to its processed form, computing the end time of single-dose
/// treatments, evaluating the medicines associated with the treatment's codes/notes, and dispatching
/// the appropriate downstream calculations (bolus, monitor, surgery, ECMO complexity, and ONi observations)
/// based on the configured code catalogues.
///
/// The to be mapped.
/// A task that represents the asynchronous mapping operation. The task result contains the processed .
/// Thrown when the parameter is null.
///
public async Task Map(PatientTreatment treatment)
{
if (treatment.SingleDose)
treatment = CalculateSingleDoseEndDate(treatment);
var tempTreatment = treatment;
await CheckTreatmentMedicines(tempTreatment);
if (tempTreatment.RequestedGiveCodes.Any())
{
if (tempTreatment.RequestedGiveCodes.Any(t =>
_medicationBolus.Contains(t.Identifier))) //remove && t.codingSystem == "FTPCS" not match always
{
_logger.LogDebug("Medication Bolus detected. Creating calculated observation");
await CalculateBolus(tempTreatment);
}
if (tempTreatment.RequestedGiveCodes.Any(t => _electroencephalogram.Contains(t.Identifier)))
await CalculateMonitor(tempTreatment);
if (tempTreatment.RequestedGiveCodes.Any(t =>
_surgery.Contains(t.Identifier) && _surgeryText.Contains(t.Text)))
await CalculateSurgery(tempTreatment);
if (treatment.RequestedGiveCodes.Any(t => _ecmo.Contains(t.Identifier)))
//If ecmo treatment is NW recalculate complexity to show ECMO if is DC recalculate complexity
await CalculateComplexityOnEcmo(treatment);
if (tempTreatment.RequestedGiveCodes.Any(t => _oniCodes.Contains(t.Identifier)))
{
if (tempTreatment.OrderControl == OrderControlType.Nw)
{
var oniObservation = new PatientObservation
{
PatientId = treatment.PatientId,
Code = "84481000140102",
Name = "ONi",
CodingSystem = "MDC",
Value = tempTreatment.RequestedGiveAmountMinimum ?? '-',
Time = tempTreatment.OrderTime ?? DateTime.Now,
Units = "ppm"
};
await _observationService.Value.InsertObservation(oniObservation, mapObs: false);
}
if (treatment.OrderControl == OrderControlType.Dc)
{
var lastOnis = await _observationService.Value.FindLastObservations(treatment.PatientId, 1,
["ONi"]);
var lastOni = lastOnis.FirstOrDefault();
if (lastOni == null) return treatment;
lastOni.Expired = true;
await _observationService.Value.UpdateObservation(lastOni);
await _observationService.Value.InsertObservation(lastOni, false);
}
}
}
return treatment;
}
///
/// Identity mapping for a : returns the supplied instance unchanged,
/// wrapped in a completed task. Provided to satisfy the customization contract; pump observations
/// do not currently require UCIN-specific calculation.
///
/// The pump observation to map.
/// A task containing the same instance that was passed in.
///
public async Task Map(PumpObservation pumpObservation)
{
return await Task.FromResult(pumpObservation);
}
///
/// Convenience overload that recalculates the "Medication" and "ERMedication" observations for a
/// patient using the supplied list of active medicines, building a transient
/// for the underlying call.
///
/// The list of active instances currently associated with the patient.
/// The unique identifier of the patient.
/// A task that represents the asynchronous calculation operation.
///
public async Task CalculateMedicineObservation(List activeMedicines, ObjectId patientId)
{
await CalculateMedicineObservation(activeMedicines, [], new PatientTreatment { PatientId = patientId });
}
///
/// Recalculates the "OpiateBoluses" observation for a patient based on the bolus treatments
/// returned by GetActiveBolus. Skips persistence when the value is unchanged from the
/// last stored observation.
///
/// The unique identifier of the patient.
/// A task that represents the asynchronous calculation operation.
///
public async Task CalculateActiveBolus(ObjectId patientId)
{
// SIN RXA
/*
* var activeBolus = treatmentService.Value.GetActiveTreatmentsByPatient(patientId)
.FindAll(p => p.requestedGiveCodes.Any(r => MedicationBolus.Contains(r.identifier)) && p.orderTime > DateTime.Now.AddHours(-12) && p.boloPom);
var bolusObs = new PatientObservation()
{
patientid = patientId,
time = DateTime.Now,
//code = "MedicationBolus", //treatment.requestedGiveCode.identifier,
//codingSystem = "ADAS", //treatment.requestedGiveCode.codingSystem,
name = "OpiateBoluses",
codingSystem = "ADAS",
value = activeBolus.Count
};
var lastBolus = observationService.Value.FindLastObservations(patientId, 1, new List { "OpiateBoluses" }).FirstOrDefault();
if (lastBolus != null && int.Parse(lastBolus.value.ToString()) == int.Parse(bolusObs.value.ToString()))
{
return;
}
observationService.Value.InsertObservation(bolusObs);
*/
//Por RXA
var activeBolus = await GetActiveBolus(patientId, null);
var bolusObs = new PatientObservation
{
PatientId = patientId,
Time = DateTime.Now,
//code = "MedicationBolus", //treatment.requestedGiveCode.identifier,
//codingSystem = "ADAS", //treatment.requestedGiveCode.codingSystem,
Name = "OpiateBoluses",
CodingSystem = "ADAS",
Value = activeBolus.Count
};
var lastsBolus = await _observationService.Value.FindLastObservations(patientId, 1, ["OpiateBoluses"]);
var lastBolus = lastsBolus.FirstOrDefault();
if (lastBolus != null && int.TryParse(lastBolus.Value.ToString(), out var lastbolus) &&
int.TryParse(bolusObs.Value.ToString(), out var bolusobs) && lastbolus == bolusobs) return;
await _observationService.Value.InsertObservation(bolusObs, mapObs: false);
}
///
/// Retrieves all treatments currently considered active for the specified patient, delegating
/// the actual retrieval to the configured .
///
/// The unique identifier of the patient.
/// A collection of active objects for the patient.
///
public async Task> GetActiveTreatmentsByPatient(ObjectId id)
{
var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(id);
return activeTreatments;
}
///
/// Identity mapping for a : returns the supplied instance unchanged,
/// wrapped in a completed task. Provided to satisfy the customization contract; diagnoses do not
/// currently require UCIN-specific calculation.
///
/// The to map.
/// A task containing the same instance that was passed in.
///
public Task Map(PatientDiagnosis diagnosis)
{
return Task.FromResult(diagnosis);
}
///
/// Resolves time-inconsistencies for a new patient observation when it arrives with the same
/// (down-to-the-second) timestamp as the latest stored observation of the same name. The new
/// observation's Time is shifted forward by one second, and a fresh
/// is generated to avoid duplicate-key collisions.
///
/// The new patient observation to evaluate for time inconsistencies.
///
/// A task that represents the asynchronous operation. The task result contains the fixed
/// if the input had a valid name; otherwise, .
///
///
public async Task FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
if (string.IsNullOrEmpty(newObservation.Name))
{
_logger.LogError("Observation name is null or empty: {newObservation}", newObservation);
return null;
}
var lastObservations =
await _observationService.Value.FindLastObservations(newObservation.PatientId, 1, [newObservation.Name]);
var lastObservation = lastObservations.FirstOrDefault();
if (lastObservation != null && DateTime.Compare(
new DateTime(lastObservation.Time.Year, lastObservation.Time.Month,
lastObservation.Time.Day, lastObservation.Time.Hour,
lastObservation.Time.Minute, lastObservation.Time.Second),
new DateTime(newObservation.Time.Year, newObservation.Time.Month,
newObservation.Time.Day, newObservation.Time.Hour,
newObservation.Time.Minute, newObservation.Time.Second)
) >= 0)
newObservation.Time = lastObservation.Time.AddSeconds(1);
newObservation.Id = ObjectId.GenerateNewId();
return newObservation;
}
///
/// Identity mapping for a list of instances: returns the supplied
/// list unchanged, wrapped in a completed task. Provided to satisfy the customization contract;
/// the pre-mapping phase does not currently require UCIN-specific transformation.
///
/// The list of patient observations to be pre-mapped.
/// A task containing the original list of patient observations.
///
public Task> PreMapList(List listToInsert)
{
return Task.FromResult(listToInsert);
}
///
/// Identity mapping for a source alarm observation paired with a :
/// returns the supplied observation unchanged, wrapped in a completed task. Provided to satisfy the
/// customization contract; alarms are not currently transformed in the UCIN customization.
///
/// The patient observation that triggered the alarm.
/// The alarm metadata to be inserted alongside the observation.
/// A task containing the same instance that was passed in.
///
public Task MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
return Task.FromResult(obs);
}
///
/// Placeholder alarm dispatch hook used by the customization contract. The UCIN customization
/// does not currently implement custom alarm dispatching; calling this method always throws.
///
/// The patient observation that triggered the alarm.
/// The alarm display name.
/// The optional alarm code from .
/// Never returns a result.
/// Always thrown because alarm dispatch is not implemented in the UCIN customization.
///
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
throw new NotImplementedException();
}
///
/// When a "Surgery" observation is marked as expired, inserts a follow-up "Surgery" observation
/// with value 0 at the same time plus one second, signaling that the surgery complexity
/// contribution has been removed. Errors are logged and rethrown.
///
/// The base patient observation expected to be a with Name == "Surgery".
/// A task that represents the asynchronous insert operation.
/// Thrown when cannot be cast to .
/// Rethrown after the underlying exception is written to the console for diagnostics.
///
public async Task CheckSurgeryExpired(BasePatientObservation obs)
{
try
{
var pobs = (PatientObservation)obs;
if (!pobs.Expired) return;
var surgeryObs = new PatientObservation
{
PatientId = obs.PatientId,
Name = "Surgery",
Min = 0,
Max = 5,
CodingSystem = "ADAS",
Time = obs.Time.AddSeconds(1),
Value = 0,
Expires = pobs.Expires
};
_logger.LogDebug("CheckSurgeryExpired. Inserting: {obs}", surgeryObs.ToString());
await _observationService.Value.InsertObservation(surgeryObs, mapObs: false);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
///
/// For hourly accumulation observations, checks whether another observation with the same name
/// already exists within the same hour and, if so, shifts this observation's Time forward
/// by one second so the system can identify the most recent entry.
///
/// The base patient observation expected to be a .
/// A task that represents the asynchronous operation. The task result contains the (possibly time-shifted) base patient observation.
///
public async Task CheckObsExistsAndIncrementTime(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
var lastObsFromHour =
await _observationService.Value.FindLastBeforeDate(pobs.PatientId, pobs.Time.AddMinutes(59), obs.Name);
if (lastObsFromHour != null &&
DateTime.Compare(new DateTime(lastObsFromHour.Time.Year, lastObsFromHour.Time.Month,
lastObsFromHour.Time.Day, lastObsFromHour.Time.Hour, 0, 0)
, new DateTime(pobs.Time.Year, pobs.Time.Month, pobs.Time.Day, pobs.Time.Hour, 0, 0)) == 0)
pobs.Time = lastObsFromHour.Time.AddSeconds(1);
return pobs;
}
///
/// For temperature observations, checks whether another observation with the same name already
/// exists at the exact same timestamp and, if so, shifts this observation's Time forward
/// by one second so the system can identify the most recent entry.
///
/// The base patient observation expected to be a .
/// A task that represents the asynchronous operation. The task result contains the (possibly time-shifted) base patient observation.
///
public async Task CheckObsWithSameTimeExistsAndIncrementTime(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
var existObsWithSameTime =
await _observationService.Value.FindAnyWithSameDate(pobs.PatientId, pobs.Time, pobs.Name);
if (existObsWithSameTime != null && existObsWithSameTime.Any()) pobs.Time = pobs.Time.AddSeconds(1);
return pobs;
}
///
/// Computes the Oxygenation Index (PMAP × FiO2 × 100 / PaO2) using the supplied observation plus
/// the latest stored AirPressure_Mean, FiO2, and PaO2 values. Persists the
/// resulting Oxygenation_Index observation with a 10-second expiration matching FiO2's
/// expiration window. Skipped gracefully when PaO2 is zero, when any of the inputs cannot be
/// parsed, or when an exception is caught and logged.
///
/// The observation that triggered the calculation (one of AirPressure_Mean, FiO2, or PaO2).
/// A task that represents the asynchronous calculation operation.
///
public async Task CalculateOxygenationIndex(BasePatientObservation obs)
{
var toSearchList = new List();
PatientObservation? airPressMeanObs = null;
PatientObservation? fiO2Obs = null;
PatientObservation? paO2Obs = null;
try
{
toSearchList.AddRange(new List { "AirPressure_Mean", "FiO2", "PaO2" });
var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, toSearchList);
switch (obs.Name)
{
case "AirPressure_Mean":
airPressMeanObs = (PatientObservation)obs;
fiO2Obs = values.FirstOrDefault(o => o.Name == "FiO2");
paO2Obs = values.FirstOrDefault(o => o.Name == "PaO2");
break;
case "FiO2":
fiO2Obs = (PatientObservation)obs;
airPressMeanObs = values.FirstOrDefault(o => o.Name == "AirPressure_Mean");
paO2Obs = values.FirstOrDefault(o => o.Name == "PaO2");
break;
case "PaO2":
paO2Obs = (PatientObservation)obs;
fiO2Obs = values.FirstOrDefault(p => p.Name == "FiO2");
airPressMeanObs = values.FirstOrDefault(o => o.Name == "AirPressure_Mean");
break;
}
//Controlar que fio2 no haya expirada al recogerla
if (fiO2Obs != null && paO2Obs != null && airPressMeanObs != null)
if (int.TryParse(airPressMeanObs.Value.ToString(), out var airPressureMean) &&
int.TryParse(fiO2Obs.Value.ToString(), out var fio2) &&
int.TryParse(paO2Obs.Value.ToString(), out var pao2))
{
if (pao2 == 0)
{
_logger.LogWarning(
"Calculate Oxygenation Index observation. PaO2: {pao2}, observation: {obsName}", pao2,
obs.Name);
return;
}
var oxygenationIndexValue = airPressureMean * fio2 * 100 / pao2;
var oxygenationIndexObs = new PatientObservation
{
PatientId = obs.PatientId,
Name = "Oxygenation_Index",
CodingSystem = "ADAS",
Value = oxygenationIndexValue,
Time = DateTime.Now,
Expires = 10 //Expires in 10 seg because its FiO2 expire time.
};
await _observationService.Value.InsertObservation(oxygenationIndexObs, mapObs: false);
}
}
catch (Exception ex)
{
_logger.LogError("Error calculate Oxygenation Index observation. Exception: {exMessage}", ex.Message);
}
}
///
/// Sets the EndTime of a single-dose treatment to 8 hours after its OrderTime
/// (or 8 hours from now when OrderTime is ).
///
/// The single-dose treatment whose end time is to be calculated.
/// The same instance with its EndTime updated.
///
private static PatientTreatment CalculateSingleDoseEndDate(PatientTreatment treatment)
{
/* Deprecated calc finish treatment date on shift end. Now every treatment single dose is last 8 Hours
DateTime now = DateTime.UtcNow;
//Turno actual, tenemos que poner como fecha fin la fecha de inicio del turno siguiente
var shiftNow = Shift.Where(
i => DateTime.ParseExact(i, "HH:mm", CultureInfo.InvariantCulture).Ticks < DateTime.Now.Ticks
).LastOrDefault();
var index = Shift.IndexOf(shiftNow);
string endDateShift;
if (Shift.Count <= index + 1) endDateShift = Shift[0];
else endDateShift = Shift[index + 1];
var HourMinutes = endDateShift.Split(':');
if (now.Hour.CompareTo(int.Parse(HourMinutes[0])) > 0)
now = now.AddDays(1);
DateTime finisTreatmentDate = new DateTime(now.Year, now.Month, now.Day, int.Parse(HourMinutes[0]), int.Parse(HourMinutes[1]), 0);
*/
treatment.EndTime = treatment.OrderTime?.AddHours(8) ?? DateTime.Now.AddHours(8);
return treatment;
}
///
/// Resolves the medicines associated with a treatment (by matching its codes and notes against the
/// medicine catalogue, falling back to NotesIndicatingMedication patterns), aggregates the
/// patient's active treatments and medicines, detects parenteral nutrition (NPT) treatments, and
/// triggers CalculateMedicineObservation to update the Medication and ERMedication observations.
///
/// The treatment whose medicines should be checked.
/// A task that represents the asynchronous check operation.
///
public async Task CheckTreatmentMedicines(PatientTreatment treatment)
{
//sI TIENE UNA NOTA CON NPT SON DE TIPO NUTRICIÓN PARENTERAL Y SUMAN 1
var listNotesCode = treatment.Notes.Select(note => note.Comment).ToList();
if (treatment.RequestedGiveCodes.Any())
listNotesCode.AddRange(treatment.RequestedGiveCodes.Select(treatmentCodes => treatmentCodes.Identifier));
//var medicines = medicineService.Value.GetByCodeOrNote(listNotesCode);
var medicines = await _medicineService.GetByCodeOrNote(listNotesCode);
var parentalNutritionMedicine = await CalculateParentalNutritionMedicine(treatment);
medicines.Add(parentalNutritionMedicine);
//If not detect any medicine in medicine table, check if contáis NTE with, if it's true, it's a medicine.
/*
NTE|||Medicación|^OrderDefinition\r
NTE|||Perfusiones|^OrderDefinition\r
NTE|||SUEROS Y HEMODER|^OrderDefinition\r
*/
if (medicines is { Count: 0 })
{
var isMedicine = treatment.Notes.Any(n => _notesIndicatingMedication.Contains(n.Comment));
if (isMedicine && treatment.RequestedGiveCodes is { Count: > 0 })
treatment.RequestedGiveCodes.ForEach(c =>
{
if (!string.IsNullOrEmpty(c.Identifier))
medicines.Add(new Medicine
{
Codes = [c.Identifier],
Name = c.Text
});
});
}
if (medicines.Any())
{
if (!string.IsNullOrEmpty(treatment.RequestedGiveTreatment))
try
{
medicines =
[
new Medicine
{
Name = treatment.RequestedGiveTreatment,
Type = medicines.FindAll(t => t.Type.Any())
.Select(m => m.Type.Aggregate((x, y) => x + "," + y)).Distinct().ToList(),
Codes = medicines.FindAll(t => t.Codes.Any())
.Select(m => m.Codes.Aggregate((x, y) => x + "," + y)).Distinct().ToList(),
Group = medicines.FindAll(t => t.Group.Any())
.Select(m => m.Group.Aggregate((x, y) => x + "," + y)).Distinct().ToList(),
Notes = medicines.FindAll(t => t.Notes.Any())
.Select(m => m.Notes.Aggregate((x, y) => x + "," + y)).Distinct().ToList()
}
];
}
catch (Exception ex)
{
_logger.LogError("Error Checking Treatment Medicines. {medicines} . Exception {ex}",
string.Join(",", medicines), ex);
}
var _ = await GetActiveTreatmentsByPatient(treatment.PatientId);
var activeTreatments = _.ToList();
//var activeMedicines = medicineService.Value.GetMedicinesOfTreatments(activeTreatments);
var __ = await _medicineService.GetMedicinesOfTreatments(activeTreatments);
var activeMedicines = __.ToList();
var activeNptTreatments =
activeTreatments.FindAll(t => t?.Notes.FirstOrDefault(n => n.Comment == "NPT") != null);
if (activeNptTreatments.Count > 0)
{
//activeMedicines.AddRange((IEnumerable)activeNPTTreatments.Select(async s => await CalculateParentalNutritionMedicine(s)).Where(parentalMedicine => parentalMedicine != null));
//activeMedicines.AddRange(await Task.WhenAll(activeNPTTreatments.Select(async s => await CalculateParentalNutritionMedicine(s))).Where(parentalMedicine => parentalMedicine != null));
var tasks = activeNptTreatments.Select(async s => await CalculateParentalNutritionMedicine(s));
var taskResults = await Task.WhenAll(tasks);
var filteredMedicines = taskResults.ToList();
if (filteredMedicines.Any()) filteredMedicines.ForEach(m => { activeMedicines.Add(m); });
}
await CalculateMedicineObservation(activeMedicines, medicines, treatment);
}
}
///
/// Builds a synthetic representing parenteral nutrition (NPT) when the
/// treatment's notes indicate a NPT order. Lipid-based NPTs are tagged with
/// ParenteralNutritionLipids; all other NPTs are tagged with ParenteralNutrition.
///
/// The treatment to inspect for NPT indicators, or .
/// A task that resolves to a instance describing the parenteral nutrition, or an unnamed, untyped instance when no NPT notes are present.
///
private static Task CalculateParentalNutritionMedicine(PatientTreatment? treatment)
{
List medicineType = [];
Note? commentType = null;
if (treatment?.Notes.FirstOrDefault(n => n.Comment == "NPT") != null)
{
commentType = treatment.Notes.FirstOrDefault(n => n.CommentType == "formularybaseformulation");
if (treatment.Notes.FirstOrDefault(n =>
n.Comment is "LÍPIDOS NEONATALES AL 20%" or "LÍPIDOS NEONATALES AL 20% CON...") != null)
medicineType = [nameof(MedicineEnum.Types.ParenteralNutritionLipids)];
else
medicineType = [nameof(MedicineEnum.Types.ParenteralNutrition)];
}
var medicine = new Medicine
{
Type = medicineType,
Name = commentType != null ? commentType.Comment : "UnNamed"
};
return Task.FromResult(medicine);
}
///
/// Maintains the patient's active medicine list according to the treatment's OrderControl
/// (NW adds, XO adds if missing, DC removes), counts the distinct medicine
/// types, persists the Medication observation if its value changed, and then recalculates
/// the ERMedication observation.
///
/// The list of active medicines for the patient. Modified in-place.
/// The medicines to add or remove depending on OrderControl.
/// The treatment that triggered the recalculation.
/// A task that represents the asynchronous calculation operation.
///
public async Task CalculateMedicineObservation(List activeMedicines, List medicines,
PatientTreatment treatment)
{
var medicinesWithType = activeMedicines.FindAll(m => m.Type.Any());
foreach (var medicine in medicines)
if (treatment.OrderControl is OrderControlType.Nw or OrderControlType.Xo)
{
if (treatment.StartTime != null && DateTime.UtcNow.CompareTo(treatment.StartTime) < 0) return;
if (treatment.OrderControl == OrderControlType.Nw)
activeMedicines.Add(medicine);
if (treatment.OrderControl == OrderControlType.Xo
&& activeMedicines.All(m => m.Name != medicine.Name)) activeMedicines.Add(medicine);
if (medicinesWithType != null &&
medicinesWithType.All(m => m.Type != medicine.Type))
medicinesWithType.Add(medicine);
}
else if (treatment.OrderControl == OrderControlType.Dc)
{
if (medicinesWithType != null)
{
medicinesWithType.Remove(medicine);
activeMedicines.RemoveAll(m => m.Name == medicine.Name);
}
}
var medicineTypes = medicinesWithType!.SelectMany(m => m.Type.Select(t => new { type = t }))
.GroupBy(x => x.type).Count();
var medicineObs = new PatientObservation
{
PatientId = treatment.PatientId,
Name = "Medication",
CodingSystem = "ADAS",
Value = medicineTypes,
Time = DateTime.Now
};
var lastMedicationsObs = await _observationService.Value.FindLastObservations(treatment.PatientId, 1,
["Medication"]);
var lastMedicationObs = lastMedicationsObs.FirstOrDefault();
if (lastMedicationObs == null || !lastMedicationObs.Value.Equals(medicineObs.Value))
await _observationService.Value.InsertObservation(medicineObs, mapObs: false);
await CalculateErMedication(activeMedicines, treatment);
}
///
/// Calculates the patient's risk level from their active medicines and persists the
/// ERMedication observation with min/max bounds of 0 and 5, skipping persistence
/// when the value matches the last stored observation.
///
/// The list of active medicines used to derive the risk level.
/// The treatment that triggered the recalculation.
/// A task that represents the asynchronous calculation operation.
///
private async Task CalculateErMedication(List activeMedicines, PatientTreatment treatment)
{
//var activeTreatments = treatmentService.Value.GetActiveTreatmentsByPatient(treatment.patientid);
var result = await CalculateErMedicineLevels(activeMedicines);
var erMedicationObs = new PatientObservation
{
Name = "ERMedication",
CodingSystem = "ADAS",
PatientId = treatment.PatientId,
Time = DateTime.Now,
Min = 0,
Max = 5,
Value = result
};
var lastErMedicationsObsList = await _observationService.Value.FindLastObservations(treatment.PatientId, 1,
["ERMedication"]);
var lastErMedicationObs = lastErMedicationsObsList.FirstOrDefault();
if (lastErMedicationObs == null || !lastErMedicationObs.Value.Equals(erMedicationObs.Value))
await _observationService.Value.InsertObservation(erMedicationObs, mapObs: false);
}
///
/// Maps the patient's active medicine list to a discrete risk level (0 to 5) based on medicine
/// types and groups: lipidic parenteral nutrition or multiple high-risk medicines map to 5,
/// non-lipid NPT or one or two high-risk medicines map to 4, metabolic medicines map to 2,
/// remaining medicines map to 1, and a list containing only iron/vitamin D maps to 0.
///
/// The list of active medicines to evaluate.
/// A task that resolves to the integer risk level (0–5).
///
private static Task CalculateErMedicineLevels(List activeMedicines)
{
var ironVitaminDCodes = new List { "374424002", "175041000140104" };
//Aquí a cambiar que la nutrición parenteral siempre sume 4 no 5 quitar de medicinas de 5 solo suma 5 en caso de ser
//Los lípidos son dos órdenes de NP con el segmento RXO6.2 (NPT: 1 ml LÍPIDOS NEONATALES AL 20% CON MEDICAMENTOS 1 o
//NPT: 1 ml LÍPIDOS NEONATALES AL 20% 1)
var medicines5Points = activeMedicines.Where(m =>
m.Type.Contains(nameof(MedicineEnum.Types.Pge1)) ||
m.Type.Contains(nameof(MedicineEnum.Types.Insulin)) ||
m.Group.Contains(nameof(MedicineEnum.Group.DoubleSignature)) ||
m.Group.Contains(nameof(MedicineEnum.Group.Vasoactive)) ||
m.Type.Contains(nameof(MedicineEnum.Types.ParenteralNutritionLipids))
)
.ToList();
if (medicines5Points.Count > 2 ||
activeMedicines.FirstOrDefault(m =>
m.Type.Contains(nameof(MedicineEnum.Types.ParenteralNutritionLipids))) !=
null) return Task.FromResult(5);
if (medicines5Points.Count is > 0 and <= 2 ||
activeMedicines.FirstOrDefault(m => m.Type.Contains(nameof(MedicineEnum.Types.ParenteralNutrition))) !=
null
) return Task.FromResult(4);
var medicines2Points = activeMedicines.Where(m =>
m.Group.Contains(nameof(MedicineEnum.Group.Metabolic))
).ToList();
if (medicines2Points.Count > 0) return Task.FromResult(2);
//All medicines without type/group and not vitamin D or Iron
var medicines1Point = activeMedicines.FindAll(m => !m.Codes.Any(c => ironVitaminDCodes.Contains(c)));
if (medicines1Point.Count > 0) return Task.FromResult(1);
var ironVitaminDMedicines = activeMedicines.Where(m =>
m.Codes.Any(c => ironVitaminDCodes.Contains(c))
).ToList();
if (ironVitaminDMedicines.Count == activeMedicines.Count || activeMedicines.Count == 0)
return Task.FromResult(0);
return Task.FromResult(1);
}
///
/// Maps a Resp_Mode observation's value to a discrete Resp_Type (HighFrequencyVentilation,
/// Invasive, NonInvasive, or None) by matching it against the configured catalogues for each mode.
///
/// The respiratory-mode observation expected to be a .
/// A task that represents the asynchronous insert operation.
///
public async Task CalculateVentilationMode(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
var respTypeObs = new PatientObservation
{
Name = "Resp_Type",
CodingSystem = "ADAS",
PatientId = obs.PatientId,
Time = obs.Time
};
var strValue = pobs.Value.ToString();
respTypeObs.Value = _highFrequencyVentilation.Contains(strValue ?? string.Empty)
? nameof(RespirationType.HighFrequencyVentilation)
: _invasiveVentilation.Contains(strValue ?? string.Empty)
? respTypeObs.Value = nameof(RespirationType.Invasive)
: _nonInvasiveVentilation.Contains(strValue ?? string.Empty)
? respTypeObs.Value = nameof(RespirationType.NonInvasive)
: nameof(RespirationType.None);
await _observationService.Value.InsertIfChanged("Resp_Type", respTypeObs);
}
///
/// Calculates the patient's respiratory assistance score from the supplied observation's
/// Value (matched against the configured respiratory-type catalogue) or from the latest
/// stored Resp_Mode when the supplied observation is ONi. Active ONi treatment
/// (or a non-expired last ONi observation) forces the score to 10. Persists the resulting
/// Respiratory observation and returns the original input unchanged.
///
/// The base patient observation that triggered the calculation (expected to be a ).
/// A task that resolves to the same base patient observation that was passed in.
///
public async Task CalculateAsistResp(BasePatientObservation obs)
{
//Comprobar si hay un resp_type más nuevo y si no lo hay ignorar
var pobs = (PatientObservation)obs;
var lastsAsistResp = await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["Resp_Mode"]);
var lastAsistResp = lastsAsistResp.FirstOrDefault();
if (lastAsistResp != null && lastAsistResp.Time.CompareTo(obs.Time) > 0 &&
lastAsistResp.Name != "ONi") return obs;
var calculatedRespiratory = new PatientObservation
{
PatientId = obs.PatientId,
Time = DateTime.Now,
CodingSystem = "ADAS",
Name = "Respiratory",
Value = 0
};
if (obs.Name == "ONi")
{
//find last assist resp obs
if (lastAsistResp != null)
{
var respiratoryValue = _respiratoryTypeValue.FirstOrDefault(r => r.Item3.Contains(lastAsistResp.Value));
if (respiratoryValue != null) calculatedRespiratory.Value = respiratoryValue.Item2;
}
}
else
{
var respiratoryValue = _respiratoryTypeValue.FirstOrDefault(r => r.Item3.Contains(pobs.Value));
if (respiratoryValue != null) calculatedRespiratory.Value = respiratoryValue.Item2;
}
var isIno = !string.IsNullOrEmpty(obs.Code) && _oniCodes.Contains(obs.Code);
if (isIno && !pobs.Expired) calculatedRespiratory.Value = 10;
//Check if last ino exist and is not expired
var lastsIno = await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["ONi"]);
var lastIno = lastsIno.FirstOrDefault();
if (lastIno is { Expired: false }) calculatedRespiratory.Value = 10;
await _observationService.Value.InsertObservation(calculatedRespiratory, mapObs: false);
return obs;
}
///
/// Updates the synthetic ECMO observation according to the treatment's OrderControl:
/// NW sets it to NW, DC sets it to XO if any other ECMO treatment
/// remains active or to DC otherwise, and any other OrderControl is ignored.
/// In every case the change is followed by a complexity recalculation via CalculateComplexity.
///
/// The treatment whose ECMO state is being applied.
/// A task that represents the asynchronous recalculation operation.
///
private async Task CalculateComplexityOnEcmo(PatientTreatment treatment)
{
var ecmoObs = new PatientObservation
{
PatientId = treatment.PatientId,
Name = "ECMO"
};
switch (treatment.OrderControl)
{
case OrderControlType.Nw:
ecmoObs.Value = OrderControlType.Nw;
break;
case OrderControlType.Dc:
var haveEcmo = await _treatmentService.Value.GetActiveTreatmentsByPatient(treatment.PatientId);
ecmoObs.Value = haveEcmo.Any(t => t != null && t.RequestedGiveCodes.Any(r =>
_ecmo.Contains(r.Identifier) &&
t.PlacerOrder?.EntityIdentifier != treatment.PlacerOrder?.EntityIdentifier))
? OrderControlType.Xo
: OrderControlType.Dc;
break;
default:
return;
}
await CalculateComplexity(ecmoObs);
}
///
/// Recalculates the patient's overall Complexity observation from scratch using the latest
/// values for the configured complexity-contributing observations (weight, medication, surgery,
/// intravenous lines, monitor, and respiratory), the active ECMO treatment state, and the
/// optionally supplied which is preferred over the database value when
/// it is newer and not expired. The result is persisted only when its integer value differs
/// from the last stored complexity observation, with time inconsistencies resolved via
/// FixTimeInconsistencyWithLast. Returns the original observation unchanged.
///
/// The base patient observation that triggered the recalculation.
/// When , the supplied is not added to the calculation inputs (useful for synthetic observations like ECMO).
/// A task that resolves to the same base patient observation that was passed in.
///
public async Task CalculateComplexity(BasePatientObservation obs, bool ignoreObs = false)
{
try
{
var logUid = Guid.NewGuid();
var complexity = new PatientObservation
{
PatientId = obs.PatientId,
Time = DateTime.UtcNow,
CodingSystem = "ADAS",
Name = "Complexity",
Value = 0
};
var complexityValue = 0;
var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(obs.PatientId);
var haveEcmo = activeTreatments.Any(t =>
t != null && t.RequestedGiveCodes.Any(r => _ecmo.Contains(r.Identifier)));
var pobs = obs as PatientObservation;
var complexityObservations =
await _observationService.Value.FindLastObservations(obs.PatientId, 1, _complexityObservations);
//Nos acaba de entrar, la usamos por delante de la última de base de datos
//21-02-22 No es correcto, solo remover si el tiempo de esa observación que entra es más nuevo que la de bd
var actualObsInBd = complexityObservations.FirstOrDefault(o => o.Name == obs.Name);
if (actualObsInBd != null && obs.Time > actualObsInBd.Time)
complexityObservations.RemoveAll(it =>
it.Name == obs.Name);
if (!ignoreObs && pobs is { Expired: false }) complexityObservations.Add(pobs);
foreach (var observation in complexityObservations)
switch (observation.Name)
{
case "Weight_Current":
// if(observation.expired) break; A este no le importa que esté expirado, lo tiene que hacer siempre.
var weightCurrentValue = await CalculateWeight(observation);
_logger.LogDebug(
"complexity for patient {patientid} id log{logUID} => Weight_Current plus complexity: {WeightCurrentValue}",
observation.PatientId, logUid, weightCurrentValue);
complexityValue += weightCurrentValue;
break;
case "Weight_Newborn": //Only count newborn weight when weight current don't exist.
if (complexityObservations.FirstOrDefault(o => o.Name == "Weight_Current") == null)
{
var weightNewbornValue = await CalculateWeight(observation);
_logger.LogDebug(
"complexity for patient {patientid} id log{logUID} => Weight_Newborn plus complexity: {Weight_NewbornValue}",
observation.PatientId, logUid, weightNewbornValue);
complexityValue += weightNewbornValue;
}
break;
case "Medication":
if (observation.Expired) break;
var value = observation.Value.ToString();
if (!int.TryParse(value, out var valueParsed))
valueParsed = 0;
_logger.LogDebug(
"complexity for patient {patientid} id log{logUID} => Medication plus complexity: {valueParsed}",
observation.PatientId, logUid, valueParsed);
complexityValue += valueParsed;
break;
case "Surgery":
if (observation.Expired) break;
var surgeryValue = Convert.ToInt32(observation.Value);
_logger.LogDebug(
"complexity for patient {patientid} id log{logUID} => Surgery plus complexity: {surgeryValue}",
observation.PatientId, logUid, surgeryValue);
complexityValue += surgeryValue;
break;
case "IntravenousLines":
if (observation.Expired) break;
var intravenousLinesValue = Convert.ToInt32(observation.Value);
_logger.LogDebug(
"complexity for patient {patientid} id log{logUID} => IntravenousLines plus complexity: {IntravenousLinesValue}",
observation.PatientId, logUid, intravenousLinesValue);
complexityValue += intravenousLinesValue;
break;
case "Monitor":
if (observation.Expired) continue;
var monitorValue = Convert.ToInt32(observation.Value);
_logger.LogDebug(
"complexity for patient {patientid} id log{logUID} => monitor plus complexity: {monitorValue}",
observation.PatientId, logUid, monitorValue);
complexityValue += monitorValue;
break;
case "Respiratory":
if (observation.Expired) break;
var respiratoryValue = Convert.ToInt32(observation.Value);
_logger.LogDebug(
"complexity for patient {patientid} id log{logUID} => respiratory plus complexity: {respiratoryValue}",
observation.PatientId, logUid, respiratoryValue);
complexityValue += respiratoryValue;
break;
}
complexity.Value = complexityValue;
if (haveEcmo && obs.Name != "ECMO")
//complexity.value = $"ECMO ({complexityValue})";
complexity.Value = $"{complexityValue} ECMO";
if (complexityObservations.Any(o => o.Name == "ECMO"))
{
var ecmoObs = complexityObservations.FirstOrDefault(o => o.Name == "ECMO");
if (Enum.TryParse(typeof(OrderControlType), ecmoObs?.Value.ToString(), out var treatmentControl))
switch (treatmentControl)
{
case OrderControlType.Dc:
break;
//case OrderControlType.XO:
//case OrderControlType.NW:
default:
//complexity.value = $"ECMO ({complexityValue})";
complexity.Value = $"{complexityValue} ECMO";
//var pObs = await FixTimeInconsistencyWithLast(complexity);
if (pobs != null)
await _observationService.Value.InsertObservation(pobs);
break;
}
}
var lastsObsComplexity =
await _observationService.Value.FindLastObservations(obs.PatientId, 1, [complexity.Name]);
var lastObsComplexity = lastsObsComplexity.FirstOrDefault();
if (lastObsComplexity is not null)
{
var lastObsParsed = int.TryParse(lastObsComplexity.Value.ToString(), out var lastObsValue);
if (lastObsParsed && int.TryParse(complexity.Value.ToString(), out var intValue) &&
lastObsValue == intValue) return obs;
}
var fixedTimeComplexity = await FixTimeInconsistencyWithLast(complexity);
_logger.LogDebug(
"complexity for patient {patientid} id log{logUID} => inserting complexity value: {complexityValue} for patient: {complexityPatientid} at time {fixedTimeComplexityTime}",
complexity.PatientId, logUid, complexity.Value, complexity.PatientId, fixedTimeComplexity?.Time);
if (fixedTimeComplexity != null)
await _observationService.Value.InsertObservation(fixedTimeComplexity, mapObs: false);
return obs;
}
catch (Exception ex)
{
_logger.LogError("Error calculating complexity. Exception: {ex}", ex);
return obs;
}
}
///
/// Recomputes the IntravenousLines complexity score by reconciling the incoming
/// IntravenousLinesObs against the patient's active intravenous lines. Detects and
/// corrects common ICCA mis-clicks (inserts incorrectly carrying a remove time), updates an
/// existing line in place when only the duration changed, and otherwise adds the new line to
/// the active set. The aggregated score is capped at 10 and persisted with bounds [0, 10].
/// InvalidCastExceptions are logged and swallowed.
///
/// The base patient observation that triggered the calculation (expected to be a whose Value is a ).
///
/// A task that resolves to the original base patient observation when processing succeeds or
/// fails gracefully with logging; resolves to when the function returns
/// early because the incoming observation is a duplicate insert (in which case the caller should
/// not overwrite the stored observation).
///
///
public async Task CalculateIntravenousLineObservation(BasePatientObservation obs)
{
try
{
var pobs = (PatientObservation)obs;
var intravenousLineValue = (PatientIntravenousLinesValue)pobs.Value;
//Check for doctor miss click on ICCA. sometimes they put insert with remove time
//when it has a remove time always is remove, never insert
if (intravenousLineValue is { Action: "Insertado", RemoveTime: not null })
intravenousLineValue.Action = "Retirado";
//GetByCodeSysAndCode active Intravenous observations
//var intraVenousLineObservations = observationService.Value.FindLastObservations(obs.patientid, 1, new List { obs.name });
List activeIntraVenousLineObservations = [];
try
{
activeIntraVenousLineObservations =
await _observationService.Value.FindLastIntravenousLinesObservationByLocation(obs.PatientId);
}
catch (Exception ex)
{
_logger.LogWarning(
"Cannot transform to PatientObservation value to PatientIntravenousLinesValue {exMessage}",
ex.Message);
}
if (activeIntraVenousLineObservations.Any())
activeIntraVenousLineObservations = activeIntraVenousLineObservations.FindAll(v =>
v != null && ((PatientIntravenousLinesValue)v.Value).Action == "Insertado");
/*
var activeIntravenousLineObservations = intraVenousLineObservations.GroupBy(obs => (((PatientIntravenousLinesValue)obs.value).type,
((PatientIntravenousLinesValue)obs.value).insertTime,
((PatientIntravenousLinesValue)obs.value).location))
.Where(grp => grp.All(o => ((PatientIntravenousLinesValue)o.value).RemoveTime == null))
.SelectMany(group => group).ToList();
*/
//If intravenous obs have remove time, remove it from activeIntravenousLines before calculating.
if (intravenousLineValue.RemoveTime != null)
{
activeIntraVenousLineObservations.RemoveAll(o =>
o != null && ((PatientIntravenousLinesValue)o.Value).Location == intravenousLineValue.Location);
}
else
{
//If insert come with same data than other insert and same insertTime is update. Don't need to register it again as insert.is updated
var intravenousLineBeforeUpdate = activeIntraVenousLineObservations.FirstOrDefault(o => o != null &&
((PatientIntravenousLinesValue)o.Value).Location == intravenousLineValue.Location &&
Equals(((PatientIntravenousLinesValue)o.Value).InsertTime, intravenousLineValue.InsertTime) &&
((PatientIntravenousLinesValue)o.Value).Type == intravenousLineValue.Type &&
intravenousLineValue.RemoveTime == null);
if (intravenousLineBeforeUpdate != null)
{
//If duration changed Update register
if (((PatientIntravenousLinesValue)intravenousLineBeforeUpdate.Value).Duration ==
intravenousLineValue.Duration)
return null;
((PatientIntravenousLinesValue)intravenousLineBeforeUpdate.Value).Duration =
intravenousLineValue.Duration ?? string.Empty;
await _observationService.Value.UpdateObservation(intravenousLineBeforeUpdate);
return null;
}
if (!activeIntraVenousLineObservations.Any(o =>
o != null &&
((PatientIntravenousLinesValue)o.Value).Location == intravenousLineValue.Location &&
((PatientIntravenousLinesValue)o.Value).Type == intravenousLineValue.Type))
activeIntraVenousLineObservations.Add(pobs);
}
//Generate calculad intravenousLine obs
//var intraVenousLineScore2 = activeIntraVenousLineObservations.Select(activeObservation => IntraVenousLinesTypeValue.Where(i => i.Item3.Contains(((PatientIntravenousLinesValue)activeObservation.value).type))).Select(typeValue => typeValue.FirstOrDefault()?.Item2??0).Sum();
// Step 1: Select active observations and cast to PatientIntravenousLinesValue
var activeObservations = activeIntraVenousLineObservations.Select(activeObservation =>
(PatientIntravenousLinesValue?)activeObservation?.Value);
// Step 2: Match active observations with type-value mappings
// We use .Replace("\u00A0", " ") on type to avoid special codification for spaces between characters
var matchedValues = activeObservations.Select(activeObservation => _intraVenousLinesTypeValue.Where(i =>
activeObservation is { Type: not null } &&
i.Item3.Contains(activeObservation.Type.Replace("\u00A0", " "))))
.ToList();
// Step 3: Extract the values from matched tuples
var values = 0;
matchedValues.ForEach(typeValue => values += typeValue.FirstOrDefault()?.Item2 ?? 0);
// Step 4: Calculate the sum
var intraVenousLineScore = values;
if (intraVenousLineScore > 10) intraVenousLineScore = 10;
var intravenousObs = new PatientObservation
{
PatientId = obs.PatientId,
Name = "IntravenousLines",
Min = 0,
CodingSystem = "ADAS",
Max = 10,
Time = DateTime.UtcNow,
Value = intraVenousLineScore
};
var ob = await FixTimeInconsistencyWithLast(intravenousObs);
if (ob != null)
await _observationService.Value.InsertObservation(ob, mapObs: false);
}
catch (InvalidCastException ex)
{
_logger.LogError("CalculateIntravenousLineObservation {obs}: {exMessage}", obs, ex.Message);
}
return obs;
}
///
/// Derives the weight-related complexity contribution from a weight observation: values expressed
/// in kilograms are first converted to grams via ParseWeight, and the final integer complexity
/// contribution is mapped from the gram value using the standard UCIN brackets
/// (< 750 g → 7, 750–999 → 5, 1000–1249 → 2, 1250–1999 → 1, ≥ 2000 → 0). Returns 0 when the
/// observation value cannot be parsed as a number.
///
/// The base patient observation whose value represents the patient's weight.
/// A task that resolves to the integer complexity contribution (0–7).
///
private async Task CalculateWeight(BasePatientObservation obs)
{
var complexityValueOfWeight = 0;
if (obs.Units == "kg") obs = await ParseWeight(obs);
var pobs = (PatientObservation)obs;
if (!double.TryParse(pobs.Value.ToString(), out var valueParsed))
return complexityValueOfWeight;
complexityValueOfWeight = valueParsed switch
{
< 750 => 7,
>= 750 and <= 999 => 5,
>= 1000 and <= 1249 => 2,
>= 1250 and <= 1999 => 1,
>= 2000 => 0,
_ => complexityValueOfWeight
};
return complexityValueOfWeight;
}
///
/// Converts a weight observation expressed in kilograms to grams in-place (updates Units to
/// "gr" and multiplies Value by 1000). Returns the observation unchanged when it is
/// not a or when its value cannot be parsed; the error is also
/// logged.
///
/// The base patient observation expected to be a with a numeric value.
/// A task that resolves to the (possibly converted) base patient observation.
///
public Task ParseWeight(BasePatientObservation obs)
{
if (obs is not PatientObservation pobs || !double.TryParse(pobs.Value.ToString(), out var dValue))
{
_logger.LogError("Error casting weight Observation {obs}:", obs);
return Task.FromResult(obs);
}
pobs.Units = "gr";
pobs.Value = dValue * 1000;
return Task.FromResult(obs);
}
///
/// Recalculates the Monitor observation by adding 1 point for each active
/// transcutaneous / regional-brain-saturation / EEG monitoring signal (codes from the configured
/// catalogues) found in the most recent observations and active treatments. Persists the resulting
/// observation with bounds [0, 3]. Returns early with a warning when no recent monitor observations
/// are found.
///
///
/// The trigger for the calculation. Accepts either a
/// (preferred when available) or a . The patient identifier is
/// derived from whichever type is supplied.
///
/// A task that represents the asynchronous calculation operation.
///
public async Task CalculateMonitor(object monitorObservation)
{
var monitorValue = 0;
BasePatientObservation? pobs = null;
ObjectId patientId = new();
PatientTreatment? tobs = null;
if (monitorObservation.GetType() == typeof(PatientObservation))
{
pobs = (BasePatientObservation)monitorObservation;
patientId = pobs.PatientId;
}
else if (monitorObservation.GetType() == typeof(PatientTreatment))
{
tobs = (PatientTreatment)monitorObservation;
patientId = tobs.PatientId;
}
//Observations in last 30 min are active
var activeMonitorObservationsList = await _observationService.Value.FindLastObservations(patientId, 1,
["Saturation_RegBrain", "Saturation_RegSomatic", "Transcutaneous_O2"]);
if (!activeMonitorObservationsList.Any())
{
_logger.LogWarning("Active Monitor Observation List is null or empty for patientId: {patientId}",
patientId);
return;
}
var activeMonitorObservations = activeMonitorObservationsList.FindAll(o => !o.Expired);
if (pobs != null && !((PatientObservation)pobs).Expired)
activeMonitorObservations.Add((PatientObservation)pobs);
if (activeMonitorObservations.Any(o => o.Code != null && _transcutanous.Contains(o.Code))) monitorValue += 1;
if (activeMonitorObservations.Any(o => o.Code != null && _regionalBrainSaturation.Contains(o.Code)))
monitorValue += 1;
//Patient treatment
var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(patientId);
activeTreatments = activeTreatments.ToList().FindAll(p =>
p != null && p.RequestedGiveCodes.Any(r =>
_electroencephalogram.Contains(r.Identifier) && r.Text.Contains("Monitor EEGa")));
if (tobs != null && tobs.OrderControl != OrderControlType.Dc && activeTreatments.Any()) monitorValue += 1;
else if (tobs is { OrderControl: OrderControlType.Nw }) monitorValue += 1;
//Generate calculad monitor obs
var monitorObs = new PatientObservation
{
PatientId = patientId,
Name = "Monitor",
CodingSystem = "ADAS",
Min = 0,
Max = 3,
Time = DateTime.Now,
Value = monitorValue
};
await _observationService.Value.InsertObservation(monitorObs, mapObs: false);
}
///
/// Recalculates the Surgery complexity score by reconciling the supplied treatment's
/// OrderControl (NW adds, DC removes by placer-order identifier) against the
/// patient's other active surgery treatments, then persisting a Surgery observation
/// with a score of 5 when any active surgery treatment remains, or 0 otherwise.
///
/// The treatment that triggered the recalculation.
/// A task that represents the asynchronous calculation operation.
///
private async Task CalculateSurgery(PatientTreatment treatment)
{
var surgeryScore = 0;
var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(treatment.PatientId);
//Filter only surgery treatments
var activeSurgeryTreatments = activeTreatments.ToList().FindAll(t =>
t != null && t.RequestedGiveCodes.Any(code =>
_surgery.Contains(code.Identifier) && _surgeryText.Contains(code.Text)));
switch (treatment.OrderControl)
{
//Refactor solo con los tratamientos activos de cirugía, un DC cancela a su entityIdentifier correspondiente.
case OrderControlType.Nw:
activeSurgeryTreatments.Add(treatment);
break;
case OrderControlType.Dc:
activeSurgeryTreatments.RemoveAll(t =>
t != null && t.PlacerOrder?.EntityIdentifier == treatment.PlacerOrder?.EntityIdentifier);
break;
}
if (activeSurgeryTreatments.Count > 0) surgeryScore = 5;
var surgeryObs = new PatientObservation
{
PatientId = treatment.PatientId,
Name = "Surgery",
Min = 0,
Max = 5,
CodingSystem = "ADAS",
Time = DateTime.Now,
Value = surgeryScore
};
await _observationService.Value.InsertObservation(surgeryObs, mapObs: false);
}
///
/// Computes the patient's active bolus treatments by:
/// (1) optionally including the newly arrived treatment, (2) grouping all bolus-eligible treatments
/// by placer-order NamespaceId, (3) discarding treatments whose administration time was
/// later overridden by an end-time, (4) keeping only the latest message-time entry per distinct
/// administration time, and (5) filtering to the configured medication-bolus catalogue, an
/// administration time within the last 12 hours, and a non-empty RXA status contained in
/// _rxaStatus.
///
/// The unique identifier of the patient.
/// The optional new treatment to be included in the active-bolus set.
/// A task that resolves to the list of treatments currently counted as active boluses.
///
private async Task> GetActiveBolus(ObjectId patientId, PatientTreatment? newTreatment)
{
var treatmentsProcessed = new List();
var activeTreatments = await _treatmentService.Value.GetBolusTreatments(patientId);
if (newTreatment != null) activeTreatments.Add(newTreatment);
//Agrupar por namespaceID y quedarme con el último message time que será el último que han reecho.
//Si ese último es concluido suma 1.
var treatmentsGroupedByNamespaceId = activeTreatments
.GroupBy(t => t.PlacerOrder?.NamespaceId)
//.Select(group => group.OrderByDescending(t => t.messageTime)).ToList();
.Select(grp => grp.ToList())
.ToList();
//.Select(g => g.FirstOrDefault()).ToList();
foreach (var treatmentGroup in treatmentsGroupedByNamespaceId)
{
var modificatedBolus = treatmentGroup.FindAll(t =>
t.RequestedGiveCodesStatus.Any(r => r.EndAdministrationTime != DateTime.MinValue));
//Eliminamos todos aquellos que han sido modificados con posterioridad, esto nos lo indica cuando tiene endAdministrationTime != null
treatmentGroup.RemoveAll(t =>
modificatedBolus.Any(m => m.RequestedGiveCodesStatus.FirstOrDefault()?.EndAdministrationTime
== t.RequestedGiveCodesStatus.FirstOrDefault()?.AdministrationTime));
/* treatmentGroup.RemoveAll(t => t.requestedGiveCodesStatus
.Any(r => r.endAdministrationTime == null && modificatedBolus.FirstOrDefault(m =>
m.requestedGiveCodesStatus.Any(code => code.endAdministrationTime == r.administrationTime))
!= null));
*/
//modificatedBolus.Any(b =>
// b.requestedGiveCodesStatus.Any(code => code.endAdministrationTime == r.administrationTime))));
//Opción B a veces se cancelan sin endTime. Simplemente, agrupar por administrationTime y quedarme con el último por time del mensaje.
treatmentGroup.RemoveAll(t => !t.RequestedGiveCodesStatus.Any());
var groupByAdmTime = treatmentGroup.GroupBy(t =>
t.RequestedGiveCodesStatus.FirstOrDefault()!.AdministrationTime)
.Select(grp => grp.OrderByDescending(t => t.MessageTime).First());
treatmentsProcessed.AddRange(groupByAdmTime);
//todos los que tengan startOfAdministration y no tenga esa fecha ninguno de modificatedBolus en endOfTreatment
}
return treatmentsProcessed
.FindAll(p =>
p.RequestedGiveCodes.Any(r => _medicationBolus.Contains(r.Identifier)) &&
p.RequestedGiveCodesStatus.FirstOrDefault() != null &&
p.RequestedGiveCodesStatus.FirstOrDefault()!.AdministrationTime?.ToUniversalTime() >
DateTime.UtcNow.AddHours(-12) &&
p.RequestedGiveCodesStatus.FirstOrDefault()!.AdministrationTime?.ToUniversalTime() <= DateTime.UtcNow &&
p.RequestedGiveCodesStatus.Any(rxa => rxa.Status != null && _rxaStatus.Contains(rxa.Status)));
}
///
/// Persists the OpiateBoluses observation whose value is the count of currently active bolus
/// treatments for the patient (as computed by GetActiveBolus including the new treatment).
///
/// The treatment that triggered the recalculation and should be included in the active-bolus set.
/// A task that represents the asynchronous insert operation.
///
private async Task CalculateBolus(PatientTreatment treatment)
{
var activeBolus = await GetActiveBolus(treatment.PatientId, treatment);
var bolusObs = new PatientObservation
{
PatientId = treatment.PatientId,
Time = DateTime.Now,
Name = "OpiateBoluses",
CodingSystem = "ADAS",
Value = activeBolus.Count
};
await _observationService.Value.InsertObservation(bolusObs);
}
///
/// Applies the red-alert rule for mean arterial pressure (TAm) in neonates: when TAm is below
/// the patient's gestational age (in weeks) for the first week, or below the fixed gestational
/// age thereafter, the TAm observation's Status is set to Alert; otherwise
/// it is set to Ok. If the supplied observation is a gestational-age value (not TAm),
/// a fresh TAm observation is re-inserted with the recalculated status. The threshold is read
/// from the latest Age_Gestational observation when its value is below 2 weeks,
/// otherwise from the latest Age_Gestational_Fixed.
///
/// The base patient observation that triggered the alert evaluation (TAm, Age_Gestational, or Age_Gestational_Fixed).
/// A task that represents the asynchronous calculation operation.
///
public async Task CalculateTAmAlert(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
var tamStatus = StatusEnum.Type.Ok;
var groupedObservations = new List { pobs };
var gestationalWeeksToCheck = 0;
if (obs.Name == "Age_Gestational")
groupedObservations = await _observationService.Value.FindLastObservations(obs.PatientId, 1,
["Age_Gestational_Fixed", "TAm"]);
if (obs.Name == "Age_Gestational_Fixed")
groupedObservations = await _observationService.Value.FindLastObservations(obs.PatientId, 1,
["Age_Gestational", "TAm"]);
if (obs.Name == "TAm")
groupedObservations = await _observationService.Value.FindLastObservations(obs.PatientId, 1,
["Age_Gestational", "Age_Gestational_Fixed"]);
var ageGestational = groupedObservations.FirstOrDefault(o => o.Name == "Age_Gestational");
if (ageGestational != null && int.TryParse(ageGestational.Value.ToString(), out var ageParsed) && ageParsed < 2)
{
gestationalWeeksToCheck = Convert.ToInt32(ageGestational.Value);
}
else
{
var ageGestationalFixed = groupedObservations.FirstOrDefault(o => o.Name == "Age_Gestational_Fixed");
if (ageGestationalFixed != null) gestationalWeeksToCheck = Convert.ToInt32(ageGestationalFixed.Value);
//var gestationalWeeks = int.Parse(ageGestationalFixed.value.ToString());
//if (Convert.ToInt32(pobs.value) < gestationalWeeks) TAmStatus = ObservationStatus.Alert;
}
var tamObs = groupedObservations.FirstOrDefault(o => o.Name == "TAm");
if (tamObs != null)
if (Convert.ToInt32(tamObs.Value) < gestationalWeeksToCheck)
tamStatus = StatusEnum.Type.Alert;
if (pobs.Name == "TAm")
{
pobs.Status = tamStatus;
}
else
{
if (tamObs != null)
{
tamObs.Id = ObjectId.GenerateNewId();
tamObs.Time = DateTime.Now;
tamObs.Status = tamStatus;
await _observationService.Value.InsertObservation(tamObs, mapObs: false);
}
}
}
///
/// Computes the temperature gradient between the incubator and the patient (incubator − patient)
/// when both temperatures have been observed within 10 minutes of each other. Persists the
/// Temp_Gradient observation with the resulting value, after shifting its timestamp via
/// CheckObsWithSameTimeExistsAndIncrementTime to avoid colliding with a previous
/// gradient observation. Returns early when the companion temperature is missing or the two
/// readings fall outside the 10-minute window.
///
/// The base patient observation that triggered the calculation (Temp_Patient or Temp_Incubator).
/// A task that represents the asynchronous calculation operation.
///
public async Task CalculateTempGradient(BasePatientObservation obs)
{
List tempRetrievedesFromBd;
PatientObservation? tempRetrievedFromBd = null;
var pobs = (PatientObservation)obs;
if ("Temp_Patient".Equals(pobs.Name))
{
tempRetrievedesFromBd =
await _observationService.Value.FindLastObservations(pobs.PatientId, 1, ["Temp_Incubator"]);
tempRetrievedFromBd = tempRetrievedesFromBd.FirstOrDefault();
}
if ("Temp_Incubator".Equals(pobs.Name))
{
tempRetrievedesFromBd =
await _observationService.Value.FindLastObservations(pobs.PatientId, 1, ["Temp_Patient"]);
tempRetrievedFromBd = tempRetrievedesFromBd.FirstOrDefault();
}
//Now Temp Gradient is only calculated when temp_Patient and Temp_Incubator come in last 10 min.
//if (tempRetrievedFromBd != null && DateTime.UtcNow.CompareTo(tempRetrievedFromBd.time.ToUniversalTime().AddMinutes(10)) <= 0)
if (tempRetrievedFromBd == null) return;
var difFechas = pobs.Time.ToUniversalTime() - tempRetrievedFromBd.Time.ToUniversalTime();
//if (tempRetrievedFromBd != null && pobs.time.ToUniversalTime().CompareTo(tempRetrievedFromBd.time.ToUniversalTime().AddMinutes(10)) <= 0)
if (Math.Abs(difFechas.TotalMinutes) <= 10 &&
double.TryParse(tempRetrievedFromBd.Value.ToString(), out var tempRetrievedFromBdParsed) &&
double.TryParse(pobs.Value.ToString(), out var pobsParsed))
{
double tempGradientValue;
if ("Temp_Patient".Equals(pobs.Name))
tempGradientValue = tempRetrievedFromBdParsed - pobsParsed;
else
tempGradientValue = pobsParsed - tempRetrievedFromBdParsed;
var tempGradientObs = new PatientObservation
{
PatientId = obs.PatientId,
Name = "Temp_Gradient",
CodingSystem = "ADAS",
Time = obs.Time,
Value = tempGradientValue
};
var obsToInsert = (PatientObservation)await CheckObsWithSameTimeExistsAndIncrementTime(tempGradientObs);
obsToInsert.Id = ObjectId.GenerateNewId();
await _observationService.Value.InsertObservation(obsToInsert, mapObs: false);
}
}
///
/// Enumeration of the intravenous-line types recognised by the UCIN complexity model,
/// ordered from most invasive (Artery) to least invasive (Peripheral).
///
private enum IntraVenousLineTypes
{
Artery,
CentralVein,
Picc,
MiddleLine,
Peripheral
}
///
/// Enumeration of the respiratory-assistance types recognised by the UCIN complexity model,
/// ordered from highest score (Ino) to lowest (None).
///
// ReSharper disable once UnusedMember.Local
private enum RespiratoryTypes
{
Ino,
Vafo,
Vmc,
Vmni,
NasalCannulas,
None
}
}