315 lines
15 KiB
C#
315 lines
15 KiB
C#
using System.Collections;
|
|
using adas_core.Application.Services.Interfaces;
|
|
using adas_core.Domain.Enums;
|
|
using adas_core.Domain.Models;
|
|
using adas_core.Domain.Models.GroupedObservations;
|
|
using adas_core.Domain.Models.MongoModels;
|
|
using adas_core.Domain.Utils;
|
|
|
|
namespace adas_core.Application.Services;
|
|
|
|
/// <summary>
|
|
/// Provides a demo implementation of <see cref="IObservationDemoService"/> that coordinates observation handling using <see cref="IConfigObservationService"/> for configuration and a <see cref="Lazy{T}"/> of <see cref="IAlarmService"/> for deferred alarm access.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The service is constructed with an <see cref="IConfigObservationService"/> for observation configuration and a lazily-initialized <see cref="IAlarmService"/> so that alarm functionality is created only on first use.
|
|
/// </remarks>
|
|
/// <!-- aidoc:v1 sig=fc27ac9 -->
|
|
public class ObservationDemoService(
|
|
IConfigObservationService configObservationService,
|
|
Lazy<IAlarmService> alarmService)
|
|
: IObservationDemoService
|
|
{
|
|
/// <summary>
|
|
/// Generates a list of patient observation alarms based on the provided alarm fields, randomly skipping fields and skipping those with no matching configuration.
|
|
/// </summary>
|
|
/// <param name="patient">The patient to associate with the generated alarms.</param>
|
|
/// <param name="dataAlarmfields">The list of fields used to look up alarm configurations and produce alarms.</param>
|
|
/// <returns>A task that represents the asynchronous operation, containing the list of mapped <see cref="PatientObservationAlarm"/> instances that were successfully resolved.</returns>
|
|
/// <!-- aidoc:v1 sig=bc2b31e body=f044666 -->
|
|
public async Task<List<PatientObservationAlarm>> GenerateAlarmByField(Patient patient, List<Field> dataAlarmfields)
|
|
{
|
|
var listToReturn = new List<PatientObservationAlarm>();
|
|
foreach (var field in dataAlarmfields)
|
|
{
|
|
var sendingRandom = new Random();
|
|
|
|
if (sendingRandom.Next(0, 2) == 0 || string.IsNullOrEmpty(field.Name))
|
|
continue;
|
|
|
|
var firstCof = await configObservationService.GetConfigObservationItemsByName(field.Name);
|
|
var conf = firstCof.FirstOrDefault();
|
|
if (conf == null)
|
|
continue;
|
|
|
|
var alarmObs = new PatientObservationAlarm
|
|
{
|
|
Patient = patient,
|
|
PatientId = patient.Id,
|
|
AlarmConfig = conf.Alarm,
|
|
InactivationState = new InactivationState
|
|
{
|
|
Audio = AlarmEnum.AudioVideoState.Enabled,
|
|
Acknowledge = true,
|
|
Visual = AlarmEnum.AudioVideoState.Enabled
|
|
},
|
|
EventPhase = AlarmEnum.EventPhase.Continue,
|
|
Type = AlarmEnum.ObservationAlarmType.Sp,
|
|
Expires = conf.Expires,
|
|
State = AlarmEnum.ObservationAlarmState.Active,
|
|
Time = DateTime.UtcNow,
|
|
Persist = false,
|
|
MessageTime = DateTime.UtcNow,
|
|
Code = conf.Code,
|
|
PriorityLevel = conf.DemoConfig?.PriorityAlarm,
|
|
Name = conf.Name,
|
|
ParentData = new ParentDataClass
|
|
{
|
|
Code = conf.ParentCode,
|
|
Name = conf.ParentName,
|
|
CodingSystem = conf.CodingSystem
|
|
},
|
|
Units = conf.Units,
|
|
Value = GenerateValueRandom(conf, null) ?? 0
|
|
};
|
|
var obs = await alarmService.Value.MapObservationsByName(alarmObs);
|
|
|
|
|
|
if (obs != null)
|
|
listToReturn.Add(obs);
|
|
}
|
|
|
|
return listToReturn;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates a list of patient observations for the specified patient based on the provided data fields, using the corresponding configuration items to populate observation values, thresholds, and metadata. Fields without a name or without a matching configuration are skipped. When a configuration includes demo settings, the resulting observation is adjusted with a randomized initial date and, if required, an end time.
|
|
/// </summary>
|
|
/// <param name="patient">The patient to associate the generated observations with.</param>
|
|
/// <param name="dataFields">The list of fields used to look up observation configurations and drive observation generation.</param>
|
|
/// <returns>A task that returns the list of generated <see cref="PatientObservation"/> instances; entries are omitted when no configuration is found for a field.</returns>
|
|
/// <!-- aidoc:v1 sig=1045c66 body=1145e03 -->
|
|
public async Task<List<PatientObservation>> GenerateObservationByField(Patient patient, List<Field> dataFields)
|
|
{
|
|
var listToReturn = new List<PatientObservation>();
|
|
var rnd = new Random();
|
|
foreach (var field in dataFields)
|
|
{
|
|
if (string.IsNullOrEmpty(field.Name))
|
|
continue;
|
|
|
|
var conf = await configObservationService.GetConfigObservationItemsByName(field.Name);
|
|
var firstCof = conf.FirstOrDefault();
|
|
if (firstCof == null)
|
|
continue;
|
|
|
|
var patientObs = new PatientObservation
|
|
{
|
|
Patient = patient,
|
|
PatientId = patient.Id,
|
|
Max = firstCof.MaxAlert,
|
|
Min = firstCof.MinAlert,
|
|
MaxWarn = firstCof.MaxWarn,
|
|
MinWarn = firstCof.MinWarn,
|
|
ShowOnExpired = firstCof.ShowOnExpired,
|
|
Expires = firstCof.Expires,
|
|
UiConfiguration = firstCof.UiConfiguration,
|
|
MessageTime = DateTime.UtcNow,
|
|
Code = firstCof.Code,
|
|
ColorOnExpired = firstCof.ColorOnExpired,
|
|
AlertColor = firstCof.AlertColor,
|
|
WarnColor = firstCof.WarnColor,
|
|
CodingSystem = firstCof.CodingSystem,
|
|
Alarm = firstCof.Alarm,
|
|
InsertMode = firstCof.InsertMode,
|
|
Name = field.Name,
|
|
Time = DateTime.UtcNow,
|
|
ParentData = new ParentDataClass
|
|
{
|
|
Code = firstCof.ParentCode,
|
|
Name = firstCof.ParentName,
|
|
CodingSystem = firstCof.CodingSystem
|
|
},
|
|
Units = firstCof.Units,
|
|
Value = GenerateValueRandom(firstCof, null) ?? 0
|
|
};
|
|
var result = await configObservationService.Map(patientObs, true);
|
|
if (firstCof.DemoConfig?.RequireInitDate == true && result != null)
|
|
{
|
|
var rndRestNmbHours = rnd.Next(3, 23);
|
|
var initDate = DateTime.UtcNow.AddHours(-rndRestNmbHours);
|
|
|
|
// Si se requiere fecha de fin, devolvemos un objeto con ambas
|
|
if (firstCof.DemoConfig?.RequireEndDate == true)
|
|
{
|
|
result.Time = initDate;
|
|
result.EndTime = initDate.AddHours(1);
|
|
}
|
|
|
|
// Si no, solo devolvemos la fecha inicial
|
|
result.Time = initDate;
|
|
}
|
|
|
|
if (result != null)
|
|
listToReturn.Add(result);
|
|
}
|
|
|
|
return listToReturn;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates a <see cref="GroupedObservation"/> for the given patient and grouped field, producing time-spaced observations for each configured name and result type based on the field's regularity.
|
|
/// </summary>
|
|
/// <param name="patient">The patient to associate the generated grouped observation with.</param>
|
|
/// <param name="groupedField">The grouped field configuration that defines the names, results, regularity, and maximum number of observations to generate.</param>
|
|
/// <returns>A task that returns the populated <see cref="GroupedObservation"/> containing the generated observations.</returns>
|
|
/// <!-- aidoc:v1 sig=32f5cfc body=c681ea4 -->
|
|
public async Task<GroupedObservation> GenerateGroupedObservation(Patient patient, GroupedField groupedField)
|
|
{
|
|
if (groupedField.Names.IsNullOrEmpty() && groupedField.Name != null) groupedField.Names.Add(groupedField.Name);
|
|
var rnd = new Random();
|
|
var startTime = DateTime.UtcNow;
|
|
var go = new GroupedObservation
|
|
{
|
|
Name = groupedField.Name,
|
|
PatientId = patient.Id,
|
|
Group = groupedField.Group,
|
|
Observations = []
|
|
};
|
|
foreach (var groupedFieldName in groupedField.Names)
|
|
{
|
|
var conf = await configObservationService.GetConfigObservationItemsByName(groupedFieldName);
|
|
var firstCof = conf.FirstOrDefault();
|
|
if (firstCof == null)
|
|
continue;
|
|
|
|
foreach (var result in groupedField.Result)
|
|
{
|
|
object? prevValue = null;
|
|
for (var i = 0; i <= groupedField.Max; i++)
|
|
{
|
|
var time = startTime;
|
|
time = groupedField.Regularity switch
|
|
{
|
|
GroupedObservationEnum.Regularity.Day => time.AddDays(-i),
|
|
GroupedObservationEnum.Regularity.Minute => time.AddMinutes(-i),
|
|
GroupedObservationEnum.Regularity.Second => time.AddSeconds(-i),
|
|
_ => time.AddHours(-i)
|
|
};
|
|
var val = new GroupedObservation.GroupedObservationObs
|
|
{
|
|
Name = groupedFieldName,
|
|
MaxAlert = firstCof?.MaxAlert,
|
|
MinAlert = firstCof?.MinAlert,
|
|
Time = time,
|
|
IsFilled = false
|
|
};
|
|
var value = new GroupedObservation.GroupedObservationObsValue(
|
|
GenerateValueRandom(firstCof, prevValue) ?? rnd.Next(0, 30), time);
|
|
prevValue = value.Value;
|
|
var propertyInfo = val.GetType().GetProperty(result.ToString());
|
|
|
|
if (propertyInfo != null && propertyInfo.CanWrite) propertyInfo.SetValue(val, value);
|
|
var valueType = await configObservationService.GroupedObservationStatus(
|
|
groupedField,
|
|
result,
|
|
groupedFieldName,
|
|
val,
|
|
firstCof?.MaxAlert,
|
|
firstCof?.MinAlert
|
|
);
|
|
value.Type = valueType;
|
|
go.Observations.Add(
|
|
val
|
|
);
|
|
}
|
|
|
|
if (firstCof?.DemoConfig?.RequireInitDate.HasValue == true && firstCof.DemoConfig.RequireInitDate.Value)
|
|
break;
|
|
}
|
|
}
|
|
|
|
return go;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates a random value (or list of values) for a given configuration, falling back to "-" when no configuration is available. When the configuration specifies an array, produces multiple random values using the configured count; otherwise returns a single random value.
|
|
/// </summary>
|
|
/// <param name="firstCof">The configuration observation whose <c>DemoConfig</c> drives the value generation. If null or its <c>DemoConfig</c> is null, the method returns "-".</param>
|
|
/// <param name="prevValue">The previous value, passed through to <c>PickSingleValue</c> as context for the new random value generation.</param>
|
|
/// <returns>A list of generated values when the configuration indicates an array, or a single generated value otherwise. Null is returned when no value could be picked.</returns>
|
|
/// <!-- aidoc:v1 sig=22e46d9 body=7abbfc1 -->
|
|
private static object? GenerateValueRandom(ConfigObservation? firstCof, object? prevValue)
|
|
{
|
|
if (firstCof?.DemoConfig == null)
|
|
return "-";
|
|
|
|
// 1. Determinar cuántos valores necesitamos
|
|
var count = firstCof.DemoConfig?.ValueIsArray == true ? firstCof.DemoConfig?.RandomCount ?? 1 : 1;
|
|
var results = new List<object>();
|
|
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
var singleValue = PickSingleValue(firstCof.DemoConfig, prevValue);
|
|
if (singleValue != null)
|
|
results.Add(singleValue);
|
|
}
|
|
|
|
// 2. Si no es un array, devolvemos solo el primer elemento
|
|
return firstCof.DemoConfig?.ValueIsArray == true ? results : results.FirstOrDefault();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Picks a single value from a <see cref="DemoConfig"/> following a defined priority: first selects randomly from a predefined list of options when <c>SetInValue</c> is enabled, otherwise generates a random numeric value within the configured range, optionally applying a percentage-based variation to a previous value. Returns <c>null</c> if the configuration is null, if the options list is empty, or if random value generation is disabled.
|
|
/// </summary>
|
|
/// <param name="config">The configuration that defines how the value should be selected, including the list of options, the numeric range, and the selection mode.</param>
|
|
/// <param name="prevValue">The previously selected value, used as the base for percentage-based variation when generating a new numeric value; ignored when no previous integer value is available.</param>
|
|
/// <returns>A randomly selected value from the configured options or a generated numeric value clamped within the defined range; <c>null</c> when the configuration does not allow a value to be produced.</returns>
|
|
/// <!-- aidoc-review:v1 severity=medium kind=wrong_summary
|
|
/// "The summary and returns tag state the method returns null when the options list is empty, but the code throws ArgumentOutOfRangeException via rnd.Next(0) on the empty list rather than returning null." -->
|
|
private static object? PickSingleValue(DemoConfig? config, object? prevValue)
|
|
{
|
|
if (config == null) return null;
|
|
|
|
var rnd = new Random();
|
|
// Prioridad 1: Selección de una lista de opciones (SetInValue)
|
|
if (config is { SetInValue: true, ValueOption: not null })
|
|
{
|
|
if (config.ValueOption is List<object> { Count: > 0 } options)
|
|
{
|
|
var index = rnd.Next(options.Count);
|
|
return options.ElementAt(index);
|
|
}
|
|
|
|
// Caso especial si ValueOption llega como un JArray o lista genérica
|
|
if (config.ValueOption is IEnumerable list)
|
|
{
|
|
var tempList = list.Cast<object>().ToList();
|
|
return tempList[rnd.Next(tempList.Count)];
|
|
}
|
|
}
|
|
|
|
// Prioridad 2: Generación numérica por rango (SetRandomValue)
|
|
if (config.SetRandomValue == false)
|
|
return null;
|
|
|
|
// 1. Extraemos los límites primero para usarlos en cualquier caso
|
|
var minLimit = config.MinValue ?? 0;
|
|
var maxLimit = config.MaxValue ?? 100;
|
|
|
|
if (prevValue is not int values)
|
|
// Caso base: Si no hay valor previo, generamos uno aleatorio dentro del rango permitido
|
|
return rnd.Next(minLimit, maxLimit + 1);
|
|
|
|
if (values == 0) values = 1;
|
|
// 2. Calculamos la variación (entre 5% y 10%)
|
|
var porcentaje = rnd.Next(5, 10) / 100.0;
|
|
var direccion = rnd.Next(0, 2) == 0 ? -1 : 1;
|
|
|
|
// 3. Calculamos el valor base redondeado
|
|
var calculado = (int)Math.Round(values + values * porcentaje * direccion);
|
|
|
|
// 4. Forzamos que esté dentro del rango [minLimit, maxLimit]
|
|
return Math.Clamp(calculado, minLimit, maxLimit);
|
|
}
|
|
} |