Creado apartir del commit b888de6c92056de294456f581a56e25e734d4ab7 de develop

This commit is contained in:
jrojas
2026-06-26 09:52:35 +02:00
commit 319fd3dfb0
746 changed files with 106610 additions and 0 deletions
@@ -0,0 +1,695 @@
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;
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;
}
public Task CalculateActiveBolus(ObjectId patientId)
{
throw new NotImplementedException();
}
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;
}
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;
}
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
{
return await Task.FromResult(pumpObservation);
}
public Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
//Esto viene del schedulerService para chequear los medicamentos activos
CheckMedicationCategories(patientId, activeMedicines);
return Task.CompletedTask;
}
public async Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(id);
return activeTreatments;
}
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
throw new NotImplementedException();
}
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;
}
public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
{
return Task.FromResult(listToInsert);
}
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
return Task.FromResult(obs);
}
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
throw new NotImplementedException();
}
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;
}
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);
}
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);
}
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;
}
}
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;
}
}
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);
}
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);
}
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;
}
}
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;
}
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));
}
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;
}
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>);
}
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);
}
}
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
}
}
@@ -0,0 +1,711 @@
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 adas_core.Domain.Utils;
using adas_core.Domain.Utils.Interfaces;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
namespace adas_core.Application.Customizations.HUVH.UCIN;
public class CalculatedObservations : ICalculatedObservations
{
private const string Spo2PreObservationName = "SpO2";
private const string Spo2PostObservationName = "SpO2_Post";
private const string Spo2PrePostObservationName = "SpO2_Pre_Post";
private const string CodingSystem = "ADAS";
private readonly List<string> _complexityObservations =
[
"Respiratory_Device_Multivalue", "Intravenous_Routes_Multivalue", "Medication_Multivalue",
"System_Multivalue", "Newborn_Weight", "Surgery_Multivalue"
];
private readonly ILogger<CalculatedObservations> _logger;
private readonly IMappingUtils _mappingUtils;
private readonly Lazy<IMedicineService> _medicineService;
private readonly Lazy<IObservationService> _observationService;
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;
_mappingUtils = new MappingUtils(apiSettings);
}
public Task CalculateActiveBolus(ObjectId patientId)
{
throw new NotImplementedException();
}
public async Task<T?> Map<T>(T obs, bool onlyByName = false) where T : BasePatientObservation
{
if (obs.Name is "Intervention_In" or "Intervention_Out") await CalculateInterventionMultiValueObservation(obs);
if (obs.Name is "Surgery") await CalculateMultiValueObservation(obs);
if (obs.Name != null && _complexityObservations.Contains(obs.Name))
{
_logger.LogDebug("Mapping observation Complexity {obs}", obs);
//TODO ver si hay un máximo de puntuación por grupo de observaciones
_ = CalculateComplexity(obs);
}
if (obs.Name is Spo2PreObservationName or Spo2PostObservationName)
{
_logger.LogDebug("Pre-post saturation observation {obs}", obs);
_ = CalculateSaturation_DiffObservation(obs);
}
if (obs.Name is "ALPS" or "NPASS_Sedation" or "NPASS_Analgesia") _ = CalculatePainScale(obs);
if (obs.Name is "Last_Defecation") obs = (T)CalculateLastDefecationValue(obs);
return obs;
}
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;
}
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
{
return await Task.FromResult(pumpObservation);
}
public async Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
var medicineObs = new PatientObservation
{
PatientId = patientId,
Name = "Medication_Multivalue",
CodingSystem = CodingSystem,
Value = activeMedicines.Select(m => m.Name ?? string.Empty).Distinct().ToArray(),
Time = DateTime.Now
};
var lastMedicationsObs =
await _observationService.Value.FindLastObservations(patientId, 1, ["Medication_Multivalue"]);
var lastMedicationObs = lastMedicationsObs.FirstOrDefault();
if (lastMedicationObs != null)
{
//expiramos la anterior
lastMedicationObs.Expired = true;
_ = _observationService.Value.UpdateObservation(lastMedicationObs);
}
await _observationService.Value
.InsertObservation(
medicineObs); //mapObs:volvemos a mapear la observación para que pase por el cálculo de la complejidad
}
public async Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(id);
return activeTreatments;
}
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
throw new NotImplementedException();
}
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;
}
public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
{
return Task.FromResult(listToInsert);
}
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
return Task.FromResult(obs);
}
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
throw new NotImplementedException();
}
private async Task CalculatePainScale(BasePatientObservation obs)
{
if (obs is not PatientObservation pobs) return;
object valueObs;
switch (pobs.Name)
{
case "ALPS":
valueObs = pobs.Value;
break;
case "NPASS_Sedation":
{
//Buscamos la complementaria "NPASS Analgesia"
var analgesiaObs =
await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["NPASS_Analgesia"]);
var analgesiaValue = analgesiaObs.FirstOrDefault()?.Value.ToString() ?? "0";
valueObs = $"{pobs.Value}/{analgesiaValue}";
}
break;
case "NPASS_Analgesia":
{
//Buscamos la complementaria "NPASS Sedation"
var sedationObs =
await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["NPASS_Sedation"]);
var sedationValue = sedationObs.FirstOrDefault()?.Value.ToString() ?? "0";
valueObs = $"{sedationValue}/{pobs.Value}";
}
break;
default:
return;
}
var painScaleObs = new PatientObservation
{
PatientId = obs.PatientId,
Time = DateTime.UtcNow,
CodingSystem = "ADAS",
Name = "Pain_Scale",
Value = valueObs
};
_ = _observationService.Value.InsertObservation(painScaleObs, mapObs: false);
}
private static BasePatientObservation CalculateLastDefecationValue(BasePatientObservation obs)
{
if (obs is not PatientObservation pobs) return obs;
//Assign obs time to value
//Convert the UTC DateTime to local time.
var localTime = pobs.Time.ToLocalTime();
//Format the local time into a short date/time string.
pobs.Value = localTime.ToString("dd/MM/yyyy HH:mm");
return pobs;
}
private async Task CalculateMultiValueObservation(BasePatientObservation obs)
{
//El valor de la observación es un array de strings
if (obs is not PatientObservation pobs || string.IsNullOrEmpty(obs.Name)) return;
var newObsName = $"{obs.Name}_Multivalue";
var actualObsInBd =
await _observationService.Value.FindLastObservations(obs.PatientId, 1, [newObsName]);
var newValue = new List<string>();
if (pobs.Value is not string stringValueName) return;
if (actualObsInBd.Count > 0)
if (actualObsInBd.First().Value is string[] oldValue)
{
oldValue = oldValue.Where(x => x != stringValueName).ToArray();
newValue.AddRange(oldValue);
}
newValue.Add(stringValueName);
if (newValue.Count == 0) return;
//Expiramos la anterior
var actualObs = actualObsInBd.FirstOrDefault();
if (actualObs != null)
{
actualObs.Expired = true;
await _observationService.Value.UpdateObservation(actualObs);
}
var newObservation = new PatientObservation
{
PatientId = obs.PatientId,
Time = DateTime.UtcNow,
CodingSystem = CodingSystem,
Name = newObsName,
Value = newValue.ToArray()
};
_logger.LogDebug(
"Calculate {name}_Multivalue {outpatient} Insert Calculated {time} value {value}",
newObsName, newObservation.PatientId, newObservation.Time,
newObservation.Value.ToJson());
await _observationService.Value.InsertObservation(newObservation);
}
private async Task CalculateInterventionMultiValueObservation(BasePatientObservation obs)
{
if (obs is not PatientObservation pobs || string.IsNullOrEmpty(obs.Name) ||
pobs.Value is not string valueObs) return;
if (!GetInterventionValue(valueObs, out var r) || r == null) return;
var obsType = r.Value.type; //"Intravenous_Routes";
var obsGroup = r.Value.Name; // "Vía arterial";
var newObsName = $"{obsType}_Multivalue";
// Obtener la última observación multivalue
var result = await _observationService.Value.FindLastObservations(obs.PatientId, 1, [newObsName]);
var actualMultiValueObsInDb = result.FirstOrDefault(o => !o.Expired);
var oldValue = actualMultiValueObsInDb?.Value as string[] ?? [];
var newValue = new List<string>(oldValue);
if (obs.Name.Contains("_In"))
{
//Por seguridad solo tomamos el último valor que nos llega
//Puede ser que no se acuerden de cancelar el anterior antes de añadir un nuevo dispositivo y no puede haber dos dispositivos del mismo tipo activos al mismo tiempo
if (obsType == "Respiratory_Device")
newValue.Clear();
// Inserción dispositivo
if (!oldValue.Contains(obsGroup))
newValue.Add(obsGroup);
}
else if (obs.Name.Contains("_Out"))
{
// Retirada del dispositivo
//Buscamos la inserción
var name = $"{obs.Name.Split('_')[0]}_In";
var inObs = await _observationService.Value.FindLastNotExpiredObservatonsByPatient(obs.PatientId, name);
var sameValueObs = inObs.Where(o => o.Value == pobs.Value).ToList();
if (sameValueObs.Any())
// Si hay más de una observación del mismo tipo, no eliminar del multivalue
if (sameValueObs.Count > 1)
return;
// Actualizar valor eliminando el grupo
newValue.Remove(obsGroup);
}
else
{
return;
}
//Expiramos la anterior
if (actualMultiValueObsInDb is { Expired: false })
{
actualMultiValueObsInDb.Expired = true;
await _observationService.Value.UpdateObservation(actualMultiValueObsInDb);
}
// Si no hay valores nuevos, no se crea una nueva observación
if (!newValue.Any())
return;
// Crear y registrar nueva observación
var newObservation = new PatientObservation
{
PatientId = obs.PatientId,
Time = DateTime.UtcNow,
CodingSystem = CodingSystem,
Name = newObsName,
Value = newValue.ToArray()
};
_logger.LogDebug(
"Calculate {name}_Multivalue {patientid} Insert Calculated {time} value {value}",
newObsName, newObservation.PatientId, newObservation.Time,
newObservation.Value.ToJson()
);
await _observationService.Value.InsertObservation(newObservation);
}
private bool GetInterventionValue(string valueObs, out (string type, string Name, string Group)? r)
{
var code = valueObs.Split(" ")[0]; //
if (code.EndsWith(".")) // Verifica si termina con un punto
code = code.Substring(0, code.Length - 1).Replace('.', ','); // Remueve el último carácter
if (!double.TryParse(code, out var codeParsed))
{
r = null;
return false;
}
r = _mappingUtils.SearchByCode(codeParsed, "Intervention"); //TODO validar este parámetro con Lola
return r != null;
}
/**
* Calculamos la complejidad basándonos en el valor de Respiratorio/Vías/Medicación/Sistemas/Peso nacimiento/cirugía
* cuando entra una nueva observación que afecte a complejidad, se recalcula de 0 para no tener que estar pendiente de qué puntos corresponden a qué observación
* Valor máximo 46 Guardamos la observación que nos llega y recalculamos complejidad.
*/
private async Task CalculateComplexity(BasePatientObservation obs)
{
var pobs = obs as PatientObservation;
if (pobs == null) return;
var complexity = new PatientObservation
{
PatientId = obs.PatientId,
Time = DateTime.UtcNow,
CodingSystem = CodingSystem,
Name = "Complexity",
Value = 0
};
var complexityValue = 0;
// Diccionario para manejar las operaciones basadas en el nombre de la observación
var operations = new Dictionary<string, Func<ObjectId, PatientObservation?, Task<int>>>
{
{
"Respiratory_Device_Multivalue",
(patientId, observation) =>
CalculateMultivalueObservationComplexityValue(patientId, "Respiratory_Device_Multivalue",
observation)
},
{
"Intravenous_Routes_Multivalue",
(patientId, observation) =>
CalculateMultivalueObservationComplexityValue(patientId, "Intravenous_Routes_Multivalue",
observation)
},
{
"Medication_Multivalue",
(patientId, observation) =>
CalculateMultivalueObservationComplexityValue(patientId, "Medication_Multivalue", observation)
},
{
"System_Multivalue",
(patientId, observation) =>
CalculateMultivalueObservationComplexityValue(patientId, "System_Multivalue", observation)
},
{ "Newborn_Weight", CalculateWeightNewBornValue },
{
"Surgery_Multivalue",
(patientId, observation) =>
CalculateMultivalueObservationComplexityValue(patientId, "Surgery_Multivalue", observation)
}
};
// Realizar cálculos iterando sobre las operaciones
foreach (var operation in operations)
complexityValue += await operation.Value(pobs.PatientId, operation.Key.Equals(pobs.Name) ? pobs : null);
complexity.Value = Math.Min(complexityValue, 46); // Limitar el valor máximo a 46
_logger.LogDebug(
"complexity for patient {patientid} => inserting complexity value: {complexityValue} for patient: {complexityPatientid} at time {fixedTimeComplexityTime}",
complexity.PatientId, complexity.Value, complexity.PatientId, complexity.Time);
await _observationService.Value.InsertObservation(complexity, mapObs: false);
}
private async Task<int> CalculateMultivalueObservationComplexityValue(ObjectId patientId, string observationName,
PatientObservation? observation = null)
{
var result =
await _observationService.Value.FindLastObservations(patientId, 1, [observationName]);
var actualObsInBd = result.FirstOrDefault();
var valueObj = observation == null ? actualObsInBd?.Value : observation.Value;
var valueList = valueObj as string[];
var valueToReturn = valueList?.Sum(v => _mappingUtils.GetComplexityValue(v)) ?? 0;
_logger.LogDebug(
"complexity for patient {patientid} => observation: {observationName} plus complexity: {Value}", patientId,
observationName, valueToReturn);
return valueToReturn;
}
private async Task<int> CalculateWeightNewBornValue(ObjectId patientId, PatientObservation? pobs = null)
{
var result =
await _observationService.Value.FindLastObservations(patientId, 1, ["Newborn_Weight"]);
var actualObsInBd = result.FirstOrDefault();
string? strValue;
string? obsName;
if (pobs == null)
{
if (actualObsInBd?.Value == null) return 0;
strValue = actualObsInBd.Value.ToString();
obsName = actualObsInBd.Name;
}
else
{
if (pobs.Units == "kg") pobs = ParseWeight(pobs);
strValue = pobs.Value.ToString();
obsName = pobs.Name;
}
if (!double.TryParse(strValue, out var valueParsed) || obsName == null)
return 0;
var weightNewBornValue = _mappingUtils.GetComplexityValue(obsName, valueParsed);
_logger.LogDebug(
"complexity for patient {patientid} => Weight_Newborn plus complexity: {Weight_NewbornValue}",
patientId, weightNewBornValue);
return weightNewBornValue;
}
private PatientObservation ParseWeight(PatientObservation obs)
{
if (!double.TryParse(obs.Value.ToString(), out var dValue))
{
_logger.LogError("Error casting weight Observation {obs}:", obs);
return obs;
}
obs.Units = "gr";
obs.Value = dValue * 1000;
return obs;
}
private async Task CalculateSaturation_DiffObservation(BasePatientObservation obs)
{
var toSearchList = new List<string>();
PatientObservation? pre;
PatientObservation? post;
toSearchList.AddRange(new List<string> { Spo2PreObservationName, Spo2PostObservationName });
var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, toSearchList);
var time = obs.Time;
if (obs.Name == Spo2PreObservationName)
{
pre = (PatientObservation)obs;
post = values.FirstOrDefault(o => o.Name == Spo2PostObservationName);
if (post?.Time != null && time.CompareTo(post.Time) > 0) time = post.Time;
}
else
{
post = (PatientObservation)obs;
pre = values.FirstOrDefault(o => o.Name == Spo2PreObservationName);
if (pre?.Time != null && time.CompareTo(pre.Time) > 0) time = pre.Time;
}
if (pre?.Value != null && post?.Value != null)
{
var preSuccess = double.TryParse(pre.Value.ToString(), out var preValue);
if (!preSuccess) preValue = 0;
var postSuccess = double.TryParse(post.Value.ToString(), out var postValue);
if (!postSuccess) postValue = 0;
var saturationDiff = Math.Round(preValue - postValue, 2);
var saturationDiffObs = new PatientObservation
{
Name = "SpO2_Diff",
CodingSystem = CodingSystem,
PatientId = obs.PatientId,
Time = time,
Value = saturationDiff
};
var saturationPrePost = new PatientObservation
{
Name = Spo2PrePostObservationName,
CodingSystem = CodingSystem,
PatientId = obs.PatientId,
Time = time,
Value = pre.Value + "/" + post.Value
};
await _observationService.Value.InsertObservation(saturationDiffObs);
await _observationService.Value.InsertObservation(saturationPrePost);
}
}
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)
{
if (t == null) continue;
if (t.PlacerOrder?.EntityIdentifier == treatment.PlacerOrder?.EntityIdentifier)
{
//Es el mismo tratamiento actualizamos
if (t.EndTime != null && t.EndTime > DateTime.UtcNow) treatment.OrderControl = OrderControlType.Dc;
treatment.Id = t.Id; //Asignamos el ID del tratamiento existente para actualizarlo
_ = _treatmentService.Value.UpdateTreatment(treatment);
}
else
{
result.Add(t);
}
}
return result;
}
private async Task CheckTreatmentMedicines(PatientTreatment treatment)
{
var newMedicines = new List<Medicine>();
var hasVasoactives = false;
foreach (var code in treatment.RequestedGiveCodes)
{
var m = await _medicineService.Value.GetByCode(code.Identifier);
if (m == null) continue;
//Si el código es de tipo nutrición entonces creamos una observación de ese tipo que tendrá el valor mapeado con el nombre correspondiente
//CreateNutritionObservation y NO agregamos a medicamentos
var r = _mappingUtils.SearchByCode(m.Codes.First(), "Treatment");
if (r is { type: "Nutrition" })
{
await CreateNutritionObservation(r.Value.group, treatment.PatientId);
continue;
}
if (r is { type: "Medication", group: "Drogas vasoactivas" })
hasVasoactives = true;
newMedicines.Add(m);
}
if (hasVasoactives)
{
var vObs = new PatientObservation
{
Name = "HasVasoactive",
Value = true,
Time = DateTime.UtcNow,
CodingSystem = CodingSystem
};
await _observationService.Value.InsertObservation(vObs, mapObs: false);
}
if (newMedicines.Count == 0)
return;
var _ = await GetActiveTreatmentsByPatient(treatment.PatientId);
var activeTreatments = _.ToList();
var __ = await _medicineService.Value.GetMedicinesOfTreatments(activeTreatments); //Todo revisar
var activeMedicines = __.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();
await CalculateMedicineObservation(uniqueMedicines, treatment.PatientId);
}
private async Task CreateNutritionObservation(string value, ObjectId patientId)
{
var nutritionObs = new PatientObservation
{
PatientId = patientId,
Name = "Feeding_Type",
CodingSystem = CodingSystem,
Value = value,
Time = DateTime.Now
};
if (value.ToLower().Contains("parenteral"))
{
nutritionObs.Name = "Parenteral";
nutritionObs.Value = "SI";
}
var lastNutritionObsList =
await _observationService.Value.FindLastObservations(patientId, 1, [nutritionObs.Name]);
var lastNutritionObs = lastNutritionObsList.FirstOrDefault();
if (lastNutritionObs is { Expired: false })
{
//expiramos la anterior
lastNutritionObs.Expired = true;
_ = _observationService.Value.UpdateObservation(lastNutritionObs);
}
await _observationService.Value.InsertObservation(nutritionObs);
}
}