1144 lines
53 KiB
C#
1144 lines
53 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.MongoModels;
|
||
using adas_core.Domain.Models.Pumps;
|
||
using Microsoft.Extensions.DependencyInjection;
|
||
using Microsoft.Extensions.Logging;
|
||
using Microsoft.Extensions.Options;
|
||
using MongoDB.Bson;
|
||
|
||
namespace adas_core.Application.Customizations.HRYC;
|
||
|
||
/// <summary>
|
||
/// Implements the HRYC-specific clinical calculations that derive secondary
|
||
/// <see cref="PatientObservation"/> values (NEWS alarms, IROX, Resp_Rate_Calculated, Hydric_Balance_Calculated,
|
||
/// Weight_Diff, Diuresis_Weight, Delta_Pressure, Daily_Balance_Calculated, Allergies, DVE, Drainage_Height,
|
||
/// Resp_Type, and Hour_Balance) from incoming raw observations and active configurations loaded from
|
||
/// <see cref="ApiSettings"/>.
|
||
/// </summary>
|
||
/// <!-- aidoc:v1 sig=1b16cfc -->
|
||
public class CalculatedObservations : ICalculatedObservations
|
||
{
|
||
private readonly IOptions<ApiSettings> _apiSettings;
|
||
private readonly List<string> _codesForInvasiveVentilation = [];
|
||
|
||
private readonly IConfigObservationService _configObservationService;
|
||
|
||
//private readonly Lazy<IBoxService> _boxService;
|
||
private readonly List<string> _highFrequencyVentilation = [];
|
||
private readonly Lazy<ILightBeaconService> _lightBeaconService;
|
||
private readonly ILogger<CalculatedObservations> _logger;
|
||
private readonly List<string> _nonInvasiveVentilation = [];
|
||
private readonly Lazy<IObservationService> _observationService;
|
||
private readonly Lazy<IPatientService> _patientService;
|
||
|
||
|
||
/// <summary>
|
||
/// Initializes a new instance of the <see cref="CalculatedObservations"/> class,
|
||
/// resolving its dependencies (observation, patient, light-beacon, and config-observation services,
|
||
/// plus the logger) from the supplied <see cref="IServiceProvider"/> and loading the configured
|
||
/// code catalogues (high-frequency ventilation, non-invasive ventilation, and invasive ventilation)
|
||
/// from <see cref="ApiSettings"/>.
|
||
/// </summary>
|
||
/// <param name="serviceProvider">The application's service provider used to resolve the required dependencies and configuration.</param>
|
||
/// <!-- aidoc:v1 sig=409f903 body=cf624d7 -->
|
||
public CalculatedObservations(IServiceProvider serviceProvider)
|
||
{
|
||
_observationService = serviceProvider.GetRequiredService<Lazy<IObservationService>>(); //observationService;
|
||
_patientService = serviceProvider.GetRequiredService<Lazy<IPatientService>>();
|
||
_apiSettings = serviceProvider.GetRequiredService<IOptions<ApiSettings>>(); //apiSettings;
|
||
//_boxService = serviceProvider.GetRequiredService<Lazy<IBoxService>>();
|
||
_logger = serviceProvider.GetRequiredService<ILogger<CalculatedObservations>>();
|
||
_lightBeaconService = serviceProvider.GetRequiredService<Lazy<ILightBeaconService>>();
|
||
_configObservationService =
|
||
serviceProvider.GetRequiredService<IConfigObservationService>(); //configObservationService;
|
||
|
||
|
||
var highFrenquencyVentilation = _apiSettings.Value.HighFrequencyVentilation ?? null;
|
||
highFrenquencyVentilation?.ForEach(x => _highFrequencyVentilation.Add(x.Trim()));
|
||
|
||
|
||
var nonInvasiveVentilation = _apiSettings.Value.NonInvasiveVentilation ?? null;
|
||
nonInvasiveVentilation?.ForEach(x => _nonInvasiveVentilation.Add(x.Trim()));
|
||
|
||
var codesForInvasiveVentilation = //TODO comprobar si se refiere a InvasiveVentilation
|
||
_apiSettings.Value.InvasiveVentilation ?? null;
|
||
codesForInvasiveVentilation?.ForEach(x => _codesForInvasiveVentilation.Add(x.Trim()));
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Dispatches the supplied raw observation to the appropriate HRYC calculation based on its
|
||
/// <see cref="BasePatientObservation.Name"/>. The set of supported calculations includes
|
||
/// <c>Resp_Mode</c> (ventilation type), <c>Diuresis</c> / <c>Weight_Current</c>
|
||
/// (weight difference and diuresis-per-kilogram), <c>AllergiesObs</c>, <c>DrainagesObs</c>,
|
||
/// <c>PEEP</c> / <c>Pleateu_Pressure</c> (driving pressure), <c>Daily_Balance</c>,
|
||
/// <c>Hydric_Balance</c> / <c>Hour_Balance</c> (time shifting), <c>FR</c> / <c>Vent_Rate</c>
|
||
/// (respiratory rate), <c>SpO2</c> / <c>FiO2</c> (IROX), and <c>NEWS</c> (alarms).
|
||
/// </summary>
|
||
/// <typeparam name="T">The concrete observation type, deriving from <see cref="BasePatientObservation"/>.</typeparam>
|
||
/// <param name="obs">The observation to map or transform in-place.</param>
|
||
/// <param name="onlyByName">
|
||
/// Reserved for future use. When <see langword="true"/>, restricts the mapping strategy to
|
||
/// name-based lookups only.
|
||
/// </param>
|
||
/// <returns>
|
||
/// A <see cref="Task{T}"/> that resolves to the (possibly transformed) observation,
|
||
/// or <see langword="null"/> when the input observation has no name.
|
||
/// </returns>
|
||
/// <!-- aidoc-review:v1 severity=high kind=wrong_returns
|
||
/// "Documentation claims the method returns null 'when the input observation has no name', but the method always returns the (possibly transformed) obs parameter; the code never returns null, so the null branch described in <returns> does not exist." -->
|
||
public async Task<T?> Map<T>(T obs, bool onlyByName) where T : BasePatientObservation
|
||
{
|
||
_logger.LogTrace("Mapping {name} observation {obs}", obs.Name, obs);
|
||
|
||
switch (obs.Name)
|
||
{
|
||
case "Resp_Mode":
|
||
_logger.LogTrace("Mapping Resp mode observation {obs}", obs);
|
||
await CalculateVentilationMode(obs);
|
||
break;
|
||
case "Diuresis":
|
||
case "Weight_Current":
|
||
if (obs.Name == "Weight_Current") await CalculateWeight_DiffObservation(obs);
|
||
await CalculateDiureis_WeightObservation(obs);
|
||
break;
|
||
case "AllergiesObs":
|
||
await CalculateAllergiesObservation(obs);
|
||
break;
|
||
case "DrainagesObs":
|
||
await CalculateDrainagesObservation(obs);
|
||
break;
|
||
case "PEEP":
|
||
case "Pleateu_Pressure":
|
||
await CalculateDelta_PressureObservation(obs);
|
||
break;
|
||
case "Daily_Balance":
|
||
await CalculateDaily_BalanceObservation(obs);
|
||
break;
|
||
case "Hydric_Balance":
|
||
await CalculateHydricBalanceCalculated(obs);
|
||
obs = (T)await CalculateHydricBalance(obs);
|
||
break;
|
||
case "Hour_Balance":
|
||
obs = (T)await CalculateHourBalance(obs);
|
||
break;
|
||
case "FR":
|
||
await CalculateRespRate(obs);
|
||
await CalculateIrox(obs);
|
||
break;
|
||
case "Vent_Rate":
|
||
await CalculateRespRate(obs);
|
||
break;
|
||
case "SpO2":
|
||
case "FiO2":
|
||
await CalculateIrox(obs);
|
||
break;
|
||
case "NEWS":
|
||
await CalculateNews(obs);
|
||
break;
|
||
}
|
||
|
||
return obs;
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Placeholder mapping for a <see cref="PatientTreatment"/>. The HRYC customization does not
|
||
/// currently derive observations from treatments.
|
||
/// </summary>
|
||
/// <param name="treatment">The treatment to map.</param>
|
||
/// <returns>Never returns a result.</returns>
|
||
/// <exception cref="NotImplementedException">Always thrown because treatment mapping is not implemented in the HRYC customization.</exception>
|
||
/// <!-- aidoc:v1 sig=adce250 body=bfa6f2f -->
|
||
public Task<PatientTreatment> Map(PatientTreatment treatment)
|
||
{
|
||
throw new NotImplementedException();
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Identity mapping for a <see cref="PumpObservation"/>: returns the supplied instance unchanged,
|
||
/// wrapped in a completed task. Provided to satisfy the customization contract; pump observations
|
||
/// do not require HRYC-specific calculation.
|
||
/// </summary>
|
||
/// <param name="pumpObservation">The pump observation to map.</param>
|
||
/// <returns>A task containing the same <see cref="PumpObservation"/> instance that was passed in.</returns>
|
||
/// <!-- aidoc:v1 sig=50be4c1 body=e481734 -->
|
||
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
|
||
{
|
||
return await Task.FromResult(pumpObservation);
|
||
|
||
}
|
||
|
||
/// <summary>
|
||
/// No-op implementation of the medicine-observation calculation hook. The HRYC customization does
|
||
/// not derive Medication or ERMedication observations from the active medicine list.
|
||
/// </summary>
|
||
/// <param name="activeMedicines">The list of active medicines (ignored).</param>
|
||
/// <param name="patientId">The unique identifier of the patient (ignored).</param>
|
||
/// <returns>A completed task.</returns>
|
||
/// <!-- aidoc:v1 sig=b7a50a4 body=6805ef5 -->
|
||
public Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
|
||
{
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
/// <summary>
|
||
/// No-op implementation of the active-bolus calculation hook. The HRYC customization does not
|
||
/// derive an OpiateBoluses observation.
|
||
/// </summary>
|
||
/// <param name="patientId">The unique identifier of the patient (ignored).</param>
|
||
/// <returns>A completed task.</returns>
|
||
/// <!-- aidoc:v1 sig=7d38721 body=6805ef5 -->
|
||
public Task CalculateActiveBolus(ObjectId patientId)
|
||
{
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Returns an empty enumerable of active treatments. The HRYC customization does not currently
|
||
/// maintain a per-patient active-treatment cache.
|
||
/// </summary>
|
||
/// <param name="id">The unique identifier of the patient (ignored).</param>
|
||
/// <returns>A completed task containing an empty <see cref="IEnumerable{PatientTreatment}"/>.</returns>
|
||
/// <!-- aidoc-review:v1 severity=low kind=wrong_returns
|
||
/// "The cref references IEnumerable{PatientTreatment}, but the actual return type is IEnumerable{PatientTreatment?} (element type is nullable)." -->
|
||
public Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
|
||
{
|
||
return Task.FromResult(new List<PatientTreatment?>().AsEnumerable());
|
||
}
|
||
|
||
/// <summary>
|
||
/// Identity mapping for a <see cref="PatientDiagnosis"/>: returns the supplied instance unchanged,
|
||
/// wrapped in a completed task. Provided to satisfy the customization contract.
|
||
/// </summary>
|
||
/// <param name="diagnosis">The <see cref="PatientDiagnosis"/> to map.</param>
|
||
/// <returns>A task containing the same <see cref="PatientDiagnosis"/> instance that was passed in.</returns>
|
||
/// <!-- aidoc:v1 sig=8ddf601 body=ad326e6 -->
|
||
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
|
||
{
|
||
return Task.FromResult(diagnosis);
|
||
}
|
||
|
||
|
||
|
||
/// <summary>
|
||
/// Resolves time-inconsistencies for a new patient observation when it arrives with the same
|
||
/// (down-to-the-second) timestamp as the latest stored observation of the same name. The new
|
||
/// observation's <c>Time</c> is shifted forward by one second to avoid duplicate-key collisions.
|
||
/// </summary>
|
||
/// <param name="newObservation">The new patient observation to evaluate for time inconsistencies.</param>
|
||
/// <returns>
|
||
/// A task that resolves to the (possibly time-shifted) <see cref="PatientObservation"/>.
|
||
/// Returns the input unchanged when its <c>Name</c> is null or empty, logging the error.
|
||
/// </returns>
|
||
/// <!-- aidoc-review:v1 severity=medium kind=wrong_summary
|
||
/// "Summary states the time is shifted 'when it arrives with the same (down-to-the-second) timestamp' as the latest observation, but the code actually shifts whenever the last observation's second-truncated time is greater than OR equal to the new observation's (DateTime.Compare >= 0), i.e., it also corrects cases where the new observation arrives earlier than the latest one." -->
|
||
public async Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
|
||
{
|
||
if (string.IsNullOrEmpty(newObservation.Name))
|
||
{
|
||
_logger.LogError(
|
||
"Error FixTimeInconsistencyWithLast newObservation name ia 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 a batch of patient observations, optimising the order in which they are persisted
|
||
/// when both <c>Vent_Rate</c> and <c>FR</c> are present. If both observations are present with
|
||
/// non-zero values, <c>Vent_Rate</c> is inserted first (and removed from the returned list) so
|
||
/// that <see cref="CalculateRespRate"/> can derive <c>Resp_Rate_Calculated</c> correctly.
|
||
/// </summary>
|
||
/// <param name="listToInsert">The list of patient observations to pre-map. Modified in-place.</param>
|
||
/// <returns>
|
||
/// A task that resolves to the resulting list of observations (with <c>Vent_Rate</c> removed
|
||
/// when it was inserted synchronously, or the original list otherwise).
|
||
/// </returns>
|
||
/// <!-- aidoc:v1 sig=e508c09 body=d3232c6 -->
|
||
public async Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
|
||
{
|
||
var ventRate = listToInsert.FirstOrDefault(obs => obs.Name == "MDC_VENT_RESP_RATE");
|
||
var respRate = listToInsert.FirstOrDefault(obs => obs.Name == "MDC_RESP_RATE");
|
||
|
||
// Check if obs for Vent_Rate and FR is on the list to insert
|
||
if (ventRate == null || respRate == null) return listToInsert;
|
||
|
||
var ventRateValue = double.TryParse(ventRate.Value.ToString(), out var v1) ? v1 : 0;
|
||
// ventRate is on the list to insert but its value is 0
|
||
if (ventRateValue == 0)
|
||
{
|
||
_logger.LogDebug("Find ventRate on PreMap request not processed because its value is 0");
|
||
return listToInsert;
|
||
}
|
||
|
||
var respRateValue = double.TryParse(ventRate.Value.ToString(), out var v2) ? v2 : 0;
|
||
// respRate is on the list to insert but its value is 0
|
||
if (respRateValue == 0)
|
||
{
|
||
_logger.LogDebug("Find respRate on PreMap request not processed because its value is 0");
|
||
return listToInsert;
|
||
}
|
||
|
||
|
||
// Exists both obs and have value != 0
|
||
// In that case we need to calculate Resp_Rate_Calculated just with the value of Vent_Rate
|
||
try
|
||
{
|
||
_logger.LogDebug(
|
||
"Find on PreMap Vent_Rate and FR in the same request process Vent_Rate first to calculate Resp_Rate_Calculated");
|
||
await _observationService.Value.InsertObservation(ventRate);
|
||
listToInsert.Remove(ventRate);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
_logger.LogError("Error while PreMap on insert obs: {ventRate} exception: {e}", ventRate, e);
|
||
}
|
||
|
||
return listToInsert;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Identity mapping for a source alarm observation paired with a <see cref="PatientObservationAlarm"/>:
|
||
/// returns the supplied observation unchanged, wrapped in a completed task. Provided to satisfy the
|
||
/// customization contract.
|
||
/// </summary>
|
||
/// <param name="obs">The patient observation that triggered the alarm.</param>
|
||
/// <param name="alarmToInsert">The alarm metadata to be inserted alongside the observation.</param>
|
||
/// <returns>A task containing the same <see cref="PatientObservation"/> instance that was passed in.</returns>
|
||
/// <!-- aidoc:v1 sig=405db8e body=ea790e4 -->
|
||
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
|
||
{
|
||
return Task.FromResult(obs);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Placeholder alarm dispatch hook used by the customization contract. The HRYC customization
|
||
/// dispatches NEWS alarms through its private <c>SendAlarm(BasePatientObservation, AlarmEnum.Name)</c>
|
||
/// overload; calling this public overload always throws.
|
||
/// </summary>
|
||
/// <param name="obs">The patient observation that triggered the alarm.</param>
|
||
/// <param name="name">The alarm display name.</param>
|
||
/// <param name="code">The optional alarm code from <see cref="AlarmEnum.Name"/>.</param>
|
||
/// <returns>Never returns a result.</returns>
|
||
/// <exception cref="NotImplementedException">Always thrown because this overload is not used in the HRYC customization.</exception>
|
||
/// <!-- aidoc:v1 sig=90426a4 body=bfa6f2f -->
|
||
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
|
||
{
|
||
throw new NotImplementedException();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Translates a NEWS (National Early Warning Score) value into an alarm level: a value below 5
|
||
/// (or an unparsable value) is reported as <c>NewsOff</c>, between 5 and 7 as <c>NewsWarning</c>,
|
||
/// and 7 or above as <c>NewsAlert</c>. Returns early when the patient cannot be located.
|
||
/// </summary>
|
||
/// <param name="obs">The base patient observation that triggered the calculation, expected to be a <see cref="PatientObservation"/> whose <c>Name</c> is <c>NEWS</c>.</param>
|
||
/// <returns>A task that represents the asynchronous alarm evaluation.</returns>
|
||
/// <!-- aidoc:v1 sig=a2f90ec body=b1f8d11 -->
|
||
private async Task CalculateNews(BasePatientObservation obs)
|
||
{
|
||
var pobs = (PatientObservation)obs;
|
||
var parsed = int.TryParse(pobs.Value.ToString(), out var valueParsed);
|
||
var patient = await _patientService.Value.FindById(obs.PatientId);
|
||
if (patient == null) return;
|
||
//var beaconConfig = await _boxService.Value.GetBeaconConfig(new PatientLocation(patient.PointOfCare, patient.Bed));
|
||
|
||
//power off beacon
|
||
|
||
//recuperar la beacon del paciente y encenderla apagarla si el valor es > 5&6 o 7 alert
|
||
|
||
if ((parsed && valueParsed < 5) || !parsed)
|
||
//await _balizaService.Value.PowerOffLed(beaconCnf);
|
||
//await _observationService.Value.SendObsBroadcast(obs);
|
||
await SendAlarm(obs, AlarmEnum.Name.NewsOff);
|
||
else
|
||
switch (valueParsed)
|
||
{
|
||
case >= 5 and < 7:
|
||
_logger.LogDebug("beacon yellow alert by NEWS for patient: {patientid}", obs.PatientId);
|
||
await SendAlarm(obs, AlarmEnum.Name.NewsWarning);
|
||
//await _balizaService.Value.SendBeaconColor(patient, LightBeaconColor.YELLOW);
|
||
break;
|
||
case >= 7:
|
||
_logger.LogDebug("beacon red alert by NEWS for patient: {patientid}", obs.PatientId);
|
||
await SendAlarm(obs, AlarmEnum.Name.NewsAlert);
|
||
//await _balizaService.Value.SendBeaconColor(patient, LightBeaconColor.RED);
|
||
break;
|
||
}
|
||
|
||
//if (obsAlarm == null) return;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Builds a synthetic <c>Alarm_<Name></c> observation, looks up its configuration, and when
|
||
/// the configured alarm and its beacon are enabled, dispatches a light-beacon colour change for
|
||
/// the patient's point-of-care. The alarm observation is always persisted. Errors are logged and
|
||
/// swallowed.
|
||
/// </summary>
|
||
/// <param name="obs">The base patient observation that triggered the alarm (used to derive <c>PatientId</c> and <c>Time</c>).</param>
|
||
/// <param name="name">The <see cref="AlarmEnum.Name"/> describing the alarm kind (for example, <c>NewsOff</c>, <c>NewsWarning</c>, <c>NewsAlert</c>).</param>
|
||
/// <returns>A task that represents the asynchronous alarm dispatch.</returns>
|
||
/// <!-- aidoc-review:v1 severity=high kind=wrong_summary
|
||
/// "Summary states 'The alarm observation is always persisted', but the method returns early without calling InsertObservation when configObs is null (and also when an exception is thrown), so persistence is not guaranteed." -->
|
||
private async Task SendAlarm(BasePatientObservation obs, AlarmEnum.Name name)
|
||
{
|
||
var pobs = (PatientObservation)obs;
|
||
PatientObservation nObs = new()
|
||
{
|
||
CodingSystem = "ADAS_ALARM",
|
||
Code = name.ToString(),
|
||
Name = $"Alarm_{name}",
|
||
PatientId = pobs.PatientId,
|
||
Time = pobs.Time
|
||
};
|
||
try
|
||
{
|
||
//ConfigObservations
|
||
var configObs = await _configObservationService.Get(new PatientObservation
|
||
{
|
||
Name = nObs.Name,
|
||
PatientId = obs.PatientId
|
||
}
|
||
);
|
||
if (configObs == null)
|
||
{
|
||
_logger.LogError("Config observation is null on send alarm NEWS for {nObs}", nObs);
|
||
return;
|
||
}
|
||
|
||
if (configObs is { Alarm.Enabled: true })
|
||
{
|
||
nObs.Alarm = configObs.Alarm;
|
||
|
||
if (configObs.Alarm.Beacon is { Enabled: true })
|
||
{
|
||
var patient = await _patientService.Value.FindById(obs.PatientId);
|
||
if (patient != null)
|
||
{
|
||
_logger.LogDebug(
|
||
"PatientId: {nObsPatientId}. Send Beacon alarmName {configObsAlarmBeaconBeaconColorValue}",
|
||
nObs.PatientId, configObs.Alarm.Beacon.BeaconColor);
|
||
nObs.Value = configObs.Alarm.Beacon.BeaconColor.ToString();
|
||
SendBeaconCode(configObs.Alarm.Beacon.BeaconColor, patient);
|
||
}
|
||
}
|
||
}
|
||
|
||
_logger.LogDebug(
|
||
"Insert obs alarm for news at: {DateTimeNow} patientId: {nObsPatientId} color value: {nObsValue}",
|
||
DateTime.Now, nObs.PatientId, nObs.Value);
|
||
await _observationService.Value.InsertObservation(nObs);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError("Exception sending alarm: {exMessage}", ex.Message);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Sends a colour command to the light beacon associated with the patient's point-of-care.
|
||
/// Maps <see cref="AlarmEnum.BeaconColor"/> values to <see cref="LightBeaconColor"/> commands
|
||
/// (blue, yellow, red, or off) and logs an error when the patient has no point-of-care identifier.
|
||
/// </summary>
|
||
/// <param name="color">The beacon colour to apply.</param>
|
||
/// <param name="patient">The patient whose associated beacon should be updated.</param>
|
||
/// <!-- aidoc:v1 sig=aced090 body=6297307 -->
|
||
private void SendBeaconCode(AlarmEnum.BeaconColor color, Patient patient)
|
||
{
|
||
if (!patient.PointOfCareId.HasValue)
|
||
{
|
||
_logger.LogError("Error sending beacon color on calculateObservations poc id on patient is null {Patient}",
|
||
patient.ToString());
|
||
return;
|
||
}
|
||
|
||
switch (color)
|
||
{
|
||
case AlarmEnum.BeaconColor.Blue:
|
||
_lightBeaconService.Value.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Blue);
|
||
break;
|
||
case AlarmEnum.BeaconColor.Yellow:
|
||
_lightBeaconService.Value.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Yellow);
|
||
break;
|
||
case AlarmEnum.BeaconColor.Red:
|
||
_lightBeaconService.Value.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Red);
|
||
break;
|
||
case AlarmEnum.BeaconColor.None:
|
||
_lightBeaconService.Value.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Off);
|
||
break;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Computes the IROX index (<c>SpO2 / FiO2 / FR</c>) when <c>SpO2</c>, <c>FiO2</c>, and <c>FR</c>
|
||
/// are all present in the latest observations and were recorded within the last 10 minutes.
|
||
/// Persists the resulting <c>IROX</c> observation. Skips silently when any input is missing,
|
||
/// zero, or unparsable. Errors are logged and swallowed.
|
||
/// </summary>
|
||
/// <param name="obs">The base patient observation that triggered the calculation (<c>SpO2</c>, <c>FiO2</c>, or <c>FR</c>).</param>
|
||
/// <returns>A task that represents the asynchronous calculation operation.</returns>
|
||
/// <!-- aidoc-review:v1 severity=low kind=stale_summary
|
||
/// "Documentation states observations must be 'within the last 10 minutes', but the code checks `o.Time.CompareTo(DateTime.UtcNow.AddMinutes(10)) <= 0` (10 minutes in the future), which is a much more permissive check that allows any past observation and does not enforce a 10-minute-ago cutoff." -->
|
||
private async Task CalculateIrox(BasePatientObservation obs)
|
||
{
|
||
try
|
||
{
|
||
var pob = (PatientObservation)obs;
|
||
var obsToCalc = new List<PatientObservation> { pob };
|
||
switch (pob.Name)
|
||
{
|
||
case "SpO2":
|
||
|
||
var fio2Frvalues = await _observationService.Value.FindLastObservations(obs.PatientId, 1,
|
||
["FiO2", "FR"]);
|
||
if (fio2Frvalues.Count > 0) obsToCalc.AddRange(fio2Frvalues);
|
||
|
||
break;
|
||
case "FiO2":
|
||
var spo2Frvalues = await _observationService.Value.FindLastObservations(obs.PatientId, 1,
|
||
["SpO2", "FR"]);
|
||
if (spo2Frvalues.Count > 0) obsToCalc.AddRange(spo2Frvalues);
|
||
|
||
break;
|
||
case "FR":
|
||
var fio2Spo2Values = await _observationService.Value.FindLastObservations(obs.PatientId, 1,
|
||
["FiO2", "SpO2"]);
|
||
|
||
if (fio2Spo2Values.Count > 0) obsToCalc.AddRange(fio2Spo2Values);
|
||
|
||
|
||
break;
|
||
}
|
||
|
||
if (!obsToCalc.All(o => o.Time.CompareTo(DateTime.UtcNow.AddMinutes(10)) <= 0)) return;
|
||
|
||
var fio2 = obsToCalc.FirstOrDefault(o => o.Name == "FiO2");
|
||
var fr = obsToCalc.FirstOrDefault(o => o.Name == "FR");
|
||
var spo2 = obsToCalc.FirstOrDefault(o => o.Name == "SpO2");
|
||
if (fio2 == null || fr == null || spo2 == null) return;
|
||
|
||
|
||
if (!double.TryParse(spo2.Value.ToString(), out var nSpo2) ||
|
||
!double.TryParse(fio2.Value.ToString(), out var nFio2) ||
|
||
!double.TryParse(fr.Value.ToString(), out var nFr))
|
||
{
|
||
_logger.LogWarning("Error parsing values. fio2: {fio2}, fr: {fr}, spo2: {spo2}", fio2, fr, spo2);
|
||
return;
|
||
}
|
||
|
||
var spo2Value = nSpo2;
|
||
var fio2Value = nFio2;
|
||
var frValue = nFr;
|
||
|
||
if (spo2Value == 0 || fio2Value == 0 || frValue == 0) return;
|
||
|
||
|
||
var value = nSpo2 / nFio2 / nFr;
|
||
|
||
var irox = new PatientObservation
|
||
{
|
||
Name = "IROX",
|
||
CodingSystem = "ADAS",
|
||
PatientId = obs.PatientId,
|
||
Time = obs.Time,
|
||
Value = value
|
||
};
|
||
await _observationService.Value.InsertObservation(irox);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
_logger.LogError("Exception calculating IROX, error: {eMessage}", e.Message);
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// Persists a <c>Resp_Rate_Calculated</c> observation when the supplied observation's value is
|
||
/// non-zero. For <c>FR</c>, the insert is skipped if a recent (within
|
||
/// <c>CalculateRespRateVentExpires</c> minutes) non-zero <c>Vent_Rate</c> observation exists;
|
||
/// the inserted observation's <c>Time</c> is set to the original <c>Time</c> for <c>FR</c>, or
|
||
/// shifted one second forward for any other name.
|
||
/// </summary>
|
||
/// <param name="obs">The base patient observation that triggered the calculation (<c>FR</c> or <c>Vent_Rate</c>).</param>
|
||
/// <returns>A task that represents the asynchronous insert operation.</returns>
|
||
/// <!-- aidoc:v1 sig=0c7e3f7 body=e68f279 -->
|
||
private async Task CalculateRespRate(BasePatientObservation obs)
|
||
{
|
||
var pobs = (PatientObservation)obs;
|
||
_logger.LogDebug("CalculateRespRate {obsPatientid} wait {obsName} {obsTime} {pobsValue}", obs.PatientId,
|
||
obs.Name, obs.Time, pobs.Value);
|
||
if (Convert.ToDouble(pobs.Value) == 0) return;
|
||
|
||
if (Convert.ToDouble(pobs.Value) == 0) return;
|
||
|
||
var insert = true;
|
||
if (obs.Name == "FR")
|
||
{
|
||
// Insertamos si no existe una obs Vent_Rate o si es existe tiene más de X" y su valor es > 0
|
||
var lastVentRate =
|
||
(await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["Vent_Rate"]))
|
||
.FirstOrDefault();
|
||
if (lastVentRate == null || (double.TryParse(lastVentRate.Value.ToString(), out var v) ? v : 0) == 0)
|
||
{
|
||
_logger.LogDebug("CalculateRespRate {id} lastVentRate: null", obs.PatientId);
|
||
}
|
||
else
|
||
{
|
||
var diff = DateTime.UtcNow.Subtract(lastVentRate.Time.ToUniversalTime());
|
||
insert =
|
||
lastVentRate.Time.ToUniversalTime().AddMinutes(_apiSettings.Value.CalculateRespRateVentExpires) <
|
||
DateTime.UtcNow;
|
||
_logger.LogDebug(
|
||
"CalculateRespRate {patientid} lastVentRate: {value} - {time} passed {diff} {insert}",
|
||
obs.PatientId, lastVentRate.Value, lastVentRate.Time, diff, insert ? "Inserted" : "Not inserted");
|
||
}
|
||
}
|
||
|
||
if (insert)
|
||
{
|
||
_logger.LogDebug(
|
||
"CalculateRespRate {patientid} Insert Resp_Rate_Calculated with {name} - {time} value {value}",
|
||
obs.PatientId, obs.Name, obs.Time, pobs.Value);
|
||
var calculatedRespRate = new PatientObservation
|
||
{
|
||
Id = new ObjectId(),
|
||
Name = "Resp_Rate_Calculated",
|
||
CodingSystem = "ADAS",
|
||
PatientId = obs.PatientId,
|
||
Time = obs.Name == "FR" ? obs.Time : obs.Time.AddSeconds(1),
|
||
Value = pobs.Value,
|
||
ParentData = new ParentDataClass { Name = obs.Name }
|
||
};
|
||
await _observationService.Value.InsertObservation(calculatedRespRate);
|
||
}
|
||
else
|
||
{
|
||
_logger.LogDebug(
|
||
"CalculateRespRate {patientid} Insert Resp_Rate_Calculated with {name} - {time} value {value}",
|
||
obs.PatientId, obs.Name, obs.Time, pobs.Value);
|
||
}
|
||
|
||
_logger.LogDebug("CalculateRespRate {patientid} release {name} {time} {value}", obs.PatientId,
|
||
obs.Name, obs.Time, pobs.Value);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Shifts the timestamp of a <c>Hydric_Balance</c> observation forward by one second when another
|
||
/// <c>Hydric_Balance</c> observation was already recorded within the same hour, so the most
|
||
/// recent entry can be identified unambiguously.
|
||
/// </summary>
|
||
/// <param name="obs">The base patient observation expected to be a <see cref="PatientObservation"/> with <c>Name</c> <c>Hydric_Balance</c>.</param>
|
||
/// <returns>A task that resolves to the (possibly time-shifted) base patient observation.</returns>
|
||
/// <!-- aidoc:v1 sig=443610d body=4a73250 -->
|
||
private async Task<BasePatientObservation> CalculateHydricBalance(BasePatientObservation obs)
|
||
{
|
||
var pobs = (PatientObservation)obs;
|
||
|
||
var lastHydricBalanceFromHour =
|
||
await _observationService.Value.FindLastBeforeDate(pobs.PatientId, pobs.Time.AddMinutes(59),
|
||
"Hydric_Balance");
|
||
|
||
if (lastHydricBalanceFromHour != null &&
|
||
DateTime.Compare(new DateTime(lastHydricBalanceFromHour.Time.Year, lastHydricBalanceFromHour.Time.Month,
|
||
lastHydricBalanceFromHour.Time.Day, lastHydricBalanceFromHour.Time.Hour, 0, 0)
|
||
, new DateTime(pobs.Time.Year, pobs.Time.Month, pobs.Time.Day, pobs.Time.Hour, 0, 0)) == 0)
|
||
pobs.Time = lastHydricBalanceFromHour.Time.AddSeconds(1);
|
||
|
||
return obs;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Shifts the timestamp of a <c>Hour_Balance</c> observation forward by one second when another
|
||
/// <c>Hour_Balance</c> observation was already recorded within the same hour.
|
||
/// </summary>
|
||
/// <param name="obs">The base patient observation expected to be a <see cref="PatientObservation"/> with <c>Name</c> <c>Hour_Balance</c>.</param>
|
||
/// <returns>A task that resolves to the (possibly time-shifted) base patient observation.</returns>
|
||
/// <!-- aidoc:v1 sig=23cf188 body=8e46c30 -->
|
||
private async Task<BasePatientObservation> CalculateHourBalance(BasePatientObservation obs)
|
||
{
|
||
var pobs = (PatientObservation)obs;
|
||
|
||
var lastHourBalanceFromHour =
|
||
await _observationService.Value.FindLastBeforeDate(pobs.PatientId, pobs.Time.AddMinutes(59),
|
||
"Hour_Balance");
|
||
|
||
if (lastHourBalanceFromHour != null &&
|
||
DateTime.Compare(new DateTime(lastHourBalanceFromHour.Time.Year, lastHourBalanceFromHour.Time.Month,
|
||
lastHourBalanceFromHour.Time.Day, lastHourBalanceFromHour.Time.Hour, 0, 0)
|
||
, new DateTime(pobs.Time.Year, pobs.Time.Month, pobs.Time.Day, pobs.Time.Hour, 0, 0)) == 0)
|
||
pobs.Time = lastHourBalanceFromHour.Time.AddSeconds(1);
|
||
|
||
return obs;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Persists a <c>Hydric_Balance_Calculated</c> observation mirroring the supplied
|
||
/// <c>Hydric_Balance</c> value, after enforcing three guards: the observation's hour must not be
|
||
/// in the future, the latest <c>Hydric_Balance_Calculated</c> for the current hour prevents
|
||
/// replaying older hours, and a newer stored <c>Hydric_Balance_Calculated</c> prevents
|
||
/// overwriting it. The stored time is shifted one second forward when an existing calculated
|
||
/// observation in the same hour is detected.
|
||
/// </summary>
|
||
/// <param name="obs">The base patient observation that triggered the calculation, expected to be a <see cref="PatientObservation"/> with <c>Name</c> <c>Hydric_Balance</c>.</param>
|
||
/// <returns>A task that represents the asynchronous insert operation.</returns>
|
||
/// <!-- aidoc:v1 sig=721b70b body=271caec -->
|
||
private async Task CalculateHydricBalanceCalculated(BasePatientObservation obs)
|
||
{
|
||
//Hydric_Balance_Calculated
|
||
//No guardamos como calculadas nunca horas futuras. En el obx siempre vienen con la hora y los minutos a 00
|
||
|
||
|
||
var pobs = (PatientObservation)obs;
|
||
var nowUniversalTime = DateTime.Now.ToUniversalTime();
|
||
//La hora de la observación es mayor que la actual. No generamos la calculada
|
||
if (DateTime.Compare(new DateTime(obs.Time.Year, obs.Time.Month, obs.Time.Day, obs.Time.Hour, 0, 0),
|
||
new DateTime(nowUniversalTime.Year, nowUniversalTime.Month, nowUniversalTime.Day, nowUniversalTime.Hour,
|
||
0, 0)) > 0
|
||
)
|
||
{
|
||
_logger.LogDebug(
|
||
"HYDRIC BALANCE: La hora de la observación es mayor que la actual. No generamos la calculada {obs}",
|
||
obs);
|
||
return;
|
||
}
|
||
|
||
//Para la calculada. Si la última que tenemos es de la hora actual y lo que viene es de una hora anterior no entra como calculada.
|
||
var lastHydricBalanceList = await _observationService.Value.FindLastObservations(obs.PatientId, 1,
|
||
["Hydric_Balance_Calculated"]);
|
||
var lastHydricBalance = lastHydricBalanceList.FirstOrDefault();
|
||
|
||
if (lastHydricBalance != null && lastHydricBalance.Time.Hour == DateTime.UtcNow.Hour &&
|
||
obs.Time.Hour != DateTime.UtcNow.Hour)
|
||
{
|
||
_logger.LogDebug(
|
||
"HYDRIC BALANCE: ultima que tenemos es de la hora actual y lo que viene es de una hora anterior no entra como calculada {obs}",
|
||
obs);
|
||
return;
|
||
}
|
||
|
||
//Si el último hydric balance es de una hora posterior a la observación que acaba de llegar no entra como calculada.
|
||
if (lastHydricBalance != null &&
|
||
DateTime.Compare(new DateTime(lastHydricBalance.Time.Year, lastHydricBalance.Time.Month,
|
||
lastHydricBalance.Time.Day, lastHydricBalance.Time.Hour, 0, 0)
|
||
, new DateTime(pobs.Time.Year, pobs.Time.Month, pobs.Time.Day, pobs.Time.Hour, 0, 0)) == 1
|
||
)
|
||
{
|
||
_logger.LogDebug(
|
||
"HYDRIC BALANCE: Si el ultimo hydric balance es de una hora posterior a la observación que acaba de llegar no entra como calculada. {obs}",
|
||
obs);
|
||
return;
|
||
}
|
||
|
||
var calculatedHydricBalance = new PatientObservation
|
||
{
|
||
Name = "Hydric_Balance_Calculated",
|
||
CodingSystem = "ADAS",
|
||
PatientId = obs.PatientId,
|
||
Time = obs.Time.AddSeconds(1), //To discriminate updates.
|
||
Value = pobs.Value,
|
||
Units = obs.Units
|
||
};
|
||
|
||
|
||
if (lastHydricBalance != null && DateTime.Compare(
|
||
new DateTime(lastHydricBalance.Time.Year, lastHydricBalance.Time.Month, lastHydricBalance.Time.Day,
|
||
lastHydricBalance.Time.Hour, 0, 0)
|
||
, new DateTime(pobs.Time.Year, pobs.Time.Month, pobs.Time.Day, pobs.Time.Hour, 0, 0)) == 0)
|
||
calculatedHydricBalance.Time = lastHydricBalance.Time.AddSeconds(1);
|
||
|
||
await _observationService.Value.InsertObservation(calculatedHydricBalance);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Maps a <c>Resp_Mode</c> observation to a <c>Resp_Type</c> value: codes listed in
|
||
/// <c>InvasiveVentilation</c> yield <c>Invasive</c>, otherwise the observation's value is matched
|
||
/// against the configured high-frequency / non-invasive catalogues (yielding
|
||
/// <c>HighFrequencyVentilation</c>, <c>NonInvasive</c>, or <c>Invasive</c> as fallback). Values
|
||
/// equal to <c>"EnESPERA"</c> are skipped.
|
||
/// </summary>
|
||
/// <param name="obs">The base patient observation that triggered the calculation, expected to be a <see cref="PatientObservation"/> with <c>Name</c> <c>Resp_Mode</c>.</param>
|
||
/// <returns>A task that represents the asynchronous insert operation.</returns>
|
||
/// <!-- aidoc-review:v1 severity=low kind=stale_summary
|
||
/// "Summary references identifier 'InvasiveVentilation' (in <c> tags) but the actual field is '_codesForInvasiveVentilation'." -->
|
||
private async Task CalculateVentilationMode(BasePatientObservation obs)
|
||
{
|
||
_logger.LogDebug("CalculateVentilationMode {obs}", obs);
|
||
var pobs = (PatientObservation)obs;
|
||
|
||
var respTypeObs = new PatientObservation
|
||
{
|
||
Name = "Resp_Type",
|
||
CodingSystem = "ADAS",
|
||
PatientId = obs.PatientId,
|
||
Time = obs.Time
|
||
};
|
||
//https://epigram.teamwork.com/#/tasks/34765750
|
||
if (obs.Code != null && _codesForInvasiveVentilation.Contains(obs.Code))
|
||
{
|
||
//añadir que sea solo cuando el valor es invasiva
|
||
respTypeObs.Value = nameof(RespirationType.Invasive);
|
||
respTypeObs.Time = obs.Time.AddSeconds(1);
|
||
await _observationService.Value.InsertIfChanged("Resp_Type", respTypeObs);
|
||
}
|
||
else
|
||
{
|
||
var strValue = pobs.Value.ToString();
|
||
if (strValue == null)
|
||
return;
|
||
|
||
//si es en espera tendrá que insertar que es en espera
|
||
if (!"EnESPERA".Equals(strValue))
|
||
{
|
||
respTypeObs.Value = _highFrequencyVentilation.Contains(strValue)
|
||
? nameof(RespirationType.HighFrequencyVentilation)
|
||
: _nonInvasiveVentilation.Contains(strValue)
|
||
? respTypeObs.Value = nameof(RespirationType.NonInvasive)
|
||
: nameof(RespirationType.Invasive);
|
||
|
||
await _observationService.Value.InsertIfChanged("Resp_Type", respTypeObs);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Computes the <c>Weight_Diff</c> observation (new weight − previous weight, rounded to two
|
||
/// decimals) by comparing the supplied <c>Weight_Current</c> observation against the most
|
||
/// recently stored one for the same patient. Skipped when either value cannot be parsed.
|
||
/// </summary>
|
||
/// <param name="obs">The base patient observation that triggered the calculation, expected to be a <see cref="PatientObservation"/> with <c>Name</c> <c>Weight_Current</c>.</param>
|
||
/// <returns>A task that represents the asynchronous insert operation.</returns>
|
||
/// <!-- aidoc:v1 sig=cb420e5 body=6df7b22 -->
|
||
private async Task CalculateWeight_DiffObservation(BasePatientObservation obs)
|
||
{
|
||
var toSearchList = new List<string>();
|
||
|
||
toSearchList.AddRange(new List<string> { "Weight_Current" });
|
||
|
||
var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, toSearchList);
|
||
|
||
var pWeight = values.FirstOrDefault(o => o.Name == "Weight_Current");
|
||
|
||
var newWeightObs = (PatientObservation)obs;
|
||
|
||
if (pWeight?.Value != null && double.TryParse(pWeight.Value.ToString(), out var weight) &&
|
||
double.TryParse(newWeightObs.Value.ToString(), out var newWeight))
|
||
{
|
||
var weightDiffValue = Math.Round(newWeight - weight, 2);
|
||
|
||
var weightDiff = new PatientObservation
|
||
{
|
||
Name = "Weight_Diff",
|
||
CodingSystem = "ADAS",
|
||
PatientId = obs.PatientId,
|
||
Time = obs.Time,
|
||
Value = weightDiffValue,
|
||
Units = obs.Units
|
||
};
|
||
|
||
await _observationService.Value.InsertObservation(weightDiff);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Computes the <c>Diuresis_Weight</c> observation (diuresis / current weight in ml/kg) when
|
||
/// either a <c>Diuresis</c> or a <c>Weight_Current</c> observation is received. When triggered
|
||
/// by <c>Weight_Current</c>, the operation is skipped if the latest <c>Diuresis</c> observation
|
||
/// has expired. Skipped silently when the weight is zero or any value is unparsable.
|
||
/// </summary>
|
||
/// <param name="obs">The base patient observation that triggered the calculation (<c>Diuresis</c> or <c>Weight_Current</c>).</param>
|
||
/// <returns>A task that represents the asynchronous insert operation.</returns>
|
||
/// <!-- aidoc:v1 sig=c96d1b4 body=0dfe456 -->
|
||
private async Task CalculateDiureis_WeightObservation(BasePatientObservation obs)
|
||
{
|
||
var toSearchList = new List<string>();
|
||
|
||
PatientObservation? diuresisObs;
|
||
PatientObservation? weightObs;
|
||
|
||
toSearchList.AddRange(new List<string> { "Diuresis", "Weight_Current" });
|
||
|
||
var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, toSearchList);
|
||
|
||
if (obs.Name == "Diuresis")
|
||
{
|
||
diuresisObs = (PatientObservation)obs;
|
||
weightObs = values.FirstOrDefault(o => o.Name == "Weight_Current");
|
||
}
|
||
else
|
||
{
|
||
weightObs = (PatientObservation)obs;
|
||
diuresisObs = values.FirstOrDefault(o => o.Name == "Diuresis");
|
||
|
||
if (diuresisObs == null || CheckExpired(diuresisObs)) return;
|
||
}
|
||
|
||
if (weightObs?.Value != null)
|
||
if (double.TryParse(weightObs.Value.ToString(), out var nWeight) && nWeight != 0 &&
|
||
double.TryParse(diuresisObs.Value.ToString(), out var diuresis))
|
||
{
|
||
var diruesisWeightValue = Math.Round(diuresis / nWeight, 2);
|
||
|
||
var diruesisWeight = new PatientObservation
|
||
{
|
||
Name = "Diuresis_Weight",
|
||
CodingSystem = "ADAS",
|
||
PatientId = obs.PatientId,
|
||
Time = obs.Time,
|
||
Value = diruesisWeightValue,
|
||
Units = "ml/kg"
|
||
};
|
||
|
||
await _observationService.Value.InsertObservation(diruesisWeight);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Aggregates a <c>AllergiesObs</c> observation into a single <c>Allergies</c> string per type,
|
||
/// special-casing drug allergies ("FÁRMACOS") into a single grouped entry and setting the
|
||
/// <c>Status</c> to <c>Alert</c> when drug allergies are present. <c>InvalidCastException</c>s
|
||
/// are logged and swallowed.
|
||
/// </summary>
|
||
/// <param name="obs">The base patient observation that triggered the calculation, expected to be a <see cref="PatientObservation"/> with <c>Name</c> <c>AllergiesObs</c> and a collection-valued <c>Value</c> of <see cref="PatientAllergiesValue"/>.</param>
|
||
/// <returns>A task that represents the asynchronous insert operation.</returns>
|
||
/// <!-- aidoc:v1 sig=16bb26b body=661f911 -->
|
||
private async Task CalculateAllergiesObservation(BasePatientObservation obs)
|
||
{
|
||
try
|
||
{
|
||
var pobs = (PatientObservation)obs;
|
||
|
||
var allergiesObs = new PatientObservation
|
||
{
|
||
Name = "Allergies",
|
||
CodingSystem = "ADAS",
|
||
PatientId = obs.PatientId,
|
||
Time = obs.Time,
|
||
MessageTime = pobs.MessageTime
|
||
};
|
||
|
||
var patientAllergiesValues =
|
||
new List<PatientAllergiesValue>((IEnumerable<PatientAllergiesValue>)pobs.Value);
|
||
|
||
var allergiesValues = patientAllergiesValues.GroupBy(o => o.Type)
|
||
.Select(x =>
|
||
new PatientAllergiesValue
|
||
{
|
||
Type = x.Key,
|
||
Value = string.Join(", ", x.Select(v => v.Value)),
|
||
Notes = string.Join(", ", x.Select(n => n.Notes))
|
||
}).ToList();
|
||
|
||
|
||
var farmacosType = false;
|
||
var farmacosValues = new List<string>();
|
||
var values = new List<string>();
|
||
|
||
foreach (var allergies in allergiesValues)
|
||
{
|
||
if (allergies.Value == null)
|
||
continue;
|
||
|
||
var type = allergies.Type?.Replace("Alergia a ", "").Replace("Alergia ", "").ToUpper();
|
||
|
||
if (type != null)
|
||
switch (type)
|
||
{
|
||
case "FÁRMACOS":
|
||
farmacosType = true;
|
||
farmacosValues.Add(allergies.Value.ToUpper());
|
||
break;
|
||
|
||
default:
|
||
values.Add(type);
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (farmacosType)
|
||
{
|
||
values.Add($"FÁRMACOS ({string.Join(", ", farmacosValues)})");
|
||
allergiesObs.Status = StatusEnum.Type.Alert;
|
||
}
|
||
|
||
allergiesObs.Value = string.Join(", ", values);
|
||
|
||
await _observationService.Value.InsertObservation(allergiesObs);
|
||
}
|
||
catch (InvalidCastException)
|
||
{
|
||
_logger.LogError("CalculateAllergiesObservation {obs}", obs);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// For a <c>DrainagesObs</c> observation of type <c>"Drenaje ventricular"</c>, persists the
|
||
/// derived <c>DVE</c> (volume) and <c>Drainage_Height</c> observations using the underlying
|
||
/// <see cref="PatientDrainagesValue"/> properties.
|
||
/// </summary>
|
||
/// <param name="obs">The base patient observation that triggered the calculation, expected to be a <see cref="PatientObservation"/> with <c>Name</c> <c>DrainagesObs</c> and a <see cref="PatientDrainagesValue"/>-typed <c>Value</c>.</param>
|
||
/// <returns>A task that represents the asynchronous insert operation.</returns>
|
||
/// <!-- aidoc:v1 sig=2e0f71d body=98b8c9e -->
|
||
private async Task CalculateDrainagesObservation(BasePatientObservation obs)
|
||
{
|
||
var pobs = (PatientObservation)obs;
|
||
|
||
var patientDraingesValue = (PatientDrainagesValue)pobs.Value;
|
||
|
||
if (patientDraingesValue.Type == "Drenaje ventricular")
|
||
{
|
||
var dve = new PatientObservation
|
||
{
|
||
Name = "DVE",
|
||
CodingSystem = "ADAS",
|
||
PatientId = obs.PatientId,
|
||
Time = obs.Time,
|
||
Units = obs.Units
|
||
};
|
||
|
||
var drainageHeight = new PatientObservation
|
||
{
|
||
Name = "Drainage_Height",
|
||
CodingSystem = "ADAS",
|
||
PatientId = obs.PatientId,
|
||
Time = obs.Time
|
||
};
|
||
|
||
if (patientDraingesValue.Volume != null)
|
||
{
|
||
dve.Value = patientDraingesValue.Volume;
|
||
await _observationService.Value.InsertObservation(dve);
|
||
}
|
||
|
||
if (patientDraingesValue.Height != null)
|
||
{
|
||
drainageHeight.Value = patientDraingesValue.Height;
|
||
await _observationService.Value.InsertObservation(drainageHeight);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Computes the <c>Delta_Pressure</c> observation (driving pressure = plateau − PEEP) when both
|
||
/// <c>PEEP</c> and <c>Pleateu_Pressure</c> observations are available. The stored <c>Time</c> is
|
||
/// the most recent of the two source observations, so the result is anchored to the latest input.
|
||
/// </summary>
|
||
/// <param name="obs">The base patient observation that triggered the calculation (<c>PEEP</c> or <c>Pleateu_Pressure</c>).</param>
|
||
/// <returns>A task that represents the asynchronous insert operation.</returns>
|
||
/// <!-- aidoc:v1 sig=fe22b05 body=bc944b3 -->
|
||
private async Task CalculateDelta_PressureObservation(BasePatientObservation obs)
|
||
{
|
||
var toSearchList = new List<string>();
|
||
|
||
PatientObservation? peep;
|
||
PatientObservation? pleateuPressure;
|
||
|
||
toSearchList.AddRange(new List<string> { "PEEP", "Pleateu_Pressure" });
|
||
|
||
var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, toSearchList);
|
||
var time = obs.Time;
|
||
double pleateuPressureValue = 0;
|
||
|
||
if (obs.Name == "PEEP")
|
||
{
|
||
peep = (PatientObservation)obs;
|
||
pleateuPressure = values.FirstOrDefault(o => o.Name == "Pleateu_Pressure");
|
||
|
||
if (pleateuPressure?.Time != null && time.CompareTo(pleateuPressure.Time) > 0) time = pleateuPressure.Time;
|
||
}
|
||
else
|
||
{
|
||
pleateuPressure = (PatientObservation)obs;
|
||
peep = values.FirstOrDefault(o => o.Name == "PEEP");
|
||
|
||
if (peep?.Time != null && time.CompareTo(peep.Time) > 0) time = peep.Time;
|
||
}
|
||
|
||
if (peep?.Value != null && pleateuPressure?.Value != null)
|
||
{
|
||
var peepSuccess = double.TryParse(peep.Value.ToString(), out var peepValue);
|
||
if (!peepSuccess) peepValue = 0;
|
||
|
||
if (!peepSuccess) pleateuPressureValue = 0;
|
||
|
||
var deltaPressure = Math.Round(pleateuPressureValue - peepValue, 2);
|
||
|
||
|
||
var deltaPressureObs = new PatientObservation
|
||
{
|
||
Name = "Delta_Pressure",
|
||
CodingSystem = "ADAS",
|
||
PatientId = obs.PatientId,
|
||
Time = time,
|
||
Value = deltaPressure
|
||
};
|
||
|
||
await _observationService.Value.InsertObservation(deltaPressureObs);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Persists a <c>Daily_Balance_Calculated</c> observation mirroring the supplied
|
||
/// <c>Daily_Balance</c> value, but only when the local hour of the observation is 8 (the daily
|
||
/// balance cut-off used by the HRYC customization).
|
||
/// </summary>
|
||
/// <param name="obs">The base patient observation that triggered the calculation, expected to be a <see cref="PatientObservation"/> with <c>Name</c> <c>Daily_Balance</c>.</param>
|
||
/// <returns>A task that represents the asynchronous insert operation.</returns>
|
||
/// <!-- aidoc:v1 sig=e959764 body=2322c22 -->
|
||
private async Task CalculateDaily_BalanceObservation(BasePatientObservation obs)
|
||
{
|
||
if (obs.Time.ToLocalTime().Hour == 8)
|
||
{
|
||
var dailyBalanceCalculated = new PatientObservation
|
||
{
|
||
Name = "Daily_Balance_Calculated",
|
||
CodingSystem = "ADAS",
|
||
PatientId = obs.PatientId,
|
||
Time = DateTime.Now,
|
||
Units = obs.Units,
|
||
Value = ((PatientObservation)obs).Value
|
||
};
|
||
|
||
await _observationService.Value.InsertObservation(dailyBalanceCalculated);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Determines whether a patient observation has expired by comparing the current time against
|
||
/// <c>Time + Expires</c> seconds, when <c>Expires</c> is set.
|
||
/// </summary>
|
||
/// <param name="obs">The patient observation to evaluate.</param>
|
||
/// <returns>
|
||
/// <see langword="true"/> when the observation has an <c>Expires</c> value and the current time
|
||
/// is past the expiration instant; otherwise, <see langword="false"/>.
|
||
/// </returns>
|
||
/// <!-- aidoc:v1 sig=c7c5c82 body=4c62b98 -->
|
||
private static bool CheckExpired(PatientObservation obs)
|
||
{
|
||
if (obs.Expires == null) return false;
|
||
|
||
var timeExpire = obs.Time.ToLocalTime().AddSeconds(Convert.ToDouble(obs.Expires));
|
||
|
||
return DateTime.Now.CompareTo(timeExpire) > 0;
|
||
}
|
||
} |