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
@@ -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
{