rama creada apartir de master en j

This commit is contained in:
jrojas
2026-06-26 10:29:23 +02:00
parent 319fd3dfb0
commit c1517fda87
2810 changed files with 1927392 additions and 25392 deletions
@@ -8,6 +8,12 @@ using MongoDB.Bson;
namespace adas_core.Application.Customizations.BD;
/// <summary>
/// Represents a service that provides calculated observations, implementing the <see cref="ICalculatedObservations"/> interface.
/// </summary>
/// <remarks>
/// This type is instantiated with a primary constructor that accepts an <see cref="IServiceProvider"/> to resolve its dependencies.
/// </remarks>
public class CalculatedObservations(IServiceProvider serviceProvider) : ICalculatedObservations
{
private readonly ILogger<CalculatedObservations> _logger =
@@ -16,21 +22,43 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
private readonly Lazy<IObservationService> _observationService =
serviceProvider.GetRequiredService<Lazy<IObservationService>>();
/// <summary>
/// Maps the specified patient observation by returning it unchanged within a completed task.
/// The <paramref name="onlyByName"/> parameter is accepted to indicate whether the mapping should match observations by name only.
/// </summary>
/// <param name="obs">The patient observation to map.</param>
/// <param name="onlyByName">Indicates whether the mapping should match observations by name only.</param>
/// <returns>A <see cref="Task{T}"/> containing the provided observation.</returns>
public Task<T?> Map<T>(T obs, bool onlyByName = false) where T : BasePatientObservation
{
return Task.FromResult(obs)!;
}
/// <summary>
/// Returns the provided <see cref="PatientTreatment"/> as a completed task without modification.
/// </summary>
/// <param name="treatment">The patient treatment instance to map.</param>
/// <returns>A completed <see cref="Task{PatientTreatment}"/> containing the provided treatment.</returns>
public Task<PatientTreatment> Map(PatientTreatment treatment)
{
return Task.FromResult(treatment);
}
/// <summary>
/// Maps the provided <see cref="PatientDiagnosis"/> instance to a completed <see cref="Task{TResult}"/>, returning the same diagnosis unchanged.
/// </summary>
/// <param name="diagnosis">The <see cref="PatientDiagnosis"/> instance to map.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the provided <see cref="PatientDiagnosis"/>.</returns>
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
return Task.FromResult(diagnosis);
}
/// <summary>
/// Maps a pump observation, creating an associated alarm observation when the observation code is "IHE PCD-04".
/// </summary>
/// <param name="pumpObservation">The pump observation to be mapped.</param>
/// <returns>The mapped pump observation.</returns>
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
{
if (pumpObservation.Code == "IHE PCD-04")
@@ -38,41 +66,92 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
return pumpObservation;
}
/// <summary>
/// Calculates medicine observations for the specified patient using the provided list of active medicines.
/// </summary>
/// <param name="activeMedicines">The list of active medicines associated with the patient.</param>
/// <param name="patientId">The unique identifier of the patient for whom the observations are calculated.</param>
public Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
return Task.CompletedTask;
}
/// <summary>
/// Asynchronously calculates the active bolus for the specified patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose active bolus is being calculated.</param>
/// <returns>A task that represents the asynchronous calculation of the active bolus.</returns>
public Task CalculateActiveBolus(ObjectId patientId)
{
return Task.CompletedTask;
}
/// <summary>
/// Retrieves the collection of active treatments associated with the specified patient identifier.
/// </summary>
/// <param name="id">The unique <see cref="ObjectId"/> identifier of the patient whose active treatments are being requested.</param>
/// <returns>A task that represents the asynchronous operation, containing an <see cref="IEnumerable{PatientTreatment}"/> of nullable <see cref="PatientTreatment"/> entries for the patient's active treatments.</returns>
/// <exception cref="NotImplementedException">Thrown to indicate that the method is not yet implemented.</exception>
public Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
throw new NotImplementedException();
}
/// <summary>
/// Resolves time inconsistencies between the provided new patient observation and the previously recorded one, returning a corrected observation when applicable.
/// </summary>
/// <param name="newObservation">The new patient observation to be reconciled against the last stored observation.</param>
/// <returns>A task that yields the time-corrected <see cref="PatientObservation"/>, or <c>null</c> when no correction is required or no prior observation exists.</returns>
/// <exception cref="NotImplementedException">Thrown because the method has not been implemented yet.</exception>
public Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
throw new NotImplementedException();
}
/// <summary>
/// Performs a pre-mapping operation on the list of patient observations before further processing.
/// In the base implementation, the list is returned unchanged, serving as a pass-through that may be overridden to apply custom transformations or validations.
/// </summary>
/// <param name="listToInsert">The list of patient observations to be pre-mapped before insertion.</param>
/// <returns>A task containing the provided list of patient observations.</returns>
public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
{
return Task.FromResult(listToInsert);
}
/// <summary>
/// Maps a source <see cref="PatientObservationAlarm"/> onto an existing <see cref="PatientObservation"/>, producing an updated observation that incorporates the alarm data.
/// </summary>
/// <param name="obs">The existing patient observation to which the alarm will be mapped.</param>
/// <param name="alarmToInsert">The source alarm to be inserted/mapped into the observation.</param>
/// <returns>A <see cref="Task{PatientObservation}"/> that represents the asynchronous mapping operation, returning the resulting patient observation.</returns>
/// <exception cref="NotImplementedException">Thrown because the method has not been implemented yet.</exception>
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
throw new NotImplementedException();
}
/// <summary>
/// Sends an alarm notification based on a patient observation, associated name, and optional alarm code.
/// </summary>
/// <param name="obs">The patient observation that triggers the alarm.</param>
/// <param name="name">The name associated with the alarm.</param>
/// <param name="code">The optional alarm code that categorizes the alarm; may be null.</param>
/// <returns>A task that represents the asynchronous alarm sending operation.</returns>
/// <exception cref="NotImplementedException">Thrown because the method is not yet implemented.</exception>
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
throw new NotImplementedException();
}
/// <summary>
/// Creates and inserts a patient observation representing a pump alarm event.
/// The observation value is populated with the device identifier when available, otherwise the infusion identifier,
/// and the patient identifier is only assigned if present on the source pump observation.
/// Any exception raised while inserting the observation is logged and swallowed rather than rethrown.
/// </summary>
/// <param name="pumpObservation">The pump observation containing the alarm state, type, timestamps, and identifiers used to build the alarm patient observation.</param>
/// <returns>A completed <see cref="Task"/> representing the insert operation, which never faults since exceptions are caught internally.</returns>
private Task CreatePumpAlarmObservation(PumpObservation pumpObservation)
{
PatientObservation patientObservationAlarm = new()
@@ -85,10 +164,10 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
: $"Infusion Id {pumpObservation.InfusionId}",
Time = pumpObservation.Time
};
if (pumpObservation.PatientId.HasValue) patientObservationAlarm.PatientId = pumpObservation.PatientId.Value;
//CheckAlarmConfig(patientObservationAlarm);
try
{
@@ -98,7 +177,7 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
{
_logger.LogError("Error inserting Pump Observation Alarm. Exception: {e}", e);
}
return Task.CompletedTask;
}
}
@@ -8,37 +8,75 @@ using MongoDB.Bson;
namespace adas_core.Application.Customizations.CHUO;
/// <summary>
/// Provides calculated observation data by implementing the <see cref="ICalculatedObservations"/> contract.
/// </summary>
/// <remarks>
/// Instances are constructed with an <see cref="IServiceProvider"/> used to resolve dependencies required by the implementation.
/// </remarks>
public class CalculatedObservations(IServiceProvider serviceProvider) : ICalculatedObservations
{
private readonly ILogger<CalculatedObservations> _logger =
serviceProvider.GetRequiredService<ILogger<CalculatedObservations>>();
/// <summary>
/// Asynchronously calculates the active bolus for the specified patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient for whom the active bolus is being calculated.</param>
public Task CalculateActiveBolus(ObjectId patientId)
{
return Task.CompletedTask;
}
/// <summary>
/// Asynchronously maps the provided <see cref="PumpObservation"/> instance, returning the same instance as the result.
/// </summary>
/// <param name="pumpObservation">The pump observation to map.</param>
/// <returns>A <see cref="Task{PumpObservation}"/> containing the provided pump observation.</returns>
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
{
return await Task.FromResult(pumpObservation);
}
/// <summary>
/// Calculates medicine observations for the specified patient based on the provided active medicines.
/// </summary>
/// <param name="activeMedicines">The list of medicines currently active for the patient.</param>
/// <param name="patientId">The identifier of the patient for whom the observation is calculated.</param>
public Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
return Task.CompletedTask;
}
/// <summary>
/// Resolves time inconsistencies between a new patient observation and the most recent one, returning a corrected observation when a discrepancy is detected.
/// </summary>
/// <param name="newObservation">The new patient observation to compare and reconcile against the last stored observation.</param>
/// <returns>A task that yields the corrected <see cref="PatientObservation"/>, or <c>null</c> when no time inconsistency is found.</returns>
/// <exception cref="NotImplementedException">Thrown because the method has not been implemented yet.</exception>
public Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
throw new NotImplementedException();
}
/// <summary>
/// Retrieves the active treatment records associated with the specified patient.
/// </summary>
/// <param name="id">The unique identifier of the patient whose active treatments are being requested.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a collection of nullable <see cref="PatientTreatment"/> objects representing the patient's active treatments.</returns>
/// <exception cref="NotImplementedException">Thrown to indicate that the method has not yet been implemented.</exception>
public Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
throw new NotImplementedException();
}
/// <summary>
/// Maps a patient observation by returning it as-is, logging the operation as a trace entry.
/// </summary>
/// <param name="obs">The patient observation to map.</param>
/// <param name="onlyByName">Indicates whether mapping should be performed by name only.</param>
/// <returns>A task containing the provided observation.</returns>
public Task<T?> Map<T>(T obs, bool onlyByName = false) where T : BasePatientObservation
{
_logger.LogTrace("Mapping {name} observation {obs}", obs.Name, obs);
@@ -47,26 +85,56 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
}
/// <summary>
/// Maps the specified <see cref="PatientTreatment"/> to a resulting <see cref="PatientTreatment"/> instance asynchronously.
/// </summary>
/// <param name="treatment">The patient treatment to be mapped.</param>
/// <returns>A task that represents the asynchronous mapping operation, containing the resulting <see cref="PatientTreatment"/>.</returns>
/// <exception cref="NotImplementedException">Thrown when the method is invoked, as the mapping has not been implemented.</exception>
public Task<PatientTreatment> Map(PatientTreatment treatment)
{
throw new NotImplementedException();
}
/// <summary>
/// Maps a <see cref="PatientDiagnosis"/> instance to a <see cref="PatientDiagnosis"/> result.
/// </summary>
/// <param name="diagnosis">The patient diagnosis to be mapped.</param>
/// <returns>A task representing the asynchronous operation, containing the mapped <see cref="PatientDiagnosis"/>.</returns>
/// <exception cref="NotImplementedException">Thrown because the mapping logic has not yet been implemented.</exception>
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
throw new NotImplementedException();
}
/// <summary>
/// Performs a pre-mapping step on a list of patient observations before they are inserted.
/// Currently acts as a pass-through, returning the provided list unchanged as a completed task, but can be overridden to apply transformations or validations.
/// </summary>
/// <param name="listToInsert">The list of patient observations to be pre-mapped prior to insertion.</param>
/// <returns>A completed task containing the provided list of patient observations.</returns>
public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
{
return Task.FromResult(listToInsert);
}
/// <summary>
/// Maps a source alarm onto a <see cref="PatientObservation"/>, returning the observation as-is without applying the alarm's changes.
/// </summary>
/// <param name="obs">The existing patient observation to be returned.</param>
/// <param name="alarmToInsert">The alarm to be associated with the observation (currently not applied).</param>
/// <returns>A completed <see cref="Task{PatientObservation}"/> containing the original observation.</returns>
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
return Task.FromResult(obs);
}
/// <summary>
/// Sends an alarm notification for the specified patient observation, identified by the given name and optional alarm code.
/// </summary>
/// <param name="obs">The patient observation that triggers the alarm.</param>
/// <param name="name">The name of the alarm to send.</param>
/// <param name="code">The optional alarm code providing additional classification for the alarm.</param>
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
return Task.CompletedTask;
@@ -10,6 +10,13 @@ using MongoDB.Bson;
namespace adas_core.Application.Customizations.H12O.UCIN;
/// <summary>
/// Implements the UCIN-specific clinical calculations that derive secondary
/// <see cref="PatientObservation"/> values (Complexity, Oxygenation_Index, Respiratory, IntravenousLines,
/// Medication, ERMedication, Monitor, Surgery, Temp_Gradient, TAm alert, etc.) from incoming
/// raw observations and active <see cref="PatientTreatment"/> entries, using the catalogue of
/// codes, groups, and types configured in <see cref="ApiSettings"/>.
/// </summary>
public class CalculatedObservations : ICalculatedObservations
{
private readonly List<string> _complexityObservations =
@@ -72,6 +79,14 @@ public class CalculatedObservations : ICalculatedObservations
private readonly Lazy<ITreatmentService> _treatmentService;
/// <summary>
/// Initializes a new instance of the <see cref="CalculatedObservations"/> class,
/// resolving its dependencies (observation, medicine, and treatment services, plus the logger)
/// from the supplied <see cref="IServiceProvider"/> and loading the configured code catalogues
/// (EEG, bolus medications, transcutaneous, regional brain saturation, surgery, respiratory,
/// ONi, ventilation modes, ECMO, and notes indicating medication) from <see cref="ApiSettings"/>.
/// </summary>
/// <param name="serviceProvider">The application's service provider used to resolve the required dependencies and configuration.</param>
public CalculatedObservations(IServiceProvider serviceProvider)
{
_observationService = serviceProvider.GetRequiredService<Lazy<IObservationService>>(); //observationService;
@@ -127,58 +142,82 @@ public class CalculatedObservations : ICalculatedObservations
ecmo?.ForEach(x => _ecmo.Add(x.Trim()));
}
/// <summary>
/// Applies the appropriate clinical calculations to a raw patient observation based on its
/// <see cref="BasePatientObservation.Name"/>, code, coding system, and expiration state, producing
/// derived observations (respiratory assistance, oxygenation index, ventilation mode, TAm alert,
/// parsed weight, temperature gradient, intravenous lines, monitor, surgery expiration, complexity,
/// and hourly/temperature time-increment handling).
/// </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>
public async Task<T?> Map<T>(T obs, bool onlyByName) where T : BasePatientObservation
{
// Early exit: if the observation has no name, there's nothing to map
if (string.IsNullOrEmpty(obs.Name)) return obs;
//Llega Vías o algo en Vías dictionary
//Solo tenemos en cuenta las observaciones de ICA
// Respiratory observations: only process SNOMED-coded respiratory metrics
if (!string.IsNullOrEmpty(obs.Code) && _respiratory.Contains(obs.Code) && obs.CodingSystem == "SNM")
{
_logger.LogDebug("Mapping observation Respiratorio {obs}", obs);
await CalculateAsistResp(obs);
}
// Oxygenation index calculation: triggered by air pressure, FiO2, or PaO2 readings
if (obs.Name is "AirPressure_Mean" or "FiO2" or "PaO2") await CalculateOxygenationIndex(obs);
//Solo tenemos en cuenta las observaciones de Central
// Ventilation mode evaluation: processes respiratory mode changes
if (obs.Name == "Resp_Mode")
{
_logger.LogDebug("Mapping Resp mode observation {obs}", obs);
await CalculateVentilationMode(obs);
}
// Mean arterial pressure (TAm) alerting: includes gestational age considerations
if (obs.Name is "TAm" or "Age_Gestational_Fixed" or "Age_Gestational") await CalculateTAmAlert(obs);
// Weight parsing: only for active (non-expired) patient observations
if (obs.Name is "Weight_Newborn" or "Weight_Current")
if (obs is PatientObservation { Expired: false })
obs = (T)await ParseWeight(obs);
// Temperature gradient calculation between incubator and patient
if (obs is { Name: "Temp_Incubator" } or { Name: "Temp_Patient" }) await CalculateTempGradient(obs);
// Intravenous lines observation processing with potential transformation
if (obs is { Name: "IntravenousLinesObs" })
{
var intraObs = await CalculateIntravenousLineObservation(obs);
// Apply the transformed observation if calculation produced a result
if (intraObs != null)
obs = (T)intraObs;
}
// Regional brain saturation monitoring: pCO2tc or transcutaneous O2 with specific codes
if (obs.Name is "pCO2tc" or "Transcutaneous_O2")
if (obs.Code != null && _regionalBrainSaturation.Contains(obs.Code))
await CalculateMonitor(obs);
// Surgery-related expiration check
if (obs.Name == "Surgery") await CheckSurgeryExpired(obs);
// Complexity calculations for predefined complex observation types
if (obs.Name != null && _complexityObservations.Contains(obs.Name))
{
_logger.LogDebug("Mapping observation Complexity {obs}", obs);
await CalculateComplexity(obs);
}
// Hourly accumulation observations: diuresis, drainages, hydric balance, and fluid entries
// These check for existing observations at the same time and increment values
if (obs.Name != null && (obs.Name.Equals("Diuresis_Hour") ||
obs.Name.Equals("Drainages_Hour") ||
obs.Name.Equals("Hydric_Balance") ||
@@ -186,13 +225,25 @@ public class CalculatedObservations : ICalculatedObservations
obs.Name.Equals("Enteral_Entries_Hour")))
obs = (T)await CheckObsExistsAndIncrementTime(obs);
// Temperature observations: handle duplicate time entries by incrementing time
if (obs.Name != null && (obs.Name.Equals("Temp_Patient") ||
obs.Name.Equals("Temp_Incubator")
))
obs = (T)await CheckObsWithSameTimeExistsAndIncrementTime(obs);
return obs;
}
/// <summary>
/// Maps a <see cref="PatientTreatment"/> to its processed form, computing the end time of single-dose
/// treatments, evaluating the medicines associated with the treatment's codes/notes, and dispatching
/// the appropriate downstream calculations (bolus, monitor, surgery, ECMO complexity, and ONi observations)
/// based on the configured code catalogues.
/// </summary>
/// <param name="treatment">The <see cref="PatientTreatment"/> to be mapped.</param>
/// <returns>A task that represents the asynchronous mapping operation. The task result contains the processed <see cref="PatientTreatment"/>.</returns>
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="treatment"/> parameter is <c>null</c>.</exception>
public async Task<PatientTreatment> Map(PatientTreatment treatment)
{
if (treatment.SingleDose)
@@ -258,18 +309,39 @@ public class CalculatedObservations : ICalculatedObservations
return treatment;
}
/// <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 currently require UCIN-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>
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
{
return await Task.FromResult(pumpObservation);
}
/// <summary>
/// Convenience overload that recalculates the "Medication" and "ERMedication" observations for a
/// patient using the supplied list of active medicines, building a transient <see cref="PatientTreatment"/>
/// for the underlying call.
/// </summary>
/// <param name="activeMedicines">The list of active <see cref="Medicine"/> instances currently associated with the patient.</param>
/// <param name="patientId">The unique identifier of the patient.</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
public async Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
await CalculateMedicineObservation(activeMedicines, [], new PatientTreatment { PatientId = patientId });
}
/// <summary>
/// Recalculates the "OpiateBoluses" observation for a patient based on the bolus treatments
/// returned by <c>GetActiveBolus</c>. Skips persistence when the value is unchanged from the
/// last stored observation.
/// </summary>
/// <param name="patientId">The unique identifier of the patient.</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
public async Task CalculateActiveBolus(ObjectId patientId)
{
// SIN RXA
@@ -317,22 +389,42 @@ public class CalculatedObservations : ICalculatedObservations
await _observationService.Value.InsertObservation(bolusObs, mapObs: false);
}
/// <summary>
/// Retrieves all treatments currently considered active for the specified patient, delegating
/// the actual retrieval to the configured <see cref="ITreatmentService"/>.
/// </summary>
/// <param name="id">The unique identifier of the patient.</param>
/// <returns>A collection of active <see cref="PatientTreatment"/> objects for the patient.</returns>
public async Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(id);
return activeTreatments;
}
/// <summary>
/// Identity mapping for a <see cref="PatientDiagnosis"/>: returns the supplied instance unchanged,
/// wrapped in a completed task. Provided to satisfy the customization contract; diagnoses do not
/// currently require UCIN-specific calculation.
/// </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>
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
return Task.FromResult(diagnosis);
}
/*
* Designed to fix observations like intravenous with the possibility to receive multiple intravenous observations in the same hl7 message.
* Sometimes ADAS calculates many in the same second. causing inconsistencies when retrieving last observation.
*/
/// <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, and a fresh <see cref="ObjectId"/>
/// is generated to avoid duplicate-key collisions.
/// </summary>
/// <param name="newObservation">The new patient observation to evaluate for time inconsistencies.</param>
/// <returns>
/// A task that represents the asynchronous operation. The task result contains the fixed
/// <see cref="PatientObservation"/> if the input had a valid name; otherwise, <see langword="null"/>.
/// </returns>
public async Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
if (string.IsNullOrEmpty(newObservation.Name))
@@ -361,21 +453,54 @@ public class CalculatedObservations : ICalculatedObservations
return newObservation;
}
/// <summary>
/// Identity mapping for a list of <see cref="PatientObservation"/> instances: returns the supplied
/// list unchanged, wrapped in a completed task. Provided to satisfy the customization contract;
/// the pre-mapping phase does not currently require UCIN-specific transformation.
/// </summary>
/// <param name="listToInsert">The list of patient observations to be pre-mapped.</param>
/// <returns>A task containing the original list of patient observations.</returns>
public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
{
return Task.FromResult(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; alarms are not currently transformed in the UCIN customization.
/// </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>
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
return Task.FromResult(obs);
}
/// <summary>
/// Placeholder alarm dispatch hook used by the customization contract. The UCIN customization
/// does not currently implement custom alarm dispatching; calling this method 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 alarm dispatch is not implemented in the UCIN customization.</exception>
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
throw new NotImplementedException();
}
/// <summary>
/// When a "Surgery" observation is marked as expired, inserts a follow-up "Surgery" observation
/// with value <c>0</c> at the same time plus one second, signaling that the surgery complexity
/// contribution has been removed. Errors are logged and rethrown.
/// </summary>
/// <param name="obs">The base patient observation expected to be a <see cref="PatientObservation"/> with <c>Name == "Surgery"</c>.</param>
/// <returns>A task that represents the asynchronous insert operation.</returns>
/// <exception cref="InvalidCastException">Thrown when <paramref name="obs"/> cannot be cast to <see cref="PatientObservation"/>.</exception>
/// <exception cref="Exception">Rethrown after the underlying exception is written to the console for diagnostics.</exception>
public async Task CheckSurgeryExpired(BasePatientObservation obs)
{
try
@@ -404,9 +529,13 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/*
*Used for observations that can come with the same time, the last which enter is the newest. So increment time so that the system can identify which is the last.
*/
/// <summary>
/// For hourly accumulation observations, checks whether another observation with the same name
/// already exists within the same hour and, if so, shifts this observation's <c>Time</c> forward
/// by one second so the system can identify the most recent entry.
/// </summary>
/// <param name="obs">The base patient observation expected to be a <see cref="PatientObservation"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the (possibly time-shifted) base patient observation.</returns>
public async Task<BasePatientObservation> CheckObsExistsAndIncrementTime(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
@@ -423,7 +552,13 @@ public class CalculatedObservations : ICalculatedObservations
return pobs;
}
/// <summary>
/// For temperature observations, checks whether another observation with the same name already
/// exists at the exact same timestamp and, if so, shifts this observation's <c>Time</c> forward
/// by one second so the system can identify the most recent entry.
/// </summary>
/// <param name="obs">The base patient observation expected to be a <see cref="PatientObservation"/>.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the (possibly time-shifted) base patient observation.</returns>
public async Task<BasePatientObservation> CheckObsWithSameTimeExistsAndIncrementTime(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
@@ -435,13 +570,15 @@ public class CalculatedObservations : ICalculatedObservations
return pobs;
}
/// <summary>
/// Oxygenation index is calculated => PMAP x FiO2 x 100 / PaO2
/// the method is called when it receives pmap or fio or pao2 and tries to take the other values to perform the
/// calculation if they are not
/// in database then does nothing.
/// Computes the Oxygenation Index (PMAP × FiO2 × 100 / PaO2) using the supplied observation plus
/// the latest stored <c>AirPressure_Mean</c>, <c>FiO2</c>, and <c>PaO2</c> values. Persists the
/// resulting <c>Oxygenation_Index</c> observation with a 10-second expiration matching FiO2's
/// expiration window. Skipped gracefully when PaO2 is zero, when any of the inputs cannot be
/// parsed, or when an exception is caught and logged.
/// </summary>
/// <param name="obs">The observation that triggered the calculation (one of <c>AirPressure_Mean</c>, <c>FiO2</c>, or <c>PaO2</c>).</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
public async Task CalculateOxygenationIndex(BasePatientObservation obs)
{
var toSearchList = new List<string>();
@@ -512,7 +649,12 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <summary>
/// Sets the <c>EndTime</c> of a single-dose treatment to 8 hours after its <c>OrderTime</c>
/// (or 8 hours from now when <c>OrderTime</c> is <see langword="null"/>).
/// </summary>
/// <param name="treatment">The single-dose treatment whose end time is to be calculated.</param>
/// <returns>The same <see cref="PatientTreatment"/> instance with its <c>EndTime</c> updated.</returns>
private static PatientTreatment CalculateSingleDoseEndDate(PatientTreatment treatment)
{
/* Deprecated calc finish treatment date on shift end. Now every treatment single dose is last 8 Hours
@@ -540,6 +682,14 @@ public class CalculatedObservations : ICalculatedObservations
return treatment;
}
/// <summary>
/// Resolves the medicines associated with a treatment (by matching its codes and notes against the
/// medicine catalogue, falling back to <c>NotesIndicatingMedication</c> patterns), aggregates the
/// patient's active treatments and medicines, detects parenteral nutrition (NPT) treatments, and
/// triggers <c>CalculateMedicineObservation</c> to update the Medication and ERMedication observations.
/// </summary>
/// <param name="treatment">The treatment whose medicines should be checked.</param>
/// <returns>A task that represents the asynchronous check operation.</returns>
public async Task CheckTreatmentMedicines(PatientTreatment treatment)
{
//sI TIENE UNA NOTA CON NPT SON DE TIPO NUTRICIÓN PARENTERAL Y SUMAN 1
@@ -627,6 +777,13 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <summary>
/// Builds a synthetic <see cref="Medicine"/> representing parenteral nutrition (NPT) when the
/// treatment's notes indicate a NPT order. Lipid-based NPTs are tagged with
/// <c>ParenteralNutritionLipids</c>; all other NPTs are tagged with <c>ParenteralNutrition</c>.
/// </summary>
/// <param name="treatment">The treatment to inspect for NPT indicators, or <see langword="null"/>.</param>
/// <returns>A task that resolves to a <see cref="Medicine"/> instance describing the parenteral nutrition, or an unnamed, untyped instance when no NPT notes are present.</returns>
private static Task<Medicine> CalculateParentalNutritionMedicine(PatientTreatment? treatment)
{
List<string> medicineType = [];
@@ -651,13 +808,16 @@ public class CalculatedObservations : ICalculatedObservations
return Task.FromResult(medicine);
}
/*
* Cada vez que entra un tratamiento recalcular medicación y riesgo de medicación: recuperamos todos los tratamientos activos del paciente,
* siendo activos: todos aquellos que no han sido cancelados DC Y SIENDO NW NUEVO,
* Medicación: Si treatment.orderControl = NW entra nuevo sumamos 1 si no hay ninguno de ese tipo ya activo
* si entra treatment.orderControl = DC restamos 1 si no hay ninguno de ese tipo medición.type
* y
*/
/// <summary>
/// Maintains the patient's active medicine list according to the treatment's <c>OrderControl</c>
/// (<c>NW</c> adds, <c>XO</c> adds if missing, <c>DC</c> removes), counts the distinct medicine
/// types, persists the <c>Medication</c> observation if its value changed, and then recalculates
/// the <c>ERMedication</c> observation.
/// </summary>
/// <param name="activeMedicines">The list of active medicines for the patient. Modified in-place.</param>
/// <param name="medicines">The medicines to add or remove depending on <c>OrderControl</c>.</param>
/// <param name="treatment">The treatment that triggered the recalculation.</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
public async Task CalculateMedicineObservation(List<Medicine> activeMedicines, List<Medicine> medicines,
PatientTreatment treatment)
{
@@ -708,7 +868,14 @@ public class CalculatedObservations : ICalculatedObservations
await CalculateErMedication(activeMedicines, treatment);
}
/// <summary>
/// Calculates the patient's risk level from their active medicines and persists the
/// <c>ERMedication</c> observation with min/max bounds of 0 and 5, skipping persistence
/// when the value matches the last stored observation.
/// </summary>
/// <param name="activeMedicines">The list of active medicines used to derive the risk level.</param>
/// <param name="treatment">The treatment that triggered the recalculation.</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
private async Task CalculateErMedication(List<Medicine> activeMedicines, PatientTreatment treatment)
{
//var activeTreatments = treatmentService.Value.GetActiveTreatmentsByPatient(treatment.patientid);
@@ -733,7 +900,14 @@ public class CalculatedObservations : ICalculatedObservations
await _observationService.Value.InsertObservation(erMedicationObs, mapObs: false);
}
/// <summary>
/// Maps the patient's active medicine list to a discrete risk level (0 to 5) based on medicine
/// types and groups: lipidic parenteral nutrition or multiple high-risk medicines map to 5,
/// non-lipid NPT or one or two high-risk medicines map to 4, metabolic medicines map to 2,
/// remaining medicines map to 1, and a list containing only iron/vitamin D maps to 0.
/// </summary>
/// <param name="activeMedicines">The list of active medicines to evaluate.</param>
/// <returns>A task that resolves to the integer risk level (05).</returns>
private static Task<int> CalculateErMedicineLevels(List<Medicine> activeMedicines)
{
var ironVitaminDCodes = new List<string> { "374424002", "175041000140104" };
@@ -784,8 +958,12 @@ public class CalculatedObservations : ICalculatedObservations
return Task.FromResult(1);
}
//TODO revisar con los valores reales cuando se sepan
/// <summary>
/// Maps a <c>Resp_Mode</c> observation's value to a discrete <c>Resp_Type</c> (HighFrequencyVentilation,
/// Invasive, NonInvasive, or None) by matching it against the configured catalogues for each mode.
/// </summary>
/// <param name="obs">The respiratory-mode observation expected to be a <see cref="PatientObservation"/>.</param>
/// <returns>A task that represents the asynchronous insert operation.</returns>
public async Task CalculateVentilationMode(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
@@ -811,6 +989,15 @@ public class CalculatedObservations : ICalculatedObservations
await _observationService.Value.InsertIfChanged("Resp_Type", respTypeObs);
}
/// <summary>
/// Calculates the patient's respiratory assistance score from the supplied observation's
/// <c>Value</c> (matched against the configured respiratory-type catalogue) or from the latest
/// stored <c>Resp_Mode</c> when the supplied observation is <c>ONi</c>. Active ONi treatment
/// (or a non-expired last ONi observation) forces the score to 10. Persists the resulting
/// <c>Respiratory</c> observation and returns the original input unchanged.
/// </summary>
/// <param name="obs">The base patient observation that triggered the calculation (expected to be a <see cref="PatientObservation"/>).</param>
/// <returns>A task that resolves to the same base patient observation that was passed in.</returns>
public async Task<BasePatientObservation> CalculateAsistResp(BasePatientObservation obs)
{
//Comprobar si hay un resp_type más nuevo y si no lo hay ignorar
@@ -862,7 +1049,14 @@ public class CalculatedObservations : ICalculatedObservations
return obs;
}
/// <summary>
/// Updates the synthetic <c>ECMO</c> observation according to the treatment's <c>OrderControl</c>:
/// <c>NW</c> sets it to <c>NW</c>, <c>DC</c> sets it to <c>XO</c> if any other ECMO treatment
/// remains active or to <c>DC</c> otherwise, and any other <c>OrderControl</c> is ignored.
/// In every case the change is followed by a complexity recalculation via <c>CalculateComplexity</c>.
/// </summary>
/// <param name="treatment">The treatment whose ECMO state is being applied.</param>
/// <returns>A task that represents the asynchronous recalculation operation.</returns>
private async Task CalculateComplexityOnEcmo(PatientTreatment treatment)
{
var ecmoObs = new PatientObservation
@@ -896,12 +1090,18 @@ public class CalculatedObservations : ICalculatedObservations
await CalculateComplexity(ecmoObs);
}
/**
* Calculamos la complejidad basándonos en el valor de PESO/VÍAS/ASSIST.RESP/MONIT./CIRUGÍA/MEDICACIÓN
* 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
* La complejidad va de 0 a 5. Guardamos la observación que nos llega y recalculamos complejidad.
*/
/// <summary>
/// Recalculates the patient's overall <c>Complexity</c> observation from scratch using the latest
/// values for the configured complexity-contributing observations (weight, medication, surgery,
/// intravenous lines, monitor, and respiratory), the active ECMO treatment state, and the
/// optionally supplied <paramref name="obs"/> which is preferred over the database value when
/// it is newer and not expired. The result is persisted only when its integer value differs
/// from the last stored complexity observation, with time inconsistencies resolved via
/// <c>FixTimeInconsistencyWithLast</c>. Returns the original observation unchanged.
/// </summary>
/// <param name="obs">The base patient observation that triggered the recalculation.</param>
/// <param name="ignoreObs">When <see langword="true"/>, the supplied <paramref name="obs"/> is not added to the calculation inputs (useful for synthetic observations like ECMO).</param>
/// <returns>A task that resolves to the same base patient observation that was passed in.</returns>
public async Task<BasePatientObservation> CalculateComplexity(BasePatientObservation obs, bool ignoreObs = false)
{
try
@@ -909,7 +1109,11 @@ public class CalculatedObservations : ICalculatedObservations
var logUid = Guid.NewGuid();
var complexity = new PatientObservation
{
PatientId = obs.PatientId, Time = DateTime.UtcNow, CodingSystem = "ADAS", Name = "Complexity", Value = 0
PatientId = obs.PatientId,
Time = DateTime.UtcNow,
CodingSystem = "ADAS",
Name = "Complexity",
Value = 0
};
var complexityValue = 0;
var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(obs.PatientId);
@@ -1059,15 +1263,21 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <summary>
/// Calculate intravenous line when new intravenous observation enters. Get all intravenous obs and filtering actives.
/// Si existe ya un catéter del mismo tipo, apuntando a la misma localización con una fecha que ya existe y no la ha
/// sido retirada y vuelve a venir
/// una inserción, no hacemos caso porque es una actualización.
/// Recomputes the <c>IntravenousLines</c> complexity score by reconciling the incoming
/// <c>IntravenousLinesObs</c> against the patient's active intravenous lines. Detects and
/// corrects common ICCA mis-clicks (inserts incorrectly carrying a remove time), updates an
/// existing line in place when only the duration changed, and otherwise adds the new line to
/// the active set. The aggregated score is capped at 10 and persisted with bounds [0, 10].
/// <c>InvalidCastException</c>s are logged and swallowed.
/// </summary>
/// <param name="obs"></param>
/// <returns></returns>
/// <param name="obs">The base patient observation that triggered the calculation (expected to be a <see cref="PatientObservation"/> whose <c>Value</c> is a <see cref="PatientIntravenousLinesValue"/>).</param>
/// <returns>
/// A task that resolves to the original base patient observation when processing succeeds or
/// fails gracefully with logging; resolves to <see langword="null"/> when the function returns
/// early because the incoming observation is a duplicate insert (in which case the caller should
/// not overwrite the stored observation).
/// </returns>
public async Task<BasePatientObservation?> CalculateIntravenousLineObservation(BasePatientObservation obs)
{
try
@@ -1194,7 +1404,15 @@ public class CalculatedObservations : ICalculatedObservations
return obs;
}
/// <summary>
/// Derives the weight-related complexity contribution from a weight observation: values expressed
/// in kilograms are first converted to grams via <c>ParseWeight</c>, and the final integer complexity
/// contribution is mapped from the gram value using the standard UCIN brackets
/// (&lt; 750 g → 7, 750999 → 5, 10001249 → 2, 12501999 → 1, ≥ 2000 → 0). Returns 0 when the
/// observation value cannot be parsed as a number.
/// </summary>
/// <param name="obs">The base patient observation whose value represents the patient's weight.</param>
/// <returns>A task that resolves to the integer complexity contribution (07).</returns>
private async Task<int> CalculateWeight(BasePatientObservation obs)
{
var complexityValueOfWeight = 0;
@@ -1219,7 +1437,14 @@ public class CalculatedObservations : ICalculatedObservations
return complexityValueOfWeight;
}
/// <summary>
/// Converts a weight observation expressed in kilograms to grams in-place (updates <c>Units</c> to
/// <c>"gr"</c> and multiplies <c>Value</c> by 1000). Returns the observation unchanged when it is
/// not a <see cref="PatientObservation"/> or when its value cannot be parsed; the error is also
/// logged.
/// </summary>
/// <param name="obs">The base patient observation expected to be a <see cref="PatientObservation"/> with a numeric value.</param>
/// <returns>A task that resolves to the (possibly converted) base patient observation.</returns>
public Task<BasePatientObservation> ParseWeight(BasePatientObservation obs)
{
if (obs is not PatientObservation pobs || !double.TryParse(pobs.Value.ToString(), out var dValue))
@@ -1233,9 +1458,19 @@ public class CalculatedObservations : ICalculatedObservations
return Task.FromResult(obs);
}
//Monitor observation can be a observation or treatment
//TODO como saber si tiene fecha de fin o frecuencia
/// <summary>
/// Recalculates the <c>Monitor</c> observation by adding 1 point for each active
/// transcutaneous / regional-brain-saturation / EEG monitoring signal (codes from the configured
/// catalogues) found in the most recent observations and active treatments. Persists the resulting
/// observation with bounds [0, 3]. Returns early with a warning when no recent monitor observations
/// are found.
/// </summary>
/// <param name="monitorObservation">
/// The trigger for the calculation. Accepts either a <see cref="PatientObservation"/>
/// (preferred when available) or a <see cref="PatientTreatment"/>. The patient identifier is
/// derived from whichever type is supplied.
/// </param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
public async Task CalculateMonitor(object monitorObservation)
{
var monitorValue = 0;
@@ -1302,7 +1537,14 @@ public class CalculatedObservations : ICalculatedObservations
await _observationService.Value.InsertObservation(monitorObs, mapObs: false);
}
/// <summary>
/// Recalculates the <c>Surgery</c> complexity score by reconciling the supplied treatment's
/// <c>OrderControl</c> (<c>NW</c> adds, <c>DC</c> removes by placer-order identifier) against the
/// patient's other active surgery treatments, then persisting a <c>Surgery</c> observation
/// with a score of 5 when any active surgery treatment remains, or 0 otherwise.
/// </summary>
/// <param name="treatment">The treatment that triggered the recalculation.</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
private async Task CalculateSurgery(PatientTreatment treatment)
{
var surgeryScore = 0;
@@ -1315,7 +1557,7 @@ public class CalculatedObservations : ICalculatedObservations
switch (treatment.OrderControl)
{
//Refactor solo con los tratamientos activos de cirugía, un DC cancela a su entityIdentifier correspondiente.
case OrderControlType.Nw:
activeSurgeryTreatments.Add(treatment);
break;
@@ -1342,7 +1584,18 @@ public class CalculatedObservations : ICalculatedObservations
await _observationService.Value.InsertObservation(surgeryObs, mapObs: false);
}
/// <summary>
/// Computes the patient's active bolus treatments by:
/// (1) optionally including the newly arrived treatment, (2) grouping all bolus-eligible treatments
/// by placer-order <c>NamespaceId</c>, (3) discarding treatments whose administration time was
/// later overridden by an end-time, (4) keeping only the latest message-time entry per distinct
/// administration time, and (5) filtering to the configured medication-bolus catalogue, an
/// administration time within the last 12 hours, and a non-empty RXA status contained in
/// <c>_rxaStatus</c>.
/// </summary>
/// <param name="patientId">The unique identifier of the patient.</param>
/// <param name="newTreatment">The optional new treatment to be included in the active-bolus set.</param>
/// <returns>A task that resolves to the list of treatments currently counted as active boluses.</returns>
private async Task<List<PatientTreatment>> GetActiveBolus(ObjectId patientId, PatientTreatment? newTreatment)
{
var treatmentsProcessed = new List<PatientTreatment>();
@@ -1401,7 +1654,12 @@ public class CalculatedObservations : ICalculatedObservations
p.RequestedGiveCodesStatus.Any(rxa => rxa.Status != null && _rxaStatus.Contains(rxa.Status)));
}
/// <summary>
/// Persists the <c>OpiateBoluses</c> observation whose value is the count of currently active bolus
/// treatments for the patient (as computed by <c>GetActiveBolus</c> including the new treatment).
/// </summary>
/// <param name="treatment">The treatment that triggered the recalculation and should be included in the active-bolus set.</param>
/// <returns>A task that represents the asynchronous insert operation.</returns>
private async Task CalculateBolus(PatientTreatment treatment)
{
var activeBolus = await GetActiveBolus(treatment.PatientId, treatment);
@@ -1417,9 +1675,17 @@ public class CalculatedObservations : ICalculatedObservations
await _observationService.Value.InsertObservation(bolusObs);
}
/*
* En rojo si TAm < Edad Gestacional en semanas en la primera semana y luego edad corregida
*/
/// <summary>
/// Applies the red-alert rule for mean arterial pressure (TAm) in neonates: when TAm is below
/// the patient's gestational age (in weeks) for the first week, or below the fixed gestational
/// age thereafter, the <c>TAm</c> observation's <c>Status</c> is set to <c>Alert</c>; otherwise
/// it is set to <c>Ok</c>. If the supplied observation is a gestational-age value (not TAm),
/// a fresh TAm observation is re-inserted with the recalculated status. The threshold is read
/// from the latest <c>Age_Gestational</c> observation when its value is below 2 weeks,
/// otherwise from the latest <c>Age_Gestational_Fixed</c>.
/// </summary>
/// <param name="obs">The base patient observation that triggered the alert evaluation (<c>TAm</c>, <c>Age_Gestational</c>, or <c>Age_Gestational_Fixed</c>).</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
public async Task CalculateTAmAlert(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
@@ -1473,7 +1739,16 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/*Entra temp axilar o incubadora. Restamos la axilar a la incubadora. Solo se calcula si están los dos valores. */
/// <summary>
/// Computes the temperature gradient between the incubator and the patient (incubator patient)
/// when both temperatures have been observed within 10 minutes of each other. Persists the
/// <c>Temp_Gradient</c> observation with the resulting value, after shifting its timestamp via
/// <c>CheckObsWithSameTimeExistsAndIncrementTime</c> to avoid colliding with a previous
/// gradient observation. Returns early when the companion temperature is missing or the two
/// readings fall outside the 10-minute window.
/// </summary>
/// <param name="obs">The base patient observation that triggered the calculation (<c>Temp_Patient</c> or <c>Temp_Incubator</c>).</param>
/// <returns>A task that represents the asynchronous calculation operation.</returns>
public async Task CalculateTempGradient(BasePatientObservation obs)
{
List<PatientObservation> tempRetrievedesFromBd;
@@ -1525,6 +1800,10 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <summary>
/// Enumeration of the intravenous-line types recognised by the UCIN complexity model,
/// ordered from most invasive (Artery) to least invasive (Peripheral).
/// </summary>
private enum IntraVenousLineTypes
{
Artery,
@@ -1534,6 +1813,10 @@ public class CalculatedObservations : ICalculatedObservations
Peripheral
}
/// <summary>
/// Enumeration of the respiratory-assistance types recognised by the UCIN complexity model,
/// ordered from highest score (Ino) to lowest (None).
/// </summary>
// ReSharper disable once UnusedMember.Local
private enum RespiratoryTypes
{
@@ -11,6 +11,9 @@ using MongoDB.Bson;
namespace adas_core.Application.Customizations.HGM;
//Custom for Hospital Gregorio Marañón
/// <summary>
/// Represents a collection of calculated observations, providing a concrete implementation of the <see cref="ICalculatedObservations"/> contract.
/// </summary>
public class CalculatedObservations : ICalculatedObservations
{
private readonly List<string> _highFrequencyVentilation = [];
@@ -35,6 +38,13 @@ public class CalculatedObservations : ICalculatedObservations
nonInvasiveVentilation?.ForEach(x => _nonInvasiveVentilation.Add(x.Trim()));
}
/// <summary>
/// Maps a patient observation by applying domain-specific transformations based on the observation name.
/// Handles the "Resp_Mode" case by calculating the ventilation mode, while observations with a null or empty name are returned unchanged.
/// </summary>
/// <param name="obs">The patient observation to be mapped.</param>
/// <param name="onlyByName">Flag indicating whether the mapping should be restricted to name-based criteria.</param>
/// <returns>The mapped patient observation, or the original observation if no applicable mapping is found.</returns>
public async Task<T?> Map<T>(T obs, bool onlyByName = false) where T : BasePatientObservation
{
if (string.IsNullOrEmpty(obs.Name)) return obs;
@@ -50,57 +60,120 @@ public class CalculatedObservations : ICalculatedObservations
}
/// <summary>
/// Asynchronously maps the specified <paramref name="treatment"/> to a <see cref="PatientTreatment"/> result.
/// </summary>
/// <param name="treatment">The <see cref="PatientTreatment"/> instance to be mapped.</param>
/// <returns>A task that represents the asynchronous mapping operation, containing the resulting <see cref="PatientTreatment"/>.</returns>
/// <exception cref="System.NotImplementedException">Thrown to indicate that the method has not yet been implemented.</exception>
public Task<PatientTreatment> Map(PatientTreatment treatment)
{
throw new NotImplementedException();
}
/// <summary>
/// Maps the provided patient diagnosis to the target representation asynchronously.
/// </summary>
/// <param name="diagnosis">The patient diagnosis to be mapped.</param>
/// <returns>A task that represents the asynchronous mapping operation, containing the mapped <see cref="PatientDiagnosis"/>.</returns>
/// <exception cref="NotImplementedException">Thrown when the method is invoked, as the mapping logic has not been implemented yet.</exception>
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
throw new NotImplementedException();
}
/// <summary>
/// Maps the provided <see cref="PumpObservation"/> by returning it unchanged, wrapped in a completed task.
/// </summary>
/// <param name="pumpObservation">The pump observation to map.</param>
/// <returns>A completed task containing the provided <see cref="PumpObservation"/>.</returns>
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
{
return await Task.FromResult(pumpObservation);
}
/// <summary>
/// Asynchronously calculates the active bolus for the specified patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient for whom the active bolus is calculated.</param>
/// <exception cref="NotImplementedException">The method is not yet implemented.</exception>
public Task CalculateActiveBolus(ObjectId patientId)
{
throw new NotImplementedException();
}
/// <summary>
/// Calculates the medicine observation for a patient based on the provided active medicines.
/// </summary>
/// <param name="activeMedicines">The list of active medicines currently associated with the patient.</param>
/// <param name="patientId">The unique identifier of the patient for whom the observation is being calculated.</param>
/// <exception cref="NotImplementedException">The method is not yet implemented.</exception>
public Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
throw new NotImplementedException();
}
/// <summary>
/// Resolves time inconsistencies between the specified <paramref name="newObservation"/> and the last recorded patient observation, returning a corrected observation when applicable.
/// </summary>
/// <param name="newObservation">The new patient observation to reconcile against the last recorded observation.</param>
/// <returns>A task that returns the corrected <see cref="PatientObservation"/>, or <c>null</c> when no last observation is available to compare against.</returns>
/// <exception cref="System.NotImplementedException">The method is not yet implemented.</exception>
public Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
throw new NotImplementedException();
}
/// <summary>
/// Retrieves the active treatments currently associated with the specified patient.
/// </summary>
/// <param name="id">The identifier of the patient whose active treatments are being requested.</param>
/// <returns>A task that yields a collection of active <see cref="PatientTreatment"/> entries for the patient.</returns>
/// <exception cref="NotImplementedException">Thrown because the method has not been implemented yet.</exception>
public Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
throw new NotImplementedException();
}
/// <summary>
/// Pre-maps the provided list of patient observations before further processing, returning the list as-is in a completed task.
/// </summary>
/// <param name="listToInsert">The list of patient observations to be pre-mapped.</param>
/// <returns>A completed task containing the provided list of patient observations.</returns>
public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
{
return Task.FromResult(listToInsert);
}
/// <summary>
/// Maps a source alarm to a patient observation, returning the provided observation as the result.
/// </summary>
/// <param name="obs">The patient observation to return as the mapped result.</param>
/// <param name="alarmToInsert">The patient observation alarm to be considered during mapping.</param>
/// <returns>A <see cref="Task{PatientObservation}"/> containing the provided <paramref name="obs"/>.</returns>
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
return Task.FromResult(obs);
}
/// <summary>
/// Sends an alarm for the specified patient observation.
/// </summary>
/// <param name="obs">The patient observation that triggers the alarm.</param>
/// <param name="name">The name associated with the alarm.</param>
/// <param name="code">The optional alarm code identifying the alarm type.</param>
/// <returns>A task that represents the asynchronous alarm sending operation.</returns>
/// <exception cref="NotImplementedException">Thrown when the method is invoked, as it is not yet implemented.</exception>
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
throw new NotImplementedException();
}
/// <summary>
/// Calculates and persists the ventilation mode (<c>Resp_Type</c>) for a patient observation by mapping the observed value against known high-frequency, invasive, and non-invasive ventilation vocabularies, defaulting to <c>None</c> when the value is empty or unrecognized.
/// </summary>
/// <param name="obs">The base patient observation whose value is used to derive the respiration type; it is cast to <see cref="PatientObservation"/> to access the value.</param>
private async Task CalculateVentilationMode(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
@@ -11,6 +11,12 @@ using Serilog;
namespace adas_core.Application.Customizations.HPAZ;
/// <summary>
/// Represents a service that provides calculated observations, receiving its dependencies through the supplied <see cref="IServiceProvider"/>.
/// </summary>
/// <remarks>
/// This class implements the <see cref="ICalculatedObservations"/> contract, exposing the behavior defined by that interface while relying on constructor-injected services for its operations.
/// </remarks>
public class CalculatedObservations(IServiceProvider serviceProvider) : ICalculatedObservations
{
private readonly Lazy<IAlarmService> _alarmService = serviceProvider.GetRequiredService<Lazy<IAlarmService>>();
@@ -32,16 +38,31 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
private readonly List<string> _pressBloodArteryMean = ["TAm"];
/// <summary>
/// Asynchronously calculates the active bolus for the specified patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose active bolus is to be calculated.</param>
public Task CalculateActiveBolus(ObjectId patientId)
{
return Task.CompletedTask;
}
/// <summary>
/// Calculates medicine observations for the specified patient based on their active medicines.
/// </summary>
/// <param name="activeMedicines">The list of medicines currently active for the patient.</param>
/// <param name="patientId">The unique identifier of the patient whose medicine observations are being calculated.</param>
public Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
return Task.CompletedTask;
}
/// <summary>
/// Retrieves the active treatments associated with the specified patient identifier.
/// Returns an empty collection when no active treatments are found.
/// </summary>
/// <param name="id">The unique identifier of the patient whose active treatments are being retrieved.</param>
/// <returns>A task that represents the asynchronous operation, containing a collection of active <see cref="PatientTreatment"/> records for the patient, or an empty collection if none exist.</returns>
public Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
return Task.FromResult<IEnumerable<PatientTreatment?>>(new List<PatientTreatment?>());
@@ -87,7 +108,11 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
return obs as T;
}
/// <summary>
/// Maps the specified pump observation to a new <see cref="PumpObservation"/> instance asynchronously.
/// </summary>
/// <param name="pumpObservation">The source <see cref="PumpObservation"/> to map.</param>
/// <returns>A task that represents the asynchronous mapping operation. The task result contains the mapped <see cref="PumpObservation"/>.</returns>
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
{
try
@@ -181,17 +206,34 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
}
/// <summary>
/// Maps a <see cref="PatientTreatment"/> instance, performing the required transformation logic.
/// </summary>
/// <param name="treatment">The <see cref="PatientTreatment"/> to be mapped.</param>
/// <returns>A task representing the asynchronous operation, containing the mapped <see cref="PatientTreatment"/>.</returns>
/// <exception cref="NotImplementedException">Thrown in all cases because the method has not yet been implemented.</exception>
public Task<PatientTreatment> Map(PatientTreatment treatment)
{
throw new NotImplementedException();
}
/// <summary>
/// Maps a <see cref="PatientDiagnosis"/> instance. This method is a placeholder and has not been implemented yet.
/// </summary>
/// <param name="diagnosis">The <see cref="PatientDiagnosis"/> to be mapped.</param>
/// <returns>A task that represents the asynchronous operation, containing the mapped <see cref="PatientDiagnosis"/>.</returns>
/// <exception cref="NotImplementedException">Thrown to indicate that the mapping logic has not been implemented.</exception>
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
throw new NotImplementedException();
}
/// <summary>
/// Ensures chronological order of patient observations by adjusting a new observation's time to one second after the most recent observation with the same name, when its time is earlier than or equal to the previous one (compared at second precision). Returns the observation unchanged and logs an error if the observation name is null or empty.
/// </summary>
/// <param name="newObservation">The new patient observation to validate and potentially adjust for time consistency.</param>
/// <returns>The patient observation with its time corrected if a time inconsistency was detected, otherwise the original observation.</returns>
public async Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
if (string.IsNullOrEmpty(newObservation.Name))
@@ -219,6 +261,12 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
return newObservation;
}
/// <summary>
/// Pre-maps a list of patient observations by creating deep copies, applying configuration-based mapping,
/// and triggering blue code calculation. Returns the original list of patient observations unchanged.
/// </summary>
/// <param name="listToMap">The list of patient observations to process for mapping and blue code calculation.</param>
/// <returns>The original list of patient observations passed to the method.</returns>
public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToMap)
{
var mappedObsList = listToMap
@@ -236,6 +284,12 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
return Task.FromResult(listToMap);
}
/// <summary>
/// Maps fields from a <see cref="PatientObservationAlarm"/> onto an existing <see cref="PatientObservation"/>, copying the alarm's EventId to Code and Event to Name when those values are provided, and always copying the alarm's Value.
/// </summary>
/// <param name="obs">The target <see cref="PatientObservation"/> instance that will be updated with values from the alarm.</param>
/// <param name="alarmToInsert">The source <see cref="PatientObservationAlarm"/> whose values are applied to <paramref name="obs"/>.</param>
/// <returns>A completed <see cref="Task{PatientObservation}"/> containing the updated <paramref name="obs"/>.</returns>
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
if (!string.IsNullOrEmpty(alarmToInsert.EventId))
@@ -249,12 +303,26 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
return Task.FromResult(obs);
}
/// <summary>
/// Sends an alarm associated with the specified patient observation, using the provided alarm name and optional code.
/// </summary>
/// <param name="obs">The patient observation that triggers or relates to the alarm.</param>
/// <param name="name">The name of the alarm to be sent.</param>
/// <param name="code">The optional alarm code identifying the type of alarm.</param>
/// <exception cref="System.NotImplementedException">Thrown because the method is not yet implemented.</exception>
Task ICalculatedObservations.SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
throw new NotImplementedException();
}
/// <summary>
/// Asynchronously sends an alarm for the specified patient observation through the alarm service, always using a None severity.
/// </summary>
/// <param name="obs">The patient observation associated with the alarm.</param>
/// <param name="name">The name of the alarm.</param>
/// <param name="code">The optional alarm code identifier.</param>
/// <param name="type">The type of alarm to send.</param>
private async Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code, AlarmEnum.Type type)
{
await _alarmService.Value.SendAlarm(obs, name, code, AlarmEnum.Severity.None, type);
@@ -281,6 +349,11 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
}
/// <summary>
/// Retrieves the alarm configuration for the given patient observation by looking up a matching configuration entry based on the observation name and patient ID, and applies it to the observation. If no matching configuration is found, the alarm is set to <c>null</c>.
/// </summary>
/// <param name="pobs">The patient observation whose alarm configuration should be resolved; its <c>Name</c> and <c>PatientId</c> are used to locate the configuration.</param>
/// <returns>The same <see cref="PatientObservation"/> instance with its <c>Alarm</c> property populated from the matching configuration, or <c>null</c> when no configuration is found.</returns>
private async Task<PatientObservation> CheckAlarmConfig(PatientObservation pobs)
{
var configObs = await _configObservationService.Get(new PatientObservation
@@ -294,6 +367,12 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
return pobs;
}
/// <summary>
/// Creates a new <see cref="PatientObservation"/> representing an alarm, using the ADAS_ALARM coding system and prefixing the name with "Alarm_". The resulting observation inherits the value, patient identifier, timestamp, and alarm status from the provided source observation.
/// </summary>
/// <param name="name">The alarm code used to identify the type of alarm; it is set as the <c>Code</c> and used to compose the <c>Name</c>.</param>
/// <param name="pobs">The source patient observation whose value, patient identifier, time, and alarm flag are copied into the new alarm observation.</param>
/// <returns>A new <see cref="PatientObservation"/> configured as an ADAS alarm observation based on the provided source.</returns>
private static PatientObservation CreateAlarmObservation(string name, PatientObservation pobs)
{
return new PatientObservation
@@ -378,6 +457,10 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
}
/// <summary>
/// Sends a Blue Code alarm for the given patient observation. If the observation is null, the method returns without taking any action; otherwise it creates the corresponding alarm observation, validates it against the alarm configuration, persists it, and dispatches the alarm as an automatically triggered Blue alarm.
/// </summary>
/// <param name="pobs">The patient observation that triggers the Blue Code alarm; when null, the method short-circuits without processing.</param>
private async Task SendBlueCode(PatientObservation? pobs)
{
if (pobs == null)
@@ -397,6 +480,12 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
//PRVC : donde aparezca cambiar y mostrar en su lugar: VCRP
//FLUJO ALTO: poner OAF en su lugar
/// <summary>
/// Calculates and normalizes the ventilation mode value for a patient observation.
/// Replaces occurrences of "PRVC" with "VCRP" and maps "FLUJ.ALTO" to "OAF" in the observation's value.
/// </summary>
/// <param name="obs">The base patient observation value whose ventilation mode will be calculated and normalized.</param>
/// <returns>The patient observation with the normalized ventilation mode value.</returns>
private BasePatientObservationValue CalculateVentilationMode(BasePatientObservationValue obs)
{
var value = obs is PatientObservationAlarm oAlarm ? oAlarm.Value
@@ -414,8 +503,13 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
}
//TODO duplicado con el calculated del 12o, sacar a un utils
/// <summary>
/// Checks whether a patient observation with the same time already exists and, if so, increments the observation time by one second to avoid a duplicate timestamp.
/// </summary>
/// <param name="obs">The patient observation to evaluate; non-<see cref="PatientObservation"/> instances are returned unchanged.</param>
/// <returns>The original observation, with its <c>Time</c> adjusted by one second when a matching observation is found.</returns>
private async Task<BasePatientObservationValue> CheckObsWithSameTimeExistsAndIncrementTime(
BasePatientObservationValue obs)
BasePatientObservationValue obs)
{
if (obs is not PatientObservation pobs) return obs;
@@ -428,6 +522,13 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
}
//S/F Calc ( craneal saturation / FiO2
/// <summary>
/// Calculates the SpO2/FiO2 (S/F) ratio for a patient by pairing a newly received oxygenation observation
/// (FiO2 or Sattc) with the most recent complementary value and inserting a derived "Sattc_FiO2" observation.
/// The derived value is only inserted when both observations exist, are not expired, parse as numbers, the saturation is at most 97, and FiO2 is non-zero; otherwise the calculation is logged and skipped, and any exception is caught and logged.
/// </summary>
/// <param name="obs">The newly received patient observation that triggered the calculation; its Name must be either "FiO2" or "Sattc".</param>
/// <param name="name">The name of the observation, used to determine whether <paramref name="obs"/> is the FiO2 or the Sattc value.</param>
private async Task CalculateSf(BasePatientObservationValue obs, string name)
{
try
@@ -558,6 +659,12 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
}
//P/F Calc ( PaO2_Tidal / FiO2
/// <summary>
/// Asynchronously calculates the PaO2/FiO2 (P/F) ratio for a patient and persists the result as a new <c>PaO2_FiO2</c> observation.
/// Supports being triggered by either an FiO2 or a PaO2_Tidal observation by pairing the provided observation with its counterpart retrieved from the most recent observations, and only persists the result when both values are present, non-expired, parseable as numeric values, and the FiO2 value is non-zero.
/// </summary>
/// <param name="obs">The current patient observation that initiated the calculation; provides the patient identifier and timestamp used for the resulting observation.</param>
/// <param name="name">The name of the observation in <paramref name="obs"/>, expected to be either <c>FiO2</c> or <c>PaO2_Tidal</c>, which determines how the counterpart value is resolved.</param>
private async Task CalculatePf(BasePatientObservationValue obs, string name)
{
try
@@ -614,15 +721,23 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
}
/// <summary>
/// Creates a <see cref="PatientObservation"/> representing a pump alarm, tagging it with the
/// ADAS_ALARM coding system and a code/name derived from the alarm <paramref name="name"/>.
/// The position label used as the observation value distinguishes main rack pumps from auxiliary rack pumps based on <see cref="PumpObservation.IsAux"/>, and falls back to the position when no drug name is available. The patient identifier is assigned when present, and the observation is then validated against the alarm configuration.
/// </summary>
/// <param name="name">The alarm identifier used to build the observation <c>Code</c> and <c>Name</c>.</param>
/// <param name="pumpObservation">The pump data source providing rack/auxiliary info, drug name, time, and optional patient id.</param>
/// <returns>A <see cref="Task{PatientObservation}"/> that yields the resulting observation, or <c>null</c> when the alarm configuration check rejects it.</returns>
private async Task<PatientObservation?> CreatePumpAlarmObservation(string name,
PumpObservation pumpObservation)
PumpObservation pumpObservation)
{
var positionPump = pumpObservation.IsAux != null && !pumpObservation.IsAux.Value
? $"Rack {pumpObservation.GatewayNumber} Bomba {pumpObservation.Number}"
: $"Rack Aux {pumpObservation.GatewayNumber} Bomba {pumpObservation.Number}";
PatientObservation patientObservation = new()
{
CodingSystem = "ADAS_ALARM",
@@ -634,7 +749,7 @@ public class CalculatedObservations(IServiceProvider serviceProvider) : ICalcula
if (pumpObservation.PatientId != null)
patientObservation.PatientId = pumpObservation.PatientId.Value;
return await CheckAlarmConfig(patientObservation);
}
@@ -11,6 +11,13 @@ 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>
public class CalculatedObservations : ICalculatedObservations
{
private readonly IOptions<ApiSettings> _apiSettings;
@@ -27,6 +34,14 @@ public class CalculatedObservations : ICalculatedObservations
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>
public CalculatedObservations(IServiceProvider serviceProvider)
{
_observationService = serviceProvider.GetRequiredService<Lazy<IObservationService>>(); //observationService;
@@ -52,6 +67,25 @@ public class CalculatedObservations : ICalculatedObservations
}
/// <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>
public async Task<T?> Map<T>(T obs, bool onlyByName) where T : BasePatientObservation
{
_logger.LogTrace("Mapping {name} observation {obs}", obs.Name, obs);
@@ -107,40 +141,89 @@ public class CalculatedObservations : ICalculatedObservations
}
/// <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>
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>
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>
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>
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>
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>
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>
public async Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
if (string.IsNullOrEmpty(newObservation.Name))
@@ -168,6 +251,17 @@ public class CalculatedObservations : ICalculatedObservations
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>
public async Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
{
var ventRate = listToInsert.FirstOrDefault(obs => obs.Name == "MDC_VENT_RESP_RATE");
@@ -210,20 +304,41 @@ public class CalculatedObservations : ICalculatedObservations
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>
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>
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
throw new NotImplementedException();
}
/// <summary>
/// IF news greater or equal than 5 and less than 7 is warning beacon and if greater or equal than 7 alert
/// 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"></param>
/// <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>
private async Task CalculateNews(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
@@ -258,6 +373,15 @@ public class CalculatedObservations : ICalculatedObservations
//if (obsAlarm == null) return;
}
/// <summary>
/// Builds a synthetic <c>Alarm_&lt;Name&gt;</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>
private async Task SendAlarm(BasePatientObservation obs, AlarmEnum.Name name)
{
var pobs = (PatientObservation)obs;
@@ -273,10 +397,10 @@ public class CalculatedObservations : ICalculatedObservations
{
//ConfigObservations
var configObs = await _configObservationService.Get(new PatientObservation
{
Name = nObs.Name,
PatientId = obs.PatientId
}
{
Name = nObs.Name,
PatientId = obs.PatientId
}
);
if (configObs == null)
{
@@ -313,6 +437,13 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <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>
private void SendBeaconCode(AlarmEnum.BeaconColor color, Patient patient)
{
if (!patient.PointOfCareId.HasValue)
@@ -339,7 +470,14 @@ public class CalculatedObservations : ICalculatedObservations
}
}
//(SpO2/FiO2)/FR IROX formula. Only calculate when all observations are in last 10 minutes.
/// <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>
private async Task CalculateIrox(BasePatientObservation obs)
{
try
@@ -412,13 +550,16 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/*
*
RespRate solo se tiene que poner cuando no hay ningún ventRate en los últimos min y veinte segundos y el último no es un 0.
*/
//static readonly SemaphoreSlim semaphoreCalculateRespRate = new(1, 1);
/// <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>
private async Task CalculateRespRate(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
@@ -479,6 +620,13 @@ public class CalculatedObservations : ICalculatedObservations
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>
private async Task<BasePatientObservation> CalculateHydricBalance(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
@@ -496,6 +644,12 @@ public class CalculatedObservations : ICalculatedObservations
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>
private async Task<BasePatientObservation> CalculateHourBalance(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
@@ -513,6 +667,16 @@ public class CalculatedObservations : ICalculatedObservations
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>
private async Task CalculateHydricBalanceCalculated(BasePatientObservation obs)
{
//Hydric_Balance_Calculated
@@ -580,6 +744,15 @@ public class CalculatedObservations : ICalculatedObservations
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>
private async Task CalculateVentilationMode(BasePatientObservation obs)
{
_logger.LogDebug("CalculateVentilationMode {obs}", obs);
@@ -620,6 +793,13 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <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>
private async Task CalculateWeight_DiffObservation(BasePatientObservation obs)
{
var toSearchList = new List<string>();
@@ -651,6 +831,14 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <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>
private async Task CalculateDiureis_WeightObservation(BasePatientObservation obs)
{
var toSearchList = new List<string>();
@@ -695,6 +883,14 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <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>
private async Task CalculateAllergiesObservation(BasePatientObservation obs)
{
try
@@ -764,6 +960,13 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <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>
private async Task CalculateDrainagesObservation(BasePatientObservation obs)
{
var pobs = (PatientObservation)obs;
@@ -803,6 +1006,13 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <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>
private async Task CalculateDelta_PressureObservation(BasePatientObservation obs)
{
var toSearchList = new List<string>();
@@ -854,6 +1064,13 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <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>
private async Task CalculateDaily_BalanceObservation(BasePatientObservation obs)
{
if (obs.Time.ToLocalTime().Hour == 8)
@@ -872,6 +1089,15 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <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>
private static bool CheckExpired(PatientObservation obs)
{
if (obs.Expires == null) return false;
@@ -11,6 +11,10 @@ using MongoDB.Driver;
namespace adas_core.Application.Customizations.HUVH.UCIA;
/// <summary>
/// Represents a concrete implementation of the <see cref="ICalculatedObservations"/> interface,
/// providing functionality to manage a collection of calculated observations.
/// </summary>
public class CalculatedObservations : ICalculatedObservations
{
private const string CodingSystem = "ADAS";
@@ -53,12 +57,23 @@ public class CalculatedObservations : ICalculatedObservations
}
/// <summary>
/// Asynchronously calculates the active bolus for the specified patient.
/// </summary>
/// <param name="patientId">The identifier of the patient whose active bolus should be calculated.</param>
/// <exception cref="NotImplementedException">Thrown when the method is invoked, as the calculation logic has not yet been implemented.</exception>
public Task CalculateActiveBolus(ObjectId patientId)
{
throw new NotImplementedException();
}
/// <summary>
/// Processes a patient observation by dispatching to the appropriate domain-specific calculation based on the observation name, handling over-sedation, ventilation mode, ECMO location, over-analgesia, driving pressure, ROX index, diuresis, and rehabilitation alarm assessments.
/// </summary>
/// <param name="obs">The patient observation to map; may be replaced with the result of the matched asynchronous calculation when applicable.</param>
/// <param name="onlyByName">Indicates whether the mapping should be performed using only the observation's name.</param>
/// <returns>The processed patient observation, potentially updated by the applicable calculation.</returns>
public async Task<T?> Map<T>(T obs, bool onlyByName = false) where T : BasePatientObservation
{
if (obs.Name is "PSI" or "RASS" or "TS") _ = CalculateOverSedation(obs);
@@ -81,6 +96,11 @@ public class CalculatedObservations : ICalculatedObservations
return obs;
}
/// <summary>
/// Maps a <see cref="PatientTreatment"/> by determining the appropriate order control status, handling cases for missing placer orders, previous expired treatments, and treatments with a defined end time.
/// </summary>
/// <param name="treatment">The patient treatment to map and update.</param>
/// <returns>The patient treatment with its order control set according to the evaluated business rules.</returns>
public async Task<PatientTreatment> Map(PatientTreatment treatment)
{
var order = treatment.PlacerOrder?.EntityIdentifier; //aquí almacenamos el número de orden
@@ -100,12 +120,22 @@ public class CalculatedObservations : ICalculatedObservations
return treatment;
}
/// <summary>
/// Maps a <see cref="PumpObservation"/> instance to its output representation as a pass-through operation.
/// </summary>
/// <param name="pumpObservation">The pump observation to map.</param>
/// <returns>A completed task containing the provided <see cref="PumpObservation"/>.</returns>
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
{
return await Task.FromResult(pumpObservation);
}
/// <summary>
/// Calculates medicine observations for a patient by validating the categories of their active medications. Typically invoked by the scheduler service to review current medication statuses.
/// </summary>
/// <param name="activeMedicines">The list of currently active medicines assigned to the patient.</param>
/// <param name="patientId">The unique identifier of the patient whose medication categories will be checked.</param>
public Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
//Esto viene del schedulerService para chequear los medicamentos activos
@@ -114,6 +144,11 @@ public class CalculatedObservations : ICalculatedObservations
return Task.CompletedTask;
}
/// <summary>
/// Retrieves all currently active treatments associated with the specified patient.
/// </summary>
/// <param name="id">The unique identifier of the patient whose active treatments should be retrieved.</param>
/// <returns>A collection of <see cref="PatientTreatment"/> objects representing the patient's active treatments.</returns>
public async Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(id);
@@ -121,13 +156,24 @@ public class CalculatedObservations : ICalculatedObservations
}
/// <summary>
/// Asynchronously maps a <see cref="PatientDiagnosis"/> instance to its corresponding data representation or transfer model.
/// </summary>
/// <param name="diagnosis">The <see cref="PatientDiagnosis"/> to be mapped.</param>
/// <returns>A task that represents the asynchronous mapping operation, containing the resulting <see cref="PatientDiagnosis"/>.</returns>
/// <exception cref="NotImplementedException">Thrown when the method is invoked, as the mapping logic has not yet been implemented.</exception>
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
throw new NotImplementedException();
}
/// <summary>
/// Ensures the timestamp of the new observation does not collide with the most recent observation for the same patient and observation name. If the new observation's time is equal to or earlier than the last observation's time (compared at second precision), the new observation's time is incremented by one second. If the observation name is null or empty, the new observation is returned unchanged after logging an error.
/// </summary>
/// <param name="newObservation">The new patient observation whose timestamp should be adjusted to avoid time inconsistencies with prior observations.</param>
/// <returns>The patient observation, with its <c>Time</c> adjusted when a time collision is detected, or the original observation when its name is null or empty.</returns>
public async Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
if (string.IsNullOrEmpty(newObservation.Name))
@@ -155,21 +201,46 @@ public class CalculatedObservations : ICalculatedObservations
return newObservation;
}
/// <summary>
/// Pre-maps the provided list of patient observations, returning it as a completed task for asynchronous processing pipelines.
/// </summary>
/// <param name="listToInsert">The list of patient observations to pre-map before insertion.</param>
/// <returns>A completed task containing the provided list of patient observations.</returns>
public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
{
return Task.FromResult(listToInsert);
}
/// <summary>
/// Maps a source alarm to a patient observation. Returns the provided observation without applying changes from the alarm.
/// </summary>
/// <param name="obs">The patient observation to be returned as the mapping result.</param>
/// <param name="alarmToInsert">The patient observation alarm intended to be associated with the observation.</param>
/// <returns>A task containing the patient observation.</returns>
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
return Task.FromResult(obs);
}
/// <summary>
/// Asynchronously sends an alarm notification associated with a patient observation, identified by a name and an optional alarm code.
/// </summary>
/// <param name="obs">The patient observation that triggers the alarm.</param>
/// <param name="name">The name associated with the alarm.</param>
/// <param name="code">The optional alarm code categorizing the alarm, or null if no code is specified.</param>
/// <returns>A task that represents the asynchronous alarm sending operation.</returns>
/// <exception cref="NotImplementedException">Thrown to indicate that the method has not yet been implemented.</exception>
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
throw new NotImplementedException();
}
/// <summary>
/// Evaluates a rehabilitation observation and marks it as Alert when the degree value is between 0 and 2 (inclusive) for more than seven consecutive days.
/// The observation is returned unchanged when it is not a patient observation, its value cannot be parsed as an integer, or its value is greater than 2.
/// </summary>
/// <param name="obs">The base patient observation to evaluate for the rehabilitation alarm condition.</param>
/// <returns>The observation with its status set to Alert when the six previous rehabilitation observations also fall within the 0-2 range; otherwise the observation is returned as-is.</returns>
private async Task<BasePatientObservation> CalculateRehabilitationAlarm(BasePatientObservation obs)
{
//En rojo si el grado es de 0 a 2 incluidos, por más de 7 días
@@ -186,6 +257,14 @@ public class CalculatedObservations : ICalculatedObservations
return obs;
}
/// <summary>
/// Calculates the diuresis index in ml/kg/h for a patient by summing the diuresis values from the last six hours
/// and dividing by the patient's weight and by six. Handles trigger observations of type "Diuresis" (which also
/// pulls the latest recorded weight) and "Weight" (which supplies the weight directly), and aborts with a warning
/// when fewer than three diuresis values are available or the weight is not greater than zero.
/// </summary>
/// <param name="obs">The incoming base patient observation; only <see cref="PatientObservation"/> instances whose
/// <c>Name</c> is "Diuresis" or "Weight" are processed, otherwise the method returns without changes.</param>
private async Task CalculateDiuresis(BasePatientObservation obs)
{
// Calculado como el sumatorio de la Diuresis de las últimas 6h
@@ -262,6 +341,10 @@ public class CalculatedObservations : ICalculatedObservations
}
/// <summary>
/// Calculates the ROX Index by dividing the SpO2/FiO2 ratio by the respiratory rate (FR) and persists the result as a new <c>Rox_Index</c> observation. The method supports both directions of the calculation: when the incoming observation is the SpO2/FiO2 ratio, it retrieves the latest FR, and vice versa. It silently returns if the observation is not a <c>PatientObservation</c>, if the observation name is not recognized, if the required paired observation cannot be found, if its value cannot be parsed, or if the FR value is zero (to avoid division by zero).
/// </summary>
/// <param name="obs">The base patient observation that triggers the ROX index calculation. Only observations named <c>SpO2_FiO2_Ratio</c> or <c>FR</c> are processed; any other type or name is ignored.</param>
private async Task CalculateRoxIndex(BasePatientObservation obs)
{
// Cálculo dividiendo el ratio SpO2/FiO2 entre la FR
@@ -310,6 +393,10 @@ public class CalculatedObservations : ICalculatedObservations
await _observationService.Value.InsertObservation(roxIndexObs, mapObs: false);
}
/// <summary>
/// Calculates the driving pressure for a patient as the difference between Pmeset and PEEP when a new Pmeset or PEEP observation is provided. Looks up the complementary observation to obtain the missing value, then creates and persists a "Driving_Pressure" observation. Returns early if the input is not a PatientObservation, the observation name is neither Pmeset nor PEEP, the complementary observation cannot be found, or its value cannot be parsed as an integer.
/// </summary>
/// <param name="obs">The base patient observation that triggered the calculation; expected to represent a Pmeset or PEEP measurement.</param>
private async Task CalculateDrivingPressure(BasePatientObservation obs)
{
//Diferencia entre la Pmeset y la PEEP
@@ -361,6 +448,11 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <summary>
/// Evaluates whether a patient meets the over-analgesia criteria based on pain-related observations (EVN between 0 and 3, ESCID equal to 3, or ANI greater than 70) combined with sustained infusions of morphine, remifentanil, or fentanyl exceeding the analgesia threshold, and records an "Over_Analgesia" observation when the conditions are satisfied.
/// </summary>
/// <param name="obs">The observation that triggered the evaluation; its value must be numeric and it is ignored if <paramref name="patientId"/> is provided (e.g., when invoked from a treatment context).</param>
/// <param name="patientId">The patient identifier when the evaluation is triggered by a treatment event; when null, it is derived from <paramref name="obs"/>.</param>
private async Task CalculateOverAnalgesia(BasePatientObservation? obs, ObjectId? patientId = null)
{
// Mandar observación 'SOBREANALGESIA'
@@ -435,6 +527,11 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <summary>
/// Normalizes the ECMO location value of a patient observation. If the observation value matches one of the ECMO variants (ECCO2r, VVDL, VA+V, VVDL+V, VV+V, or VVA), it is mapped to "ECMO"; VV and VA values are left unchanged. If the observation is not a PatientObservation, it is returned unmodified.
/// </summary>
/// <param name="obs">The base patient observation to evaluate and potentially transform.</param>
/// <returns>A task that yields the observation, with its value normalized to "ECMO" when the original value is one of the recognized ECMO variants.</returns>
private Task<BasePatientObservation> CalculateEcmoLocation(BasePatientObservation obs)
{
//Las opciones pueden ser VV, VA o ECMO.
@@ -453,6 +550,15 @@ public class CalculatedObservations : ICalculatedObservations
return Task.FromResult(obs);
}
/// <summary>
/// Determines the ventilation mode for a patient based on the type of the incoming observation:
/// assigns <c>VMNI</c> when an Inspiratory Pressure value is received without a Compliancia value,
/// assigns <c>VMI</c> when a Compliancia value is received, and reads the mode from the CCC coding
/// system when an Air Flow value is received. If a mode is resolved, it is persisted as a new
/// <c>Ventilation_Mode</c> patient observation; otherwise the method returns without inserting
/// anything.
/// </summary>
/// <param name="obs">The incoming patient observation whose name drives the ventilation mode calculation.</param>
private async Task CalculateVentilationMode(BasePatientObservation obs)
{
//- Si llega valor de PI pero NO COMPL, el tipo de ventilación es VMNI
@@ -501,6 +607,13 @@ public class CalculatedObservations : ICalculatedObservations
_ = _observationService.Value.InsertObservation(ventilationModeObservation, mapObs: false);
}
/// <summary>
/// Evaluates whether an "Over_Sedation" clinical condition should be recorded for a patient.
/// The condition is met when RASS is -4 or -5, PSI is below 25, TS is above 5, and an active treatment exceeds its sedation threshold (Propofol &gt; 3, Midazolam &gt; 0.05, or Isoflurane &gt; 10).
/// If invoked with a single observation, the method extracts the patient identifier, validates the observation value against its own threshold, and then loads the remaining required observations (RASS, PSI, TS) to confirm the full set of conditions before persisting the Over_Sedation observation.
/// </summary>
/// <param name="obs">The triggering patient observation used to evaluate the sedation state. When provided, its name and value drive the initial validation; when null, the method relies on the <paramref name="patientId"/> parameter (typically from a treatment update).</param>
/// <param name="patientId">Optional identifier of the patient to evaluate. If null, it is derived from <paramref name="obs"/>; otherwise, the method performs a full evaluation using the patient's recent observations.</param>
private async Task CalculateOverSedation(BasePatientObservation? obs, ObjectId? patientId = null)
{
//Mandar observación 'SOBRESEDACIÓN'
@@ -576,6 +689,12 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <summary>
/// Determines whether the given patient treatment has exceeded the 24-hour sustained analgesia threshold (e.g., morphine, remifentanil, or fentanyl perfusions).
/// Returns false when the treatment is null or its start time is not set.
/// </summary>
/// <param name="treatment">The patient treatment to evaluate; may be null.</param>
/// <returns><c>true</c> if the treatment has a start time and was started more than 24 hours ago; otherwise, <c>false</c>.</returns>
private static bool ExceedsAnalgesiaThreshold(PatientTreatment? treatment)
{
// Perfusiones de morfina, remifentanilo o fentanilo sostenidas > 24 h
@@ -584,6 +703,11 @@ public class CalculatedObservations : ICalculatedObservations
treatment.StartTime.Value.AddSeconds(86400) < DateTime.UtcNow;
}
/// <summary>
/// Determines whether a patient treatment exceeds the sedation threshold for Propofol, Midazolam, or Isoflurane based on requested minimum dosage amounts.
/// </summary>
/// <param name="treatment">The patient treatment to evaluate, or <c>null</c>.</param>
/// <returns><c>true</c> if the treatment is not <c>null</c> and any requested medication exceeds its defined sedation threshold; otherwise, <c>false</c>.</returns>
private static bool ExceedsSedationThreshold(PatientTreatment? treatment)
{
return (treatment != null &&
@@ -595,6 +719,11 @@ public class CalculatedObservations : ICalculatedObservations
c.Text == "Isoflorano" && treatment.RequestedGiveAmountMinimum > 10));
}
/// <summary>
/// Checks the active treatments of a patient and discontinues those whose end time is in the future by setting their <see cref="PatientTreatment.OrderControl"/> to <see cref="OrderControlType.Dc"/>, returning the remaining active treatments.
/// </summary>
/// <param name="treatment">The patient treatment whose patient is used to look up the list of active treatments to evaluate.</param>
/// <returns>A task that returns the list of active patient treatments that were not discontinued.</returns>
private async Task<List<PatientTreatment>> CheckExpiredPatientTreatments(PatientTreatment treatment)
{
//si el tratamiento ha expirado actualizamos el OrderControl a DC y devolvemos las que siguen activas
@@ -616,6 +745,13 @@ public class CalculatedObservations : ICalculatedObservations
return result;
}
/// <summary>
/// Validates the medicines associated with a patient treatment by combining newly requested medicines
/// with the patient's currently active medicines, deduplicating them by name, and verifying their
/// medication categories. If no new medicines are found, the method returns without performing any
/// further validation.
/// </summary>
/// <param name="treatment">The patient treatment whose requested medicines will be checked against the patient's active medicines.</param>
private async Task CheckTreatmentMedicines(PatientTreatment treatment)
{
var newMedicines = (await Task.WhenAll(treatment.RequestedGiveCodes
@@ -639,21 +775,26 @@ public class CalculatedObservations : ICalculatedObservations
CheckMedicationCategories(treatment.PatientId, uniqueMedicines as List<Medicine>);
}
/// <summary>
/// Classifies a patient's unique medicines into predefined clinical medication groups (e.g., sedation, inotropic, antibiotic, anxiolytic, antipsychotic, antidepressants, neuro medication, crystalloid/colloid serum, antihypertensives) and creates a multivalue observation for every category that contains at least one matching medicine. Categories whose list reference is null, medicines with a null or empty name, and categories with no matching medicines are skipped.
/// </summary>
/// <param name="patientId">The identifier of the patient whose medication categories are being evaluated and linked to the created observations.</param>
/// <param name="uniqueMedicines">The distinct list of medicines to be checked against the predefined category reference lists.</param>
private void CheckMedicationCategories(ObjectId patientId, List<Medicine> uniqueMedicines)
{
var medicationCategories = new Dictionary<string, List<string>?>
{
{ "Sedation_Medication_Multivalue", _sedationList },
{ "Inotropic_Medication_Multivalue", _inotropicMedicines },
{ "Antibiotic_Medication_Multivalue", _antibioticList },
{ "Anxiolytic_Medication_Multivalue", _anxiolyticsList },
{ "Antipsychotic_Medication_Multivalue", _antipsicoticList },
{ "Antidepressants_Medication_Multivalue", _antidepressantsList },
{ "Neuro_Medication_Multivalue", _neuroMedicationList },
{ "Crystalloid_Serum_Medication_Multivalue", _serumCrystalloidList },
{ "Colloid_Serum_Medication_Multivalue", _serumColloidList },
{ "Antihypertensives_Medication_Multivalue", _antihypertensivesList }
};
{
{ "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)
{
@@ -668,8 +809,15 @@ public class CalculatedObservations : ICalculatedObservations
}
/// <summary>
/// Creates a new multi-value patient observation containing the names of the provided medications, expiring any
/// previous non-expired observation with the same name for the patient before inserting the new one.
/// </summary>
/// <param name="medications">The collection of medications whose names will be stored as the observation values.</param>
/// <param name="patientId">The identifier of the patient the observation belongs to.</param>
/// <param name="obsName">The name used to group and look up the previous observation for the same concept.</param>
private async Task CreateMedicationMultivalueObservation(IEnumerable<Medicine?> medications, ObjectId patientId,
string obsName)
string obsName)
{
var newObs = new PatientObservation
{
@@ -12,6 +12,10 @@ using MongoDB.Bson;
namespace adas_core.Application.Customizations.HUVH.UCIN;
/// <summary>
/// Represents a concrete implementation of the <see cref="ICalculatedObservations"/> interface,
/// providing a collection of calculated observation data.
/// </summary>
public class CalculatedObservations : ICalculatedObservations
{
private const string Spo2PreObservationName = "SpO2";
@@ -46,12 +50,23 @@ public class CalculatedObservations : ICalculatedObservations
}
/// <summary>
/// Calculates the active bolus for the specified patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient for whom the active bolus is calculated.</param>
/// <exception cref="NotImplementedException">Thrown because the method has not been implemented yet.</exception>
public Task CalculateActiveBolus(ObjectId patientId)
{
throw new NotImplementedException();
}
/// <summary>
/// Maps a patient observation by applying the appropriate calculation logic based on its name, handling multi-value interventions, surgeries, complexity scoring, pre/post saturation differences, pain scales, and last defecation value.
/// </summary>
/// <param name="obs">The patient observation to be mapped, which may be transformed depending on its name.</param>
/// <param name="onlyByName">Indicates whether the mapping should be performed using only the observation name.</param>
/// <returns>The mapped patient observation, potentially enriched with computed values, wrapped in a task.</returns>
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);
@@ -78,6 +93,12 @@ public class CalculatedObservations : ICalculatedObservations
}
/// <summary>
/// Maps a <see cref="PatientTreatment"/> by determining its <see cref="OrderControlType"/> based on prior treatments and expiration status.
/// If the placer order number is missing, the treatment is returned unchanged. The order control is set to <see cref="OrderControlType.Xo"/> when previous treatments exist, <see cref="OrderControlType.Dc"/> when the treatment has expired (end time set), or <see cref="OrderControlType.Nw"/> otherwise.
/// </summary>
/// <param name="treatment">The patient treatment to map, including its placer order, end time, and medicines.</param>
/// <returns>The mapped <see cref="PatientTreatment"/> with its order control type and medicines updated.</returns>
public async Task<PatientTreatment> Map(PatientTreatment treatment)
{
var order = treatment.PlacerOrder?.EntityIdentifier; //aquí almacenamos el número de orden
@@ -97,12 +118,22 @@ public class CalculatedObservations : ICalculatedObservations
return treatment;
}
/// <summary>
/// Maps the provided <see cref="PumpObservation"/> instance by returning it unchanged, wrapped in a completed task.
/// </summary>
/// <param name="pumpObservation">The pump observation to map.</param>
/// <returns>A completed <see cref="Task{TResult}"/> containing the provided <see cref="PumpObservation"/>.</returns>
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
{
return await Task.FromResult(pumpObservation);
}
/// <summary>
/// Persists a multivalue medication observation for the specified patient based on the currently active medicines, expiring any previous medication observation recorded for the same patient.
/// </summary>
/// <param name="activeMedicines">The list of active medicines from which distinct medication names are extracted to build the observation value.</param>
/// <param name="patientId">The identifier of the patient for whom the medication observation is calculated and stored.</param>
public async Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
var medicineObs = new PatientObservation
@@ -129,6 +160,11 @@ public class CalculatedObservations : ICalculatedObservations
medicineObs); //mapObs:volvemos a mapear la observación para que pase por el cálculo de la complejidad
}
/// <summary>
/// Asynchronously retrieves the collection of active treatments associated with the specified patient identifier.
/// </summary>
/// <param name="id">The unique <see cref="ObjectId"/> of the patient whose active treatments should be fetched.</param>
/// <returns>A task that represents the asynchronous operation, containing an <see cref="IEnumerable{T}"/> of nullable <see cref="PatientTreatment"/> instances for the patient.</returns>
public async Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(id);
@@ -136,12 +172,23 @@ public class CalculatedObservations : ICalculatedObservations
}
/// <summary>
/// Asynchronously maps a <see cref="PatientDiagnosis"/> to a target <see cref="PatientDiagnosis"/> representation.
/// </summary>
/// <param name="diagnosis">The source <see cref="PatientDiagnosis"/> instance to be mapped.</param>
/// <returns>A <see cref="Task{PatientDiagnosis}"/> that represents the asynchronous mapping operation, containing the mapped <see cref="PatientDiagnosis"/>.</returns>
/// <exception cref="T:System.NotImplementedException">Thrown when the method is invoked, as the mapping logic has not yet been implemented.</exception>
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
throw new NotImplementedException();
}
/// <summary>
/// Adjusts the time of a new patient observation to ensure it is at least one second after the most recent observation with the same name, preventing time conflicts. If the observation name is null or empty, the method logs an error and returns the observation unchanged.
/// </summary>
/// <param name="newObservation">The new patient observation whose time may be adjusted to follow the most recent observation for the same patient and measurement.</param>
/// <returns>The patient observation with a corrected time if a time conflict was detected, or the original observation if no adjustment was needed or the name was invalid.</returns>
public async Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
if (string.IsNullOrEmpty(newObservation.Name))
@@ -169,21 +216,45 @@ public class CalculatedObservations : ICalculatedObservations
return newObservation;
}
/// <summary>
/// Performs a pre-mapping operation on a list of patient observations, returning the list for further processing.
/// </summary>
/// <param name="listToInsert">The list of patient observations to pre-map.</param>
/// <returns>A task that represents the asynchronous operation, containing the provided list of patient observations.</returns>
public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
{
return Task.FromResult(listToInsert);
}
/// <summary>
/// Maps a source alarm onto a patient observation, returning the observation unchanged without applying the alarm data.
/// </summary>
/// <param name="obs">The patient observation that the alarm is to be associated with.</param>
/// <param name="alarmToInsert">The alarm intended to be inserted into the observation.</param>
/// <returns>A task containing the patient observation, returned as-is.</returns>
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
return Task.FromResult(obs);
}
/// <summary>
/// Sends an alarm notification for the specified patient observation, identified by name and optional alarm code.
/// </summary>
/// <param name="obs">The patient observation that triggers the alarm.</param>
/// <param name="name">The name associated with the alarm.</param>
/// <param name="code">The optional alarm code categorizing the type of alarm to be sent.</param>
/// <exception cref="NotImplementedException">Thrown because the method has not been implemented.</exception>
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
throw new NotImplementedException();
}
/// <summary>
/// Calculates and stores a derived "Pain_Scale" observation in the ADAS coding system based on the provided source observation.
/// Handles the ALPS scale by using its value directly, and the NPASS scale by combining the complementary "NPASS_Sedation" and "NPASS_Analgesia" observations into a single combined value (defaulting to "0" when the complementary observation is not found).
/// Returns without creating a record when the observation is not a <see cref="PatientObservation"/> or when its name does not match any of the supported scales.
/// </summary>
/// <param name="obs">The source patient observation used to derive the pain scale; only instances of <see cref="PatientObservation"/> with a supported name (ALPS, NPASS_Sedation, NPASS_Analgesia) are processed.</param>
private async Task CalculatePainScale(BasePatientObservation obs)
{
if (obs is not PatientObservation pobs) return;
@@ -197,24 +268,24 @@ public class CalculatedObservations : ICalculatedObservations
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";
{
//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}";
}
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";
{
//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}";
}
valueObs = $"{sedationValue}/{pobs.Value}";
}
break;
default:
return;
@@ -232,6 +303,11 @@ public class CalculatedObservations : ICalculatedObservations
_ = _observationService.Value.InsertObservation(painScaleObs, mapObs: false);
}
/// <summary>
/// Calculates the last defecation value for a patient observation by assigning the observation's time, converted to local time and formatted as a date/time string, to its value. If the observation is not a <see cref="PatientObservation"/>, it is returned unchanged.
/// </summary>
/// <param name="obs">The base patient observation to process.</param>
/// <returns>The original <see cref="BasePatientObservation"/> if it is not a <see cref="PatientObservation"/>; otherwise, the <see cref="PatientObservation"/> with its value set to the locally formatted observation time.</returns>
private static BasePatientObservation CalculateLastDefecationValue(BasePatientObservation obs)
{
if (obs is not PatientObservation pobs) return obs;
@@ -246,6 +322,10 @@ public class CalculatedObservations : ICalculatedObservations
}
/// <summary>
/// Calculates and persists a multi-value observation by appending the current value to the existing list of values for the corresponding "Multivalue" observation. Expirates the previous multi-value observation before inserting the new one.
/// </summary>
/// <param name="obs">The base patient observation used to derive the multi-value observation; it must be a <see cref="PatientObservation"/> with a non-empty name and a string value.</param>
private async Task CalculateMultiValueObservation(BasePatientObservation obs)
{
//El valor de la observación es un array de strings
@@ -298,6 +378,10 @@ public class CalculatedObservations : ICalculatedObservations
await _observationService.Value.InsertObservation(newObservation);
}
/// <summary>
/// Calculates and persists a multi-value observation that aggregates the active intervention groups (e.g., devices, routes) for a patient based on incoming "<c>_In</c>" and "<c>_Out</c>" observations. Handles the insertion of new groups (with special duplicate prevention for respiratory devices) and the removal of groups when a corresponding "Out" event is received, expiring the previous multi-value observation and inserting a new one when the resulting set is not empty.
/// </summary>
/// <param name="obs">The base patient observation that triggered the calculation; expected to be a <see cref="PatientObservation"/> with a non-empty name and a string value representing the intervention group.</param>
private async Task CalculateInterventionMultiValueObservation(BasePatientObservation obs)
{
if (obs is not PatientObservation pobs || string.IsNullOrEmpty(obs.Name) ||
@@ -380,6 +464,14 @@ public class CalculatedObservations : ICalculatedObservations
await _observationService.Value.InsertObservation(newObservation);
}
/// <summary>
/// Extracts a numeric code from the observation value, normalizes it, and resolves the corresponding intervention
/// by looking it up in the mapping repository. Returns false when the code cannot be parsed as a number or when no
/// matching intervention is found.
/// </summary>
/// <param name="valueObs">Observation text from which the leading token is taken as the candidate intervention code.</param>
/// <param name="r">When the method returns true, contains the resolved intervention's type, name, and group; otherwise null.</param>
/// <returns><c>true</c> if a matching intervention was found; <c>false</c> if the code is not numeric or the lookup yields no result.</returns>
private bool GetInterventionValue(string valueObs, out (string type, string Name, string Group)? r)
{
var code = valueObs.Split(" ")[0]; //
@@ -404,6 +496,10 @@ public class CalculatedObservations : ICalculatedObservations
* 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.
*/
/// <summary>
/// Calcula la complejidad del paciente en función de las observaciones registradas.
/// </summary>
/// <param name="obs">Observación del paciente que se va a evaluar para recalcular su complejidad.</param>
private async Task CalculateComplexity(BasePatientObservation obs)
{
var pobs = obs as PatientObservation;
@@ -466,8 +562,15 @@ public class CalculatedObservations : ICalculatedObservations
await _observationService.Value.InsertObservation(complexity, mapObs: false);
}
/// <summary>
/// Calculates the total complexity value for a multivalue observation belonging to a patient. Uses the supplied observation when provided; otherwise retrieves the most recent one from the database and sums the complexity values resolved via the mapping utility, returning 0 when no data is available.
/// </summary>
/// <param name="patientId">The identifier of the patient whose observation is being evaluated.</param>
/// <param name="observationName">The name of the multivalue observation to look up and calculate complexity for.</param>
/// <param name="observation">An optional pre-loaded observation that overrides the database lookup when supplied.</param>
/// <returns>The aggregated complexity value for the observation, or 0 if no observation or values are found.</returns>
private async Task<int> CalculateMultivalueObservationComplexityValue(ObjectId patientId, string observationName,
PatientObservation? observation = null)
PatientObservation? observation = null)
{
var result =
await _observationService.Value.FindLastObservations(patientId, 1, [observationName]);
@@ -486,6 +589,13 @@ public class CalculatedObservations : ICalculatedObservations
}
/// <summary>
/// Calculates the complexity value associated with a newborn's weight observation for the specified patient.
/// When <paramref name="pobs"/> is null, the most recent stored "Newborn_Weight" observation is used; otherwise the provided observation is used, converting from kilograms when applicable. Returns 0 when no value can be retrieved or parsed, or when the observation name is missing, and otherwise returns the complexity value resolved via the mapping utilities.
/// </summary>
/// <param name="patientId">The identifier of the patient whose newborn weight complexity is being calculated.</param>
/// <param name="pobs">An optional patient observation to use instead of the latest stored one; if provided with "kg" units, its value is converted.</param>
/// <returns>The calculated newborn weight complexity value, or 0 when no valid value is available.</returns>
private async Task<int> CalculateWeightNewBornValue(ObjectId patientId, PatientObservation? pobs = null)
{
var result =
@@ -522,6 +632,12 @@ public class CalculatedObservations : ICalculatedObservations
return weightNewBornValue;
}
/// <summary>
/// Parses a weight observation by converting its value to grams (multiplying by 1000) and assigning the "gr" unit.
/// If the value cannot be parsed as a double, the error is logged and the original observation is returned unchanged.
/// </summary>
/// <param name="obs">The patient observation containing the weight value to parse and convert.</param>
/// <returns>The patient observation with the value converted to grams, or the original observation if the value could not be parsed.</returns>
private PatientObservation ParseWeight(PatientObservation obs)
{
if (!double.TryParse(obs.Value.ToString(), out var dValue))
@@ -535,7 +651,13 @@ public class CalculatedObservations : ICalculatedObservations
return obs;
}
/// <summary>
/// Calculates the SpO2 difference between pre and post observations for a patient and persists the resulting SpO2_Diff and combined pre/post observations.
/// Handles both pre and post observation entry points, adjusts the timestamp to the earlier of the two observations, and defaults to 0 when a value cannot be parsed as a double.
/// </summary>
/// <param name="obs">The incoming patient observation (either a pre or post SpO2 measurement) used to drive the calculation and identify the counterpart observation.</param>
/// <returns>A task that represents the asynchronous insert of the derived observations.</returns>
private async Task CalculateSaturation_DiffObservation(BasePatientObservation obs)
{
var toSearchList = new List<string>();
@@ -593,6 +715,11 @@ public class CalculatedObservations : ICalculatedObservations
}
}
/// <summary>
/// Checks for expired patient treatments and updates the matching treatment's <see cref="OrderControlType"/> to <c>DC</c> when its end time is in the future, returning the remaining active treatments for the same patient.
/// </summary>
/// <param name="treatment">The patient treatment being evaluated, used to locate the existing record by its placer order entity identifier and to persist the updated state.</param>
/// <returns>A task that yields the list of active patient treatments for the patient that are not the same placer order as the supplied treatment.</returns>
private async Task<List<PatientTreatment>> CheckExpiredPatientTreatments(PatientTreatment treatment)
{
//si el tratamiento ha expirado actualizamos el OrderControl a DC y devolvemos las que siguen activas
@@ -622,6 +749,14 @@ public class CalculatedObservations : ICalculatedObservations
return result;
}
/// <summary>
/// Validates the medicines requested in a patient treatment, handling nutrition items by creating
/// dedicated observations, detecting vasoactive drugs to record a flag, and combining remaining
/// medicines with the patient's active treatment medicines to calculate a unified medicine observation.
/// Medicines that are not found by code are skipped, and the method returns early when no new
/// medicines remain after filtering.
/// </summary>
/// <param name="treatment">The patient treatment whose requested give codes are inspected for medicines, nutrition items, and vasoactive drugs.</param>
private async Task CheckTreatmentMedicines(PatientTreatment treatment)
{
var newMedicines = new List<Medicine>();
@@ -679,6 +814,11 @@ public class CalculatedObservations : ICalculatedObservations
await CalculateMedicineObservation(uniqueMedicines, treatment.PatientId);
}
/// <summary>
/// Creates a new feeding/nutrition observation for the specified patient. When the value indicates parenteral feeding, the observation name is normalized to "Parenteral" and the value to "SI". Any previous non-expired observation with the same name is marked as expired before the new one is inserted.
/// </summary>
/// <param name="value">The feeding type value to record; if it contains "parenteral" (case-insensitive), the observation is stored as a parenteral entry.</param>
/// <param name="patientId">The identifier of the patient to whom the observation belongs.</param>
private async Task CreateNutritionObservation(string value, ObjectId patientId)
{
var nutritionObs = new PatientObservation
@@ -7,63 +7,127 @@ using MongoDB.Bson;
namespace adas_core.Application.Customizations.NursePlan;
/// <summary>
/// Provides calculated observations by leveraging services obtained from the injected service provider.
/// </summary>
public class CalculatedObservations(IServiceProvider serviceProvider) : ICalculatedObservations
{
private readonly Lazy<IObservationService> _observationService =
new(serviceProvider.GetRequiredService<IObservationService>);
/// <summary>
/// Asynchronously calculates the active bolus for the specified patient.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose active bolus is to be calculated.</param>
/// <exception cref="System.NotImplementedException">Thrown when the method is invoked, as the calculation logic has not yet been implemented.</exception>
public Task CalculateActiveBolus(ObjectId patientId)
{
throw new NotImplementedException();
}
/// <summary>
/// Asynchronously maps the provided PumpObservation, returning the input instance as the mapping result.
/// </summary>
/// <param name="pumpObservation">The PumpObservation to be mapped.</param>
/// <returns>A task containing the mapped PumpObservation.</returns>
public async Task<PumpObservation> Map(PumpObservation pumpObservation)
{
return await Task.FromResult(pumpObservation);
}
/// <summary>
/// Asynchronously calculates a medicine observation for the specified patient based on their active medicines.
/// </summary>
/// <param name="activeMedicines">The collection of medicines currently active for the patient.</param>
/// <param name="patientId">The identifier of the patient whose medicine observation is being calculated.</param>
/// <exception cref="NotImplementedException">The method is not yet implemented.</exception>
public Task CalculateMedicineObservation(List<Medicine> activeMedicines, ObjectId patientId)
{
throw new NotImplementedException();
}
/// <summary>
/// Asynchronously resolves time inconsistencies between the supplied observation and the most recent previous observation, returning a corrected observation or <c>null</c> when no correction is required.
/// </summary>
/// <param name="newObservation">The new patient observation whose timestamps may need to be reconciled with the last recorded observation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the fixed <see cref="PatientObservation"/>, or <c>null</c> when no time inconsistency is detected.</returns>
/// <exception cref="NotImplementedException">Thrown because the method has not been implemented yet.</exception>
public Task<PatientObservation?> FixTimeInconsistencyWithLast(PatientObservation newObservation)
{
throw new NotImplementedException();
}
/// <summary>
/// Asynchronously retrieves the active treatment records associated with the specified patient.
/// </summary>
/// <param name="id">The unique identifier of the patient whose active treatments are being queried.</param>
/// <returns>A task representing the asynchronous operation, containing a collection of active PatientTreatment entries, which may include null values, for the specified patient.</returns>
/// <exception cref="NotImplementedException">The method has not yet been implemented.</exception>
public Task<IEnumerable<PatientTreatment?>> GetActiveTreatmentsByPatient(ObjectId id)
{
throw new NotImplementedException();
}
/// <summary>
/// Returns the provided patient observation wrapped in a completed task without modification.
/// </summary>
/// <param name="obs">The patient observation to return.</param>
/// <param name="onlyByName">Reserved flag that is not used in the current implementation.</param>
/// <returns>A completed task containing the provided observation.</returns>
public Task<T?> Map<T>(T obs, bool onlyByName = false) where T : BasePatientObservation
{
return Task.FromResult(obs)!;
}
/// <summary>
/// Maps a patient treatment to its target representation asynchronously, returning the provided treatment instance unchanged.
/// </summary>
/// <param name="treatment">The patient treatment to map.</param>
/// <returns>A task containing the mapped patient treatment.</returns>
public Task<PatientTreatment> Map(PatientTreatment treatment)
{
return Task.FromResult(treatment);
}
/// <summary>
/// Maps a <see cref="PatientDiagnosis"/> instance to a completed task containing the same instance.
/// </summary>
/// <param name="diagnosis">The patient diagnosis to be returned as a completed task result.</param>
/// <returns>A completed <see cref="Task{PatientDiagnosis}"/> containing the provided diagnosis.</returns>
public Task<PatientDiagnosis> Map(PatientDiagnosis diagnosis)
{
return Task.FromResult(diagnosis);
}
/// <summary>
/// Returns the provided <paramref name="obs"/> as-is, ignoring the <paramref name="alarmToInsert"/> parameter.
/// </summary>
/// <param name="obs">The patient observation to return.</param>
/// <param name="alarmToInsert">The patient observation alarm; not used in the current implementation.</param>
/// <returns>A completed task containing the original <paramref name="obs"/>.</returns>
public Task<PatientObservation> MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert)
{
return Task.FromResult(obs);
}
/// <summary>
/// Sends an alarm notification based on the specified patient observation, name, and optional alarm code.
/// </summary>
/// <param name="obs">The patient observation that triggers the alarm.</param>
/// <param name="name">The name associated with the alarm.</param>
/// <param name="code">The optional alarm code identifying the alarm type.</param>
public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code)
{
return Task.CompletedTask;
}
/// <summary>
/// Returns the provided patient observation list unchanged as a pre-mapping step, typically used as a hook before further mapping or insertion operations.
/// </summary>
/// <param name="listToInsert">The list of patient observations to be pre-mapped.</param>
/// <returns>A completed <see cref="Task{TResult}"/> containing the original list of patient observations.</returns>
public Task<List<PatientObservation>> PreMapList(List<PatientObservation> listToInsert)
{
return Task.FromResult(listToInsert);