Files
adas-core/adas-core.Application/Customizations/HUVH/UCIA/CalculatedObservations.cs
T
2026-06-26 10:29:23 +02:00

843 lines
42 KiB
C#

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;
using MongoDB.Driver;
namespace adas_core.Application.Customizations.HUVH.UCIA;
/// <summary>
/// Represents a concrete implementation of the <see cref="ICalculatedObservations"/> interface,
/// providing functionality to manage a collection of calculated observations.
/// </summary>
public class CalculatedObservations : ICalculatedObservations
{
private const string CodingSystem = "ADAS";
private readonly List<string>? _antibioticList;
private readonly List<string>? _antidepressantsList;
private readonly List<string>? _antihypertensivesList;
private readonly List<string>? _antipsicoticList;
private readonly List<string>? _anxiolyticsList;
private readonly List<string>? _inotropicMedicines;
private readonly ILogger<CalculatedObservations> _logger;
private readonly Lazy<IMedicineService> _medicineService;
private readonly List<string>? _neuroMedicationList;
private readonly Lazy<IObservationService> _observationService;
private readonly List<string>? _sedationList;
private readonly List<string>? _serumColloidList;
private readonly List<string>? _serumCrystalloidList;
private readonly Lazy<ITreatmentService> _treatmentService;
public CalculatedObservations(IServiceProvider serviceProvider)
{
_treatmentService = serviceProvider.GetRequiredService<Lazy<ITreatmentService>>();
_medicineService = serviceProvider.GetRequiredService<Lazy<IMedicineService>>();
_observationService = serviceProvider.GetRequiredService<Lazy<IObservationService>>();
_logger = serviceProvider.GetRequiredService<ILogger<CalculatedObservations>>();
var apiSettings = serviceProvider.GetRequiredService<IOptions<ApiSettings>>(); //apiSettings;
_sedationList = apiSettings.Value.Sedation;
_inotropicMedicines = apiSettings.Value.InotropicMedicines;
_antibioticList = apiSettings.Value.AntibioticList;
_anxiolyticsList = apiSettings.Value.AnxiolyticList;
_antipsicoticList = apiSettings.Value.AntipsicoticList;
_antidepressantsList = apiSettings.Value.AntidepressantsList;
_neuroMedicationList = apiSettings.Value.NeuroMedicationList;
_serumCrystalloidList = apiSettings.Value.SerumCrystalloidList;
_serumColloidList = apiSettings.Value.SerumColloidList;
_antihypertensivesList = apiSettings.Value.AntihypertensivesList;
}
/// <summary>
/// Asynchronously calculates the active bolus for the specified patient.
/// </summary>
/// <param name="patientId">The identifier of the patient whose active bolus should be calculated.</param>
/// <exception cref="NotImplementedException">Thrown when the method is invoked, as the calculation logic has not yet been implemented.</exception>
public Task CalculateActiveBolus(ObjectId patientId)
{
throw new NotImplementedException();
}
/// <summary>
/// Processes a patient observation by dispatching to the appropriate domain-specific calculation based on the observation name, handling over-sedation, ventilation mode, ECMO location, over-analgesia, driving pressure, ROX index, diuresis, and rehabilitation alarm assessments.
/// </summary>
/// <param name="obs">The patient observation to map; may be replaced with the result of the matched asynchronous calculation when applicable.</param>
/// <param name="onlyByName">Indicates whether the mapping should be performed using only the observation's name.</param>
/// <returns>The processed patient observation, potentially updated by the applicable calculation.</returns>
public async Task<T?> Map<T>(T obs, bool onlyByName = false) where T : BasePatientObservation
{
if (obs.Name is "PSI" or "RASS" or "TS") _ = CalculateOverSedation(obs);
if (obs.Name is "Inspiratory_Pressure" or "Compliancia" or "Air_Flow") _ = CalculateVentilationMode(obs);
if (obs.Name is "ECMO_Location" or "Location_Drainage_Cannula" or "Location_Return_Cannula")
obs = (T)await CalculateEcmoLocation(obs);
if (obs.Name is "EVN" or "ANI" or "ESCID") _ = CalculateOverAnalgesia(obs);
if (obs.Name is "Pmeset" or "PEEP") _ = CalculateDrivingPressure(obs);
if (obs.Name is "SpO2_FiO2_Ratio" or "FR") _ = CalculateRoxIndex(obs);
if (obs.Name is "Diuresis") _ = CalculateDiuresis(obs);
if (obs.Name is "Rehabilitation") obs = (T)await CalculateRehabilitationAlarm(obs);
return obs;
}
/// <summary>
/// Maps a <see cref="PatientTreatment"/> by determining the appropriate order control status, handling cases for missing placer orders, previous expired treatments, and treatments with a defined end time.
/// </summary>
/// <param name="treatment">The patient treatment to map and update.</param>
/// <returns>The patient treatment with its order control set according to the evaluated business rules.</returns>
public async Task<PatientTreatment> Map(PatientTreatment treatment)
{
var order = treatment.PlacerOrder?.EntityIdentifier; //aquí almacenamos el número de orden
if (string.IsNullOrEmpty(order)) return treatment;
//comprobamos el estado de los tratamientos anteriores
var oldTreatments = await CheckExpiredPatientTreatments(treatment);
treatment.OrderControl = oldTreatments.Any() ? OrderControlType.Xo : OrderControlType.Nw;
if (treatment.EndTime != null)
//el tratamiento ha expirado
treatment.OrderControl = OrderControlType.Dc;
await CheckTreatmentMedicines(treatment);
return treatment;
}
/// <summary>
/// Maps a <see cref="PumpObservation"/> instance to its output representation as a pass-through operation.
/// </summary>
/// <param name="pumpObservation">The pump observation to map.</param>
/// <returns>A completed task containing the provided <see cref="PumpObservation"/>.</returns>
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
{
return await Task.FromResult(pumpObservation);
}
/// <summary>
/// Calculates medicine observations for a patient by validating the categories of their active medications. Typically invoked by the scheduler service to review current medication statuses.
/// </summary>
/// <param name="activeMedicines">The list of currently active medicines assigned to the patient.</param>
/// <param name="patientId">The unique identifier of the patient whose medication categories will be checked.</param>
public Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
//Esto viene del schedulerService para chequear los medicamentos activos
CheckMedicationCategories(patientId, activeMedicines);
return Task.CompletedTask;
}
/// <summary>
/// Retrieves all currently active treatments associated with the specified patient.
/// </summary>
/// <param name="id">The unique identifier of the patient whose active treatments should be retrieved.</param>
/// <returns>A collection of <see cref="PatientTreatment"/> objects representing the patient's active treatments.</returns>
public async Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(id);
return activeTreatments;
}
/// <summary>
/// Asynchronously maps a <see cref="PatientDiagnosis"/> instance to its corresponding data representation or transfer model.
/// </summary>
/// <param name="diagnosis">The <see cref="PatientDiagnosis"/> to be mapped.</param>
/// <returns>A task that represents the asynchronous mapping operation, containing the resulting <see cref="PatientDiagnosis"/>.</returns>
/// <exception cref="NotImplementedException">Thrown when the method is invoked, as the mapping logic has not yet been implemented.</exception>
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
throw new NotImplementedException();
}
/// <summary>
/// Ensures the timestamp of the new observation does not collide with the most recent observation for the same patient and observation name. If the new observation's time is equal to or earlier than the last observation's time (compared at second precision), the new observation's time is incremented by one second. If the observation name is null or empty, the new observation is returned unchanged after logging an error.
/// </summary>
/// <param name="newObservation">The new patient observation whose timestamp should be adjusted to avoid time inconsistencies with prior observations.</param>
/// <returns>The patient observation, with its <c>Time</c> adjusted when a time collision is detected, or the original observation when its name is null or empty.</returns>
public async Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
if (string.IsNullOrEmpty(newObservation.Name))
{
_logger.LogError(
"Error FixTimeInconsistencyWithLast. Observation name is null or empty. Observation: {newObservation}",
newObservation);
return newObservation;
}
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);
return newObservation;
}
/// <summary>
/// Pre-maps the provided list of patient observations, returning it as a completed task for asynchronous processing pipelines.
/// </summary>
/// <param name="listToInsert">The list of patient observations to pre-map before insertion.</param>
/// <returns>A completed task containing the provided list of patient observations.</returns>
public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
{
return Task.FromResult(listToInsert);
}
/// <summary>
/// Maps a source alarm to a patient observation. Returns the provided observation without applying changes from the alarm.
/// </summary>
/// <param name="obs">The patient observation to be returned as the mapping result.</param>
/// <param name="alarmToInsert">The patient observation alarm intended to be associated with the observation.</param>
/// <returns>A task containing the patient observation.</returns>
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
return Task.FromResult(obs);
}
/// <summary>
/// Asynchronously sends an alarm notification associated with a patient observation, identified by a name and an optional alarm code.
/// </summary>
/// <param name="obs">The patient observation that triggers the alarm.</param>
/// <param name="name">The name associated with the alarm.</param>
/// <param name="code">The optional alarm code categorizing the alarm, or null if no code is specified.</param>
/// <returns>A task that represents the asynchronous alarm sending operation.</returns>
/// <exception cref="NotImplementedException">Thrown to indicate that the method has not yet been implemented.</exception>
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
throw new NotImplementedException();
}
/// <summary>
/// Evaluates a rehabilitation observation and marks it as Alert when the degree value is between 0 and 2 (inclusive) for more than seven consecutive days.
/// The observation is returned unchanged when it is not a patient observation, its value cannot be parsed as an integer, or its value is greater than 2.
/// </summary>
/// <param name="obs">The base patient observation to evaluate for the rehabilitation alarm condition.</param>
/// <returns>The observation with its status set to Alert when the six previous rehabilitation observations also fall within the 0-2 range; otherwise the observation is returned as-is.</returns>
private async Task<BasePatientObservation> CalculateRehabilitationAlarm(BasePatientObservation obs)
{
//En rojo si el grado es de 0 a 2 incluidos, por más de 7 días
if (obs is not PatientObservation pobs || !int.TryParse(pobs.Value.ToString(), out var pobsValue) ||
pobsValue > 2) return obs;
var result = await _observationService.Value.FindLastObservations(obs.PatientId, 6, ["Rehabilitation"]);
var obsInAlert = result.Count(o =>
int.TryParse(o.Value.ToString(), out var oValue) && oValue is >= 0 and <= 2);
if (obsInAlert == 6) pobs.Status = StatusEnum.Type.Alert;
return obs;
}
/// <summary>
/// Calculates the diuresis index in ml/kg/h for a patient by summing the diuresis values from the last six hours
/// and dividing by the patient's weight and by six. Handles trigger observations of type "Diuresis" (which also
/// pulls the latest recorded weight) and "Weight" (which supplies the weight directly), and aborts with a warning
/// when fewer than three diuresis values are available or the weight is not greater than zero.
/// </summary>
/// <param name="obs">The incoming base patient observation; only <see cref="PatientObservation"/> instances whose
/// <c>Name</c> is "Diuresis" or "Weight" are processed, otherwise the method returns without changes.</param>
private async Task CalculateDiuresis(BasePatientObservation obs)
{
// Calculado como el sumatorio de la Diuresis de las últimas 6h
// entre el último peso medido del paciente y entre 6. Medido en ml/kg/h
// Valor = sum(Diuresis últimas 6 horas) / Weight / 6
if (obs is not PatientObservation pobs)
return;
double weightValue = 0;
var numValues = pobs.Name switch
{
"Diuresis" => 2, // Si la observación entrante es "Diuresis", necesitamos las 2 últimas para completar 6h.
"Weight" => 3, // Si la observación entrante es "Weight", necesitamos las 3 últimas de "Diuresis".
_ => 0
};
if (numValues == 0)
return; // Si no es ni Diuresis ni Weight, no aplicamos cálculo.
// Obtener las observaciones de diuresis necesarias
var diuresisObservations =
await _observationService.Value.FindLastObservations(pobs.PatientId, numValues, ["Diuresis"]);
var diuresisValues = diuresisObservations
.Select(o => double.TryParse(o.Value.ToString(), out var value) ? value : 0)
.ToList();
if (pobs.Name == "Diuresis")
{
// Si la observación entrante es Diuresis, la agregamos
if (double.TryParse(pobs.Value.ToString(), out var currentDiuresis))
diuresisValues.Insert(0, currentDiuresis);
// Buscar el último peso registrado
var weightObservations =
await _observationService.Value.FindLastObservations(pobs.PatientId, 1, ["Weight"]);
var weightObs = weightObservations.FirstOrDefault();
if (weightObs != null && double.TryParse(weightObs.Value.ToString(), out var wObsValue))
weightValue = wObsValue;
}
else if (pobs.Name == "Weight" && double.TryParse(pobs.Value.ToString(), out var wValue))
{
// Si la observación entrante es Weight, asignamos su valor directamente.
weightValue = wValue;
}
// Validaciones antes de calcular
if (diuresisValues.Count < 3)
{
_logger.LogWarning("Calculated Diuresis lacks sufficient values. Required: 3, Found: {count}",
diuresisValues.Count);
return;
}
if (weightValue <= 0)
{
_logger.LogWarning("Calculated Diuresis aborted due to invalid weight value: {weight}", weightValue);
return;
}
// Cálculo del índice de diuresis
var diuresisIndex = diuresisValues.Sum() / weightValue / 6;
var diuresisObs = new PatientObservation
{
PatientId = pobs.PatientId,
Time = DateTime.UtcNow,
CodingSystem = CodingSystem,
Name = "Calculated_Diuresis",
Value = diuresisIndex
};
await _observationService.Value.InsertObservation(diuresisObs, mapObs: false);
}
/// <summary>
/// Calculates the ROX Index by dividing the SpO2/FiO2 ratio by the respiratory rate (FR) and persists the result as a new <c>Rox_Index</c> observation. The method supports both directions of the calculation: when the incoming observation is the SpO2/FiO2 ratio, it retrieves the latest FR, and vice versa. It silently returns if the observation is not a <c>PatientObservation</c>, if the observation name is not recognized, if the required paired observation cannot be found, if its value cannot be parsed, or if the FR value is zero (to avoid division by zero).
/// </summary>
/// <param name="obs">The base patient observation that triggers the ROX index calculation. Only observations named <c>SpO2_FiO2_Ratio</c> or <c>FR</c> are processed; any other type or name is ignored.</param>
private async Task CalculateRoxIndex(BasePatientObservation obs)
{
// Cálculo dividiendo el ratio SpO2/FiO2 entre la FR
// Valor = (SpO2_FiO2_Ratio) / FR
if (obs is not PatientObservation pobs)
return;
List<string>? requiredObservations = pobs.Name switch
{
"SpO2_FiO2_Ratio" => ["FR"], // Si la observación actual es SpO2_FiO2_Ratio, necesitamos FR
"FR" => ["SpO2_FiO2_Ratio"], // Si la observación actual es FR, necesitamos SpO2_FiO2_Ratio
_ => null
};
if (requiredObservations is null)
return;
var result = await _observationService.Value.FindLastObservations(pobs.PatientId, 1, requiredObservations);
var targetObs = result.FirstOrDefault();
if (targetObs == null || !double.TryParse(targetObs.Value.ToString(), out var targetValue))
return;
// Determinar los valores necesarios para el cálculo
var spo2FiO2Ratio = pobs.Name == "SpO2_FiO2_Ratio"
? double.TryParse(pobs.Value.ToString(), out var ratio) ? ratio : 0
: targetValue;
var fr = pobs.Name == "FR"
? double.TryParse(pobs.Value.ToString(), out var frValue) ? frValue : 0
: targetValue;
if (fr == 0) // Evita división por cero
return;
var roxIndexObs = new PatientObservation
{
PatientId = pobs.PatientId,
Time = DateTime.UtcNow,
CodingSystem = CodingSystem,
Name = "Rox_Index",
Value = spo2FiO2Ratio / fr
};
await _observationService.Value.InsertObservation(roxIndexObs, mapObs: false);
}
/// <summary>
/// Calculates the driving pressure for a patient as the difference between Pmeset and PEEP when a new Pmeset or PEEP observation is provided. Looks up the complementary observation to obtain the missing value, then creates and persists a "Driving_Pressure" observation. Returns early if the input is not a PatientObservation, the observation name is neither Pmeset nor PEEP, the complementary observation cannot be found, or its value cannot be parsed as an integer.
/// </summary>
/// <param name="obs">The base patient observation that triggered the calculation; expected to represent a Pmeset or PEEP measurement.</param>
private async Task CalculateDrivingPressure(BasePatientObservation obs)
{
//Diferencia entre la Pmeset y la PEEP
//Calculo Valor = Pmeset - PEEP
try
{
if (obs is not PatientObservation pobs)
return;
var targetObservation = pobs.Name switch
{
"Pmeset" => "PEEP",
"PEEP" => "Pmeset",
_ => null
};
if (targetObservation is null)
return;
var resultList =
await _observationService.Value.FindLastObservations(obs.PatientId, 1, [targetObservation]);
var targetObs = resultList.FirstOrDefault();
if (targetObs == null || !int.TryParse(targetObs.Value.ToString(), out var targetValue))
return;
var peepValue = pobs.Name == "PEEP"
? int.TryParse(pobs.Value.ToString(), out var peep) ? peep : 0
: targetValue;
var pmesetValue = pobs.Name == "Pmeset"
? int.TryParse(pobs.Value.ToString(), out var pmeset) ? pmeset : 0
: targetValue;
var drivingPressureObs = new PatientObservation
{
PatientId = obs.PatientId,
Time = DateTime.UtcNow,
CodingSystem = CodingSystem,
Name = "Driving_Pressure",
Value = pmesetValue - peepValue
};
await _observationService.Value.InsertObservation(drivingPressureObs, mapObs: false);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
/// <summary>
/// Evaluates whether a patient meets the over-analgesia criteria based on pain-related observations (EVN between 0 and 3, ESCID equal to 3, or ANI greater than 70) combined with sustained infusions of morphine, remifentanil, or fentanyl exceeding the analgesia threshold, and records an "Over_Analgesia" observation when the conditions are satisfied.
/// </summary>
/// <param name="obs">The observation that triggered the evaluation; its value must be numeric and it is ignored if <paramref name="patientId"/> is provided (e.g., when invoked from a treatment context).</param>
/// <param name="patientId">The patient identifier when the evaluation is triggered by a treatment event; when null, it is derived from <paramref name="obs"/>.</param>
private async Task CalculateOverAnalgesia(BasePatientObservation? obs, ObjectId? patientId = null)
{
// Mandar observación 'SOBREANALGESIA'
// 0 <= EVN <= 3 o ESCID = 3 o ANI > 70
// y
// Perfusiones de morfina, remifentanilo y fentanilo sostenidas > 24h.
try
{
List<string> requiredObservations = ["EVN", "ESCID", "ANI"];
// Si patientId es null, la información proviene de una observación.
// Si tiene valor, proviene de un tratamiento.
if (!patientId.HasValue)
{
if (obs is not PatientObservation pobs || !int.TryParse(pobs.Value.ToString(), out _))
return;
patientId = pobs.PatientId;
}
var observations =
await _observationService.Value.FindLastObservations(patientId.Value, 1, requiredObservations);
// Verificar si alguna observación cumple la condición
var condition1 = observations.Any(observation =>
int.TryParse(observation.Value.ToString(), out var obsValue) && observation.Name switch
{
"EVN" when obsValue is >= 0 and <= 3 => true,
"ESCID" when obsValue == 3 => true,
"ANI" when obsValue > 70 => true,
_ => false
});
if (!condition1)
return;
// Verificar tratamientos activos y umbral de analgesia
var treatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(patientId.Value);
var patientTreatments = treatments.ToList();
if (!patientTreatments.Any()) return;
var morfina =
patientTreatments.FirstOrDefault(t => t != null && t.RequestedGiveCodes.Any(c => c.Text is "Morfina"));
var remifentanilo = patientTreatments.FirstOrDefault(t =>
t != null && t.RequestedGiveCodes.Any(c => c.Text is "Remifentanilo"));
var fentanilo =
patientTreatments.FirstOrDefault(t =>
t != null && t.RequestedGiveCodes.Any(c => c.Text is "Fentanilo"));
if (morfina != null && !ExceedsAnalgesiaThreshold(morfina) &&
remifentanilo != null && !ExceedsAnalgesiaThreshold(remifentanilo) &&
fentanilo != null && !ExceedsAnalgesiaThreshold(fentanilo))
return;
var overAnalgesiaObs = new PatientObservation
{
PatientId = patientId.Value,
Time = DateTime.UtcNow,
CodingSystem = CodingSystem,
Name = "Over_Analgesia",
Value = bool.TrueString
};
await _observationService.Value.InsertObservation(overAnalgesiaObs, mapObs: false);
}
catch (Exception ex)
{
_logger.LogError("Error calculating Over_Analgesia. Exception: {ex}", ex);
throw;
}
}
/// <summary>
/// Normalizes the ECMO location value of a patient observation. If the observation value matches one of the ECMO variants (ECCO2r, VVDL, VA+V, VVDL+V, VV+V, or VVA), it is mapped to "ECMO"; VV and VA values are left unchanged. If the observation is not a PatientObservation, it is returned unmodified.
/// </summary>
/// <param name="obs">The base patient observation to evaluate and potentially transform.</param>
/// <returns>A task that yields the observation, with its value normalized to "ECMO" when the original value is one of the recognized ECMO variants.</returns>
private Task<BasePatientObservation> CalculateEcmoLocation(BasePatientObservation obs)
{
//Las opciones pueden ser VV, VA o ECMO.
//Cuando llegue VV se muestra VV.
//Cuando llegue VA se muestra VA.
//Cuando llegue los siguientes parámetros, se debe mostrar ECMO: ECCO2r, VVDL, VA+V, VVDL+V, VV+V, VVA
if (obs is not PatientObservation pobs)
return Task.FromResult(obs);
var ecmoValues = new List<string> { "ECCO2r", "VVDL", "VA+V", "VVDL+V", "VV+V", "VVA" };
var obsValue = pobs.Value.ToString();
if (obsValue != null && ecmoValues.Contains(obsValue)) pobs.Value = "ECMO";
return Task.FromResult(obs);
}
/// <summary>
/// Determines the ventilation mode for a patient based on the type of the incoming observation:
/// assigns <c>VMNI</c> when an Inspiratory Pressure value is received without a Compliancia value,
/// assigns <c>VMI</c> when a Compliancia value is received, and reads the mode from the CCC coding
/// system when an Air Flow value is received. If a mode is resolved, it is persisted as a new
/// <c>Ventilation_Mode</c> patient observation; otherwise the method returns without inserting
/// anything.
/// </summary>
/// <param name="obs">The incoming patient observation whose name drives the ventilation mode calculation.</param>
private async Task CalculateVentilationMode(BasePatientObservation obs)
{
//- Si llega valor de PI pero NO COMPL, el tipo de ventilación es VMNI
//- Si llega valor de PS pero no llega COMPL, el tipo de ventilación es VMNI
//- Si llega COMPL, el tipo de ventilación es VMI
//- Si llega Flujo, leer el tipo de ventilación de CCC(CNAF o CTAF)
var mode = string.Empty;
switch (obs.Name)
{
case "Inspiratory_Pressure":
var complObs = await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["Compliancia"]);
if (!complObs.Any())
mode = "VMNI";
break;
case "Compliancia":
mode = "VMI";
break;
case "Air_Flow":
var cccVentMode =
await _observationService.Value.FindByPatientIdAndCodingSystemAsync(obs.PatientId, "CCC",
"Ventilation_Mode");
if (!await cccVentMode.AnyAsync())
return;
mode = cccVentMode.First().Value.ToString() ?? string.Empty;
break;
default:
return;
}
if (string.IsNullOrEmpty(mode)) return;
var ventilationModeObservation = new PatientObservation
{
PatientId = obs.PatientId,
Time = DateTime.UtcNow,
CodingSystem = CodingSystem,
Name = "Ventilation_Mode",
Value = mode
};
_ = _observationService.Value.InsertObservation(ventilationModeObservation, mapObs: false);
}
/// <summary>
/// Evaluates whether an "Over_Sedation" clinical condition should be recorded for a patient.
/// The condition is met when RASS is -4 or -5, PSI is below 25, TS is above 5, and an active treatment exceeds its sedation threshold (Propofol &gt; 3, Midazolam &gt; 0.05, or Isoflurane &gt; 10).
/// If invoked with a single observation, the method extracts the patient identifier, validates the observation value against its own threshold, and then loads the remaining required observations (RASS, PSI, TS) to confirm the full set of conditions before persisting the Over_Sedation observation.
/// </summary>
/// <param name="obs">The triggering patient observation used to evaluate the sedation state. When provided, its name and value drive the initial validation; when null, the method relies on the <paramref name="patientId"/> parameter (typically from a treatment update).</param>
/// <param name="patientId">Optional identifier of the patient to evaluate. If null, it is derived from <paramref name="obs"/>; otherwise, the method performs a full evaluation using the patient's recent observations.</param>
private async Task CalculateOverSedation(BasePatientObservation? obs, ObjectId? patientId = null)
{
//Mandar observación 'SOBRESEDACIÓN'
//cuando:
//(RASS =-4 o RASS =-5) y
//PSI < 25 y
//TS > 5 y
//(Propofol > 3 o Midazolam > 0,05 ó Isoflorano > 10)
try
{
var pobsName = string.Empty;
//si patientId viene null viene la observación
//Si tiene valor viene de un tratamiento
if (!patientId.HasValue)
{
if (obs is not PatientObservation pobs || !int.TryParse(pobs.Value.ToString(), out var pobsValue))
return;
patientId = pobs.PatientId;
pobsName = pobs.Name;
switch (pobsName)
{
case "RASS" when pobsValue is not (-4 or -5):
case "PSI" when pobsValue >= 25:
case "TS" when pobsValue <= 5:
return;
}
}
var requiredObservations = pobsName switch
{
"RASS" => ["PSI", "TS"],
"PSI" => ["RASS", "TS"],
"TS" => ["RASS", "PSI"],
_ => new List<string> { "RASS", "PSI", "TS" }
};
var result = await _observationService.Value.FindLastObservations(patientId.Value, 1, requiredObservations);
var observations = result.ToDictionary(r => r.Name?.ToString() ?? "null");
if ((pobsName != "RASS" && (!observations.TryGetValue("RASS", out var rassObs) ||
!int.TryParse(rassObs.Value.ToString(), out var rassValue) ||
rassValue is not (-4 or -5))) ||
(pobsName != "PSI" && (!observations.TryGetValue("PSI", out var psiObs) ||
!int.TryParse(psiObs.Value.ToString(), out var psiValue) || psiValue >= 25)) ||
(pobsName != "TS" && (!observations.TryGetValue("TS", out var tsObs) ||
!int.TryParse(tsObs.Value.ToString(), out var tsValue) || tsValue <= 5)))
return;
var treatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(patientId.Value);
if (!treatments.Any(ExceedsSedationThreshold)) return;
var overSedationObs = new PatientObservation
{
PatientId = patientId.Value,
Time = DateTime.UtcNow,
CodingSystem = CodingSystem,
Name = "Over_Sedation",
Value = bool.TrueString
};
await _observationService.Value.InsertObservation(overSedationObs, mapObs: false);
}
catch (Exception ex)
{
_logger.LogError("Error calculating Over_Sedation. Exception: {ex}", ex);
throw;
}
}
/// <summary>
/// Determines whether the given patient treatment has exceeded the 24-hour sustained analgesia threshold (e.g., morphine, remifentanil, or fentanyl perfusions).
/// Returns false when the treatment is null or its start time is not set.
/// </summary>
/// <param name="treatment">The patient treatment to evaluate; may be null.</param>
/// <returns><c>true</c> if the treatment has a start time and was started more than 24 hours ago; otherwise, <c>false</c>.</returns>
private static bool ExceedsAnalgesiaThreshold(PatientTreatment? treatment)
{
// Perfusiones de morfina, remifentanilo o fentanilo sostenidas > 24 h
return treatment is { StartTime: not null } &&
treatment.StartTime.Value.AddSeconds(86400) < DateTime.UtcNow;
}
/// <summary>
/// Determines whether a patient treatment exceeds the sedation threshold for Propofol, Midazolam, or Isoflurane based on requested minimum dosage amounts.
/// </summary>
/// <param name="treatment">The patient treatment to evaluate, or <c>null</c>.</param>
/// <returns><c>true</c> if the treatment is not <c>null</c> and any requested medication exceeds its defined sedation threshold; otherwise, <c>false</c>.</returns>
private static bool ExceedsSedationThreshold(PatientTreatment? treatment)
{
return (treatment != null &&
treatment.RequestedGiveCodes.Any(c =>
c.Text == "Propofol" && treatment.RequestedGiveAmountMinimum > 3)) ||
(treatment != null && treatment.RequestedGiveCodes.Any(c =>
c.Text == "Midazolam" && treatment.RequestedGiveAmountMinimum > 0.05)) ||
(treatment != null && treatment.RequestedGiveCodes.Any(c =>
c.Text == "Isoflorano" && treatment.RequestedGiveAmountMinimum > 10));
}
/// <summary>
/// Checks the active treatments of a patient and discontinues those whose end time is in the future by setting their <see cref="PatientTreatment.OrderControl"/> to <see cref="OrderControlType.Dc"/>, returning the remaining active treatments.
/// </summary>
/// <param name="treatment">The patient treatment whose patient is used to look up the list of active treatments to evaluate.</param>
/// <returns>A task that returns the list of active patient treatments that were not discontinued.</returns>
private async Task<List<PatientTreatment>> CheckExpiredPatientTreatments(PatientTreatment treatment)
{
//si el tratamiento ha expirado actualizamos el OrderControl a DC y devolvemos las que siguen activas
var result = new List<PatientTreatment>();
var oldTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(treatment.PatientId);
foreach (var t in oldTreatments.Where(t => t != null))
if (t is { EndTime: not null } && t.EndTime > DateTime.UtcNow)
{
t.OrderControl = OrderControlType.Dc;
_ = _treatmentService.Value.UpdateTreatment(t);
}
else if (t != null)
{
result.Add(t);
}
return result;
}
/// <summary>
/// Validates the medicines associated with a patient treatment by combining newly requested medicines
/// with the patient's currently active medicines, deduplicating them by name, and verifying their
/// medication categories. If no new medicines are found, the method returns without performing any
/// further validation.
/// </summary>
/// <param name="treatment">The patient treatment whose requested medicines will be checked against the patient's active medicines.</param>
private async Task CheckTreatmentMedicines(PatientTreatment treatment)
{
var newMedicines = (await Task.WhenAll(treatment.RequestedGiveCodes
.Select(async code => await _medicineService.Value.GetByCode(code.Identifier))))
.Where(m => m != null)
.ToList();
if (newMedicines.Count == 0) return;
var activeTreatments = (await GetActiveTreatmentsByPatient(treatment.PatientId)).ToList();
var activeMedicines = (await _medicineService.Value.GetMedicinesOfTreatments(activeTreatments)).ToList();
// Combina ambas listas y selecciona solo elementos únicos con el mismo nombre
var uniqueMedicines = newMedicines
.Concat(activeMedicines)
.GroupBy(m => m?.Name)
.Select(g => g.First())
.ToList();
// uniqueMedicines contiene la lista de medicamentos activos de un paciente
CheckMedicationCategories(treatment.PatientId, uniqueMedicines as List<Medicine>);
}
/// <summary>
/// Classifies a patient's unique medicines into predefined clinical medication groups (e.g., sedation, inotropic, antibiotic, anxiolytic, antipsychotic, antidepressants, neuro medication, crystalloid/colloid serum, antihypertensives) and creates a multivalue observation for every category that contains at least one matching medicine. Categories whose list reference is null, medicines with a null or empty name, and categories with no matching medicines are skipped.
/// </summary>
/// <param name="patientId">The identifier of the patient whose medication categories are being evaluated and linked to the created observations.</param>
/// <param name="uniqueMedicines">The distinct list of medicines to be checked against the predefined category reference lists.</param>
private void CheckMedicationCategories(ObjectId patientId, List<Medicine> uniqueMedicines)
{
var medicationCategories = new Dictionary<string, List<string>?>
{
{ "Sedation_Medication_Multivalue", _sedationList },
{ "Inotropic_Medication_Multivalue", _inotropicMedicines },
{ "Antibiotic_Medication_Multivalue", _antibioticList },
{ "Anxiolytic_Medication_Multivalue", _anxiolyticsList },
{ "Antipsychotic_Medication_Multivalue", _antipsicoticList },
{ "Antidepressants_Medication_Multivalue", _antidepressantsList },
{ "Neuro_Medication_Multivalue", _neuroMedicationList },
{ "Crystalloid_Serum_Medication_Multivalue", _serumCrystalloidList },
{ "Colloid_Serum_Medication_Multivalue", _serumColloidList },
{ "Antihypertensives_Medication_Multivalue", _antihypertensivesList }
};
foreach (var category in medicationCategories)
{
var observations = uniqueMedicines?
.Where(m => category.Value != null && !string.IsNullOrEmpty(m.Name) && m.Name != null &&
category.Value.Contains(m.Name))
.ToList();
if (observations != null && observations.Any())
_ = CreateMedicationMultivalueObservation(observations, patientId, category.Key);
}
}
/// <summary>
/// Creates a new multi-value patient observation containing the names of the provided medications, expiring any
/// previous non-expired observation with the same name for the patient before inserting the new one.
/// </summary>
/// <param name="medications">The collection of medications whose names will be stored as the observation values.</param>
/// <param name="patientId">The identifier of the patient the observation belongs to.</param>
/// <param name="obsName">The name used to group and look up the previous observation for the same concept.</param>
private async Task CreateMedicationMultivalueObservation(IEnumerable<Medicine?> medications, ObjectId patientId,
string obsName)
{
var newObs = new PatientObservation
{
PatientId = patientId,
Name = obsName,
CodingSystem = CodingSystem,
Value = medications.Select(m => m?.Name).ToArray(),
Time = DateTime.Now
};
var lastObsList = await _observationService.Value.FindLastObservations(patientId, 1, [obsName]);
var lastObs = lastObsList.FirstOrDefault();
if (lastObs is { Expired: false })
{
//expiramos la anterior
lastObs.Expired = true;
_ = _observationService.Value.UpdateObservation(lastObs);
}
await _observationService.Value.InsertObservation(newObs,
mapObs: false); //mapObs:no volvemos a mapear la observación
}
}