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 _complexityObservations = [ "Respiratory_Device_Multivalue", "Intravenous_Routes_Multivalue", "Medication_Multivalue", "System_Multivalue", "Newborn_Weight", "Surgery_Multivalue" ]; private readonly ILogger _logger; private readonly IMappingUtils _mappingUtils; private readonly Lazy _medicineService; private readonly Lazy _observationService; private readonly Lazy _treatmentService; public CalculatedObservations(IServiceProvider serviceProvider) { _treatmentService = serviceProvider.GetRequiredService>(); _medicineService = serviceProvider.GetRequiredService>(); _observationService = serviceProvider.GetRequiredService>(); _logger = serviceProvider.GetRequiredService>(); var apiSettings = serviceProvider.GetRequiredService>(); //apiSettings; _mappingUtils = new MappingUtils(apiSettings); } public Task CalculateActiveBolus(ObjectId patientId) { throw new NotImplementedException(); } public async Task Map(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 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 Map(PumpObservation pumpObservation) { return await Task.FromResult(pumpObservation); } public async Task CalculateMedicineObservation(List 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> GetActiveTreatmentsByPatient(ObjectId id) { var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(id); return activeTreatments; } public Task Map(PatientDiagnosis diagnosis) { throw new NotImplementedException(); } public async Task 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> PreMapList(List listToInsert) { return Task.FromResult(listToInsert); } public Task 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(); 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(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>> { { "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 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 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(); PatientObservation? pre; PatientObservation? post; toSearchList.AddRange(new List { 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> CheckExpiredPatientTreatments(PatientTreatment treatment) { //si el tratamiento ha expirado actualizamos el OrderControl a DC y devolvemos las que siguen activas var result = new List(); 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(); 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); } }