using System.Globalization; using adas_core.Application.Repositories.Interfaces; using adas_core.Application.Services.Interfaces; using adas_core.Application.Subscriptions; using adas_core.Domain.Enums; using adas_core.Domain.Models; using adas_core.Domain.Models.AppSettings; using adas_core.Domain.Models.GroupedObservations; using adas_core.Domain.Utils; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using MongoDB.Bson; using static adas_core.Domain.Models.GroupedObservation; namespace adas_core.Application.Services; public class GroupedObservationService : IGroupedObservationService { private readonly CacheSettings? _cacheSettings; private readonly ICacheService _cacheService; private readonly IConfigObservationService _configObservationService; private readonly ILogger _logger; private readonly IObservationRepository _observationRepository; public GroupedObservationService( IObservationRepository observationRepository, IConfigObservationService configObservationService, ILogger logger, ICacheService cacheService, IOptions apiSettings, IOptions cacheSettings) { _observationRepository = observationRepository; _configObservationService = configObservationService; _logger = logger; _cacheService = cacheService; _cacheSettings = cacheSettings.Value; } // PUBLIC API (Use cases) public async Task GenerateGroupedObservation( ObjectId patientId, GroupedField groupedField, string timeZoneId = "Romance Standard Time", bool cacheIsChecked = false, CancellationToken ct = default) { // Loader que calcula TODA la lista de observaciones agrupadas async Task> ComputeObservationsAsync() { var result = await _observationRepository.AggregatedPatientGroupedObservations(patientId, groupedField); var tz = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId); result = FillHours(result, groupedField); if (groupedField.StartTimeShift.Count > 0 && groupedField.Regularity == GroupedObservationEnum.Regularity.Shift) { var shiftGroupObservations = GenerateShiftObservations(result, groupedField); result = CalculateShiftObservations(shiftGroupObservations, groupedField); } if (groupedField.Result.Contains(GroupedObservationEnum.Result.HalfHour)) { result = CalculateHalfHourObservations(result); result = FillHours(result, groupedField); } if (groupedField.Result.Contains(GroupedObservationEnum.Result.LastFilled)) result = await CalculateLastFilledObservations(result, groupedField, patientId); var list = new List(); foreach (var it in result.ToList()) { var id = it.TryGetElement("_id", out var _) ? it.GetElement("_id").Value.AsBsonDocument : null; var name = id?.Get("name")?.AsString; if (name is null or "_id") { var idValue = id?.Get("value") as BsonDocument; name = idValue?.Get("name")?.AsString; } var uTime = it.Get("time")?.ToUniversalTime(); if (!uTime.HasValue) { _logger.LogError("Error getting time to universal time. Bson document: {it}", it); continue; } var obsTime = TimeZoneInfo.ConvertTimeFromUtc(uTime.Value, tz); obsTime = DateTime.SpecifyKind(obsTime, DateTimeKind.Local); var obs = new GroupedObservationObs { Time = obsTime, Name = name ?? string.Empty }; if (it.TryGetValue("shift", out var shiftValue) && it.TryGetValue("day", out _)) { obs.Shift = shiftValue.ToInt32(); obs.ShiftDate = obs.Time; } if (id != null && id.Contains("min")) obs.MinAlert = id.Get("min")?.ToDouble(); if (id != null && id.Contains("max")) obs.MaxAlert = id.Get("max")?.ToDouble(); if (id != null && it.Contains("isFilled")) obs.IsFilled = (bool)it.Get("isFilled"); if (groupedField.Result.Contains(GroupedObservationEnum.Result.First)) { var g = !it.Contains("first") ? null : it.Get("first")?.AsBsonDocument; if (g != null) { var value = g.Get("value")?.ToObject(); var min = g.Get("min")?.ToDouble(); var max = g.Get("max")?.ToDouble(); var uTim = g.Get("time")?.ToUniversalTime(); if (!uTim.HasValue) { _logger.LogError("Error getting utc time in 'first': {it}", it); goto addObs; } var time = TimeZoneInfo.ConvertTimeFromUtc(uTim.Value, tz); time = DateTime.SpecifyKind(time, DateTimeKind.Local); if (string.IsNullOrEmpty(name) || value == null) { _logger.LogError("Invalid 'first' grouped obs (name/value) patient:{patientId}", patientId); goto addObs; } obs.First = new GroupedObservationObsValue( value, time, await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.First, name, value, min, max) ); } } if (groupedField.Result.Contains(GroupedObservationEnum.Result.Last)) { var g = !it.Contains("last") ? null : it.Get("last")?.AsBsonDocument; if (g != null) { var value = g.Get("value")?.ToObject(); var min = g.Get("min")?.ToDouble(); var max = g.Get("max")?.ToDouble(); var uTim = g.Get("time")?.ToUniversalTime(); if (!uTim.HasValue) { _logger.LogError("Error getting utc time in 'last': {it}", it); goto addObs; } var time = TimeZoneInfo.ConvertTimeFromUtc(uTim.Value, tz); time = DateTime.SpecifyKind(time, DateTimeKind.Local); if (string.IsNullOrEmpty(name) || value == null) { _logger.LogError("Invalid 'last' grouped obs (name/value) patient:{patientId}", patientId); goto addObs; } obs.Last = new GroupedObservationObsValue( value, time, await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.Last, name, value, min, max) ); } } if (groupedField.Result.Contains(GroupedObservationEnum.Result.Min)) { var g = !it.Contains("min") ? null : it.Get("min")?.AsBsonDocument; if (g != null) { var value = g.Get("value")?.ToObject(); var min = g.Get("min")?.ToDouble(); var max = g.Get("max")?.ToDouble(); var uTim = g.Get("time")?.ToUniversalTime(); if (!uTim.HasValue) { _logger.LogError("Error getting utc time in 'min': {it}", it); goto addObs; } var time = TimeZoneInfo.ConvertTimeFromUtc(uTim.Value, tz); time = DateTime.SpecifyKind(time, DateTimeKind.Local); if (string.IsNullOrEmpty(name) || value == null) { _logger.LogError("Invalid 'min' grouped obs (name/value) patient:{patientId}", patientId); goto addObs; } obs.Min = new GroupedObservationObsValue( value, time, await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.Min, name, value, min, max) ); } } if (groupedField.Result.Contains(GroupedObservationEnum.Result.Max)) { var g = !it.Contains("max") ? null : it.Get("max")?.AsBsonDocument; if (g != null) { var value = g.Get("value")?.ToObject(); var min = g.Get("min")?.ToDouble(); var max = g.Get("max")?.ToDouble(); var uTim = g.Get("time")?.ToUniversalTime(); if (!uTim.HasValue) { _logger.LogError("Error getting utc time in 'max': {it}", it); goto addObs; } var time = TimeZoneInfo.ConvertTimeFromUtc(uTim.Value, tz); time = DateTime.SpecifyKind(time, DateTimeKind.Local); if (string.IsNullOrEmpty(name) || value == null) { _logger.LogError("Invalid 'max' grouped obs (name/value) patient:{patientId}", patientId); goto addObs; } obs.Max = new GroupedObservationObsValue( value, time, await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.Max, name, value, min, max) ); } } if (groupedField.Result.Contains(GroupedObservationEnum.Result.Sum)) { var value = !it.Contains("sum") ? null : it.Get("sum")?.ToObject(); if (value != null && !string.IsNullOrEmpty(name)) { obs.Sum = new GroupedObservationObsValue( value, null, await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.Sum, name, value, null, null) ); } } if (groupedField.Result.Contains(GroupedObservationEnum.Result.Average)) { var value = !it.Contains("average") ? null : it.Get("average")?.ToObject(); if (value != null && !string.IsNullOrEmpty(name)) { obs.Average = new GroupedObservationObsValue( value, null, await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.Average, name, value, null, null) ); } } if (groupedField.Result.Contains(GroupedObservationEnum.Result.Count)) { var value = !it.Contains("count") ? null : it.Get("count")?.AsNullableInt32; if (value != null && !string.IsNullOrEmpty(name)) { obs.Count = new GroupedObservationObsValue( value, null, await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.Count, name, value, null, null) ); } } if (groupedField.Result.Contains(GroupedObservationEnum.Result.LastFilled)) { var g = !it.Contains("lastfilled") ? null : it.Get("lastfilled")?.AsBsonDocument; if (g != null) { var value = g.Get("value")?.ToObject(); var min = g.Get("min")?.ToDouble(); var max = g.Get("max")?.ToDouble(); var uTim = g.Get("time")?.ToUniversalTime(); if (!uTim.HasValue) { _logger.LogError("Error getting utc time in 'lastfilled': {it}", it); goto addObs; } var time = TimeZoneInfo.ConvertTimeFromUtc(uTim.Value, tz); time = DateTime.SpecifyKind(time, DateTimeKind.Local); if (value != null && !string.IsNullOrEmpty(name)) { obs.LastFilled = new GroupedObservationObsValue( value, time, await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.LastFilled, name, value, min, max) ); } } } if (groupedField.Result.Contains(GroupedObservationEnum.Result.HalfHour)) { var g = !it.Contains("halfhour") ? null : it.Get("halfhour")?.AsBsonDocument; if (g != null) { var value = g.Get("value")?.ToObject(); var min = g.Get("min")?.ToDouble(); var max = g.Get("max")?.ToDouble(); var uTim = g.Get("time")?.ToUniversalTime(); if (!uTim.HasValue) { _logger.LogError("Error getting utc time in 'halfhour': {it}", it); goto addObs; } var time = TimeZoneInfo.ConvertTimeFromUtc(uTim.Value, tz); time = DateTime.SpecifyKind(time, DateTimeKind.Local); if (value != null && !string.IsNullOrEmpty(name)) { obs.HalfHour = new GroupedObservationObsValue( value, time, await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.Last, name, value, min, max) ); } } } addObs: _logger.LogTrace("PatientId: {patientId}. Add to grouped observation: {obs}", patientId, obs); list.Add(obs); } return list; } // caché explícita por clave + TTL --- if (!cacheIsChecked) { var (key, ttl) = CacheKeys.GroupedObsKeyWithTtl(_cacheSettings, patientId, groupedField.Name ?? string.Empty); var observations = await _cacheService.GetOrSetObjectAsync( key, ComputeObservationsAsync, ttl, ct); return new GroupedObservation { PatientId = patientId, Group = groupedField.Group, Name = groupedField.Name, Observations = observations }; } // Re-cálculo forzado (sin tocar caché) var computed = await ComputeObservationsAsync(); return new GroupedObservation { PatientId = patientId, Group = groupedField.Group, Name = groupedField.Name, Observations = computed }; } public async Task GenerateGroupedObservation( ObjectId patientId, GroupedField groupedField, List wsgLastGroupedObservationObs, PatientObservation obs, string timeZoneId = "Romance Standard Time") { // Estos resultados requieren recálculo completo (no incremental) if (groupedField.Result.Contains(GroupedObservationEnum.Result.Average) || groupedField.Result.Contains(GroupedObservationEnum.Result.HalfHour) || groupedField.Result.Contains(GroupedObservationEnum.Result.Sum)) { return await GenerateGroupedObservation(patientId, groupedField, timeZoneId, true); } // Clave consistente con CacheDispatcher / backends var cacheKey = $"GroupedObs:{patientId}:{groupedField.Name}"; // Intentar leer caché actual var cached = await _cacheService.GetObjectAsync>(cacheKey, true); if (cached != null) { _logger.LogInformation("Return cached grouped obs (incremental)"); // Actualizar lista en memoria var updated = LocateCurrentObsInGroup(cached, obs, groupedField); // Persistir nueva lista (SET incremental) await _cacheService.SetObjectAsync(cacheKey, updated, true); return new GroupedObservation { PatientId = patientId, Group = groupedField.Group, Name = groupedField.Name, Observations = updated }; } // Si MISS, recálculo completo y salida return await GenerateGroupedObservation(patientId, groupedField, timeZoneId, true); } public async Task> FindLastObservations(ObjectId patientId, int num = 2, List? filterObservations = null) { var result = await _observationRepository.AggregatedPatientLastObservations(patientId, num, filterObservations); return result; } public async Task CreateNextEmptyObs(WsSubscriberGrouped ws) { var gf = new GroupedField { Max = ws.Max, Since = ws.Since, StartTimeShift = ws.StartTimeShift, Regularity = ws.Regularity, Result = ws.Result, Names = ws.Names, Name = ws.Names.First(), Group = ws.Group.FirstOrDefault().Value }; // Mantenemos el uso del HashCode var currentObsList = await _cacheService.GetObjectAsync>(ws.HashCode, false); if (currentObsList is { Count: > 0 }) { var orderCurrentObsList = currentObsList.OrderBy(c => c.Time).ToList(); foreach (var name in gf.Names) { var obs = orderCurrentObsList.LastOrDefault(c => c.Name == name) ?? orderCurrentObsList.LastOrDefault(); if (obs != null) { var (timesLeft, multiplierFactor) = TimesLeftToCreateEmptyObs(obs, gf); for (var i = 1; i <= timesLeft; i++) orderCurrentObsList.Add(CreateNextObs(obs, gf, i * multiplierFactor)); } } // Ajuste de ventana temporal a mostrar según Regularity DateTime dateLimit; var requireMinutes = false; var requireSeconds = false; switch (gf.Regularity) { case GroupedObservationEnum.Regularity.Minute: dateLimit = new DateTime( DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, DateTime.Now.Hour, DateTime.Now.Minute, 0 ).AddMinutes(gf.Max * -1); requireMinutes = true; break; case GroupedObservationEnum.Regularity.Second: dateLimit = new DateTime( DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, DateTime.Now.Hour, DateTime.Now.Minute, DateTime.Now.Second ).AddSeconds(gf.Max * -1); requireMinutes = true; requireSeconds = true; break; default: dateLimit = new DateTime( DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, DateTime.Now.Hour, 0, 0 ).AddHours(gf.Max * -1); break; } var cacheResult = orderCurrentObsList .Where(c => new DateTime( c.Time.Year, c.Time.Month, c.Time.Day, c.Time.Hour, requireMinutes ? c.Time.Minute : 0, requireSeconds ? c.Time.Second : 0 ) >= dateLimit) .ToList(); // Persistimos la lista incrementada en la misma clave para no romper el flujo actual. await _cacheService.SetObjectAsync(ws.HashCode, cacheResult, false); return new GroupedObservation { PatientId = ws.PatientId, Group = gf.Group, Name = gf.Name, Observations = cacheResult }; } // Si no hay caché o hay MISS → recálculo completo (sin usar caché en esta ruta). return await GenerateGroupedObservation(ws.PatientId, gf, ws.TimeZoneId, true); } // PRIVATE: High-level composition steps public List FillHours(List result, GroupedField groupedField) { var now = DateTime.UtcNow; //var resultNumber = groupedField.Max; var names = groupedField.Names.Count > 1 ? groupedField.Names : [groupedField.Name ?? string.Empty]; if (result.Count >= groupedField.Max * names.Count) return result; //We have data from each hour/day/minute, dont need to calc. List fillResult = []; //var first = result.FirstOrDefault(); foreach (var name in names) { var filteredDocuments = result.Where(doc => doc["_id"]["name"] == name).ToList(); if (filteredDocuments.Count >= groupedField.Max) { filteredDocuments = filteredDocuments.OrderBy(e => e.Get("time")?.ToLocalTime()).ToList(); fillResult.AddRange(filteredDocuments); continue; } var expectedDate = DateTime.MinValue; var inverseCounter = 0; for (var i = groupedField.Max; i != 0; i--) { var expectedMatch = true; var value = filteredDocuments.ElementAtOrDefault(inverseCounter); DateTime? time = null; if (value == null) expectedMatch = false; else //Estamos recogiendo la hora en UTC time = value.Get("time")?.ToUniversalTime(); switch (groupedField.Regularity) { case GroupedObservationEnum.Regularity.Day: expectedDate = now.AddDays(-i); if (!time.HasValue || expectedDate.Day != time.Value.Day) expectedMatch = false; break; case GroupedObservationEnum.Regularity.Hour: case GroupedObservationEnum.Regularity.Shift: expectedDate = now.AddHours(-i); if (!time.HasValue || expectedDate.Hour != time.Value.Hour) expectedMatch = false; break; case GroupedObservationEnum.Regularity.Minute: expectedDate = now.AddMinutes(-i); if (!time.HasValue || expectedDate.Minute != time.Value.Minute) expectedMatch = false; break; case GroupedObservationEnum.Regularity.Second: expectedDate = now.AddSeconds(-i); if (!time.HasValue || expectedDate.Second != time.Value.Second) expectedMatch = false; break; } if (!expectedMatch) { var newEntry = new BsonDocument { { "_id", new BsonDocument { { "year", expectedDate.Year }, { "month", expectedDate.Month }, { "day", expectedDate.Day }, { "hour", expectedDate.Hour }, { "minute", expectedDate.Minute }, { "name", groupedField.Name } } }, { "time", new BsonDateTime(expectedDate) }, //TODO loop del result entero //{ Result.HalfHour.ToString().ToLower(), new BsonDocument { { "value", 0 }, { "min",0 }, { "max",0 }, { "time", new BsonDateTime(expectedDate) } } } // { groupedField.regularity.ToString().ToLower(), new BsonDocument { { "value", 0 }, { "min",0 }, { "max",0 }, { "time", new BsonDateTime(expectedDate) } } } { "isFilled", true } //Marcamos como hora añadida }; foreach (var r in groupedField.Result) switch (r) { case GroupedObservationEnum.Result.LastFilled: case GroupedObservationEnum.Result.LastAll: continue; case GroupedObservationEnum.Result.Sum: case GroupedObservationEnum.Result.Average: case GroupedObservationEnum.Result.Count: newEntry.Add(new BsonElement(r.ToString().ToLower(), 0)); continue; case GroupedObservationEnum.Result.HalfHour: newEntry.Add("all", new BsonArray { new BsonDocument { { "value", 0 }, { "min", 0 }, { "max", 0 }, { "time", new BsonDateTime(expectedDate) } } }); continue; default: newEntry.Add(r.ToString().ToLower(), new BsonDocument { { "value", 0 }, { "min", 0 }, { "max", 0 }, { "time", new BsonDateTime(expectedDate) } }); continue; } if (groupedField.Result.Count != 1 || !groupedField.Result.Contains(GroupedObservationEnum.Result.LastFilled)) filteredDocuments.Add(newEntry); filteredDocuments = filteredDocuments.OrderBy(e => e.Get("time")?.ToLocalTime()).ToList(); } inverseCounter++; } if (groupedField.Max <= filteredDocuments.Count) { var value = filteredDocuments.ElementAtOrDefault(inverseCounter); if (value == null) { var newEntry = new BsonDocument { { "_id", new BsonDocument { { "year", DateTime.UtcNow.Year }, { "month", DateTime.UtcNow.Month }, { "day", DateTime.UtcNow.Day }, { "hour", DateTime.UtcNow.Hour }, { "minute", DateTime.UtcNow.Minute }, { "name", groupedField.Name } } }, { "time", new BsonDateTime(DateTime.UtcNow) }, //TODO loop del result entero { "isFilled", true } //Marcamos como hora añadida }; foreach (var r in groupedField.Result) switch (r) { case GroupedObservationEnum.Result.LastFilled: case GroupedObservationEnum.Result.LastAll: continue; case GroupedObservationEnum.Result.Sum: case GroupedObservationEnum.Result.Average: case GroupedObservationEnum.Result.Count: newEntry.Add(new BsonElement(r.ToString().ToLower(), 0)); continue; case GroupedObservationEnum.Result.HalfHour: newEntry.Add("all", new BsonArray { new BsonDocument { { "value", 0 }, { "min", 0 }, { "max", 0 }, { "time", new BsonDateTime(expectedDate) } } }); continue; default: newEntry.Add(r.ToString().ToLower(), new BsonDocument { { "value", 0 }, { "min", 0 }, { "max", 0 }, { "time", new BsonDateTime(expectedDate) } }); continue; } if (groupedField.Result.Count != 1 || !groupedField.Result.Contains(GroupedObservationEnum.Result.LastFilled)) filteredDocuments.Add(newEntry); filteredDocuments = filteredDocuments.OrderBy(e => e.Get("time")?.ToLocalTime()).ToList(); } fillResult.AddRange(filteredDocuments); } } fillResult = fillResult.OrderBy(e => e.Get("time")?.ToLocalTime()).ToList(); return fillResult; } public async Task> CalculateLastFilledObservations(List result, GroupedField groupedField, ObjectId patientId) { List resultToReturn = []; if (groupedField.Names.Any()) foreach (var name in groupedField.Names) { var resultFiltedByName = result.FindAll(b => b.Get("_id")?.AsBsonDocument.Get("name") == name); var resultCalculated = await LastFilledCalc(resultFiltedByName, groupedField, patientId, name); resultToReturn.AddRange(resultCalculated); } else return await LastFilledCalc(result, groupedField, patientId, groupedField.Name); return resultToReturn; } private async Task> LastFilledCalc(List result, GroupedField groupedField, ObjectId patientId, string? name) { var now = DateTime.UtcNow; if (result.Count == groupedField.Max) return result; //We have data from each hour/day/minute, dont need to calc. var first = result.FirstOrDefault(); object? lastObservationValue = null; var expectedFirstDate = groupedField.Regularity switch { GroupedObservationEnum.Regularity.Day => now.AddDays(-groupedField.Max), GroupedObservationEnum.Regularity.Hour => now.AddHours(-groupedField.Max), GroupedObservationEnum.Regularity.Minute => now.AddMinutes(-groupedField.Max), GroupedObservationEnum.Regularity.Second => now.AddSeconds(-groupedField.Max), _ => DateTime.MinValue }; //first > fecha minima tenemos que buscar el ultimo valor if (first == null || first.Get("time")?.ToUniversalTime().CompareTo(expectedFirstDate) > 0) { var lastObservation = await _observationRepository.FindLastObservationBeforeDate(patientId, groupedField.Name, expectedFirstDate); if (lastObservation != null) lastObservationValue = lastObservation.Value; } var expectedDate = DateTime.MinValue; var inverseCounter = 0; for (var i = groupedField.Max; i != 0; i--) { var expectedMatch = true; var value = result.ElementAtOrDefault(inverseCounter); DateTime? time = null; if (value == null) expectedMatch = false; else time = value.Get("time")?.ToUniversalTime(); switch (groupedField.Regularity) { case GroupedObservationEnum.Regularity.Day: expectedDate = now.AddDays(-i); if (!time.HasValue || expectedDate.Day != time.Value.Day) expectedMatch = false; break; case GroupedObservationEnum.Regularity.Hour: expectedDate = now.AddHours(-i); if (!time.HasValue || expectedDate.Hour != time.Value.Hour) expectedMatch = false; break; case GroupedObservationEnum.Regularity.Minute: expectedDate = now.AddMinutes(-i); if (!time.HasValue || expectedDate.Minute != time.Value.Minute) expectedMatch = false; break; case GroupedObservationEnum.Regularity.Second: expectedDate = now.AddSeconds(-i); if (!time.HasValue || expectedDate.Second != time.Value.Second) expectedMatch = false; break; } if (!expectedMatch) { var lastResult = result.ElementAtOrDefault(inverseCounter - 1); object? lastFilledValue; if (lastResult == null) { lastFilledValue = lastObservationValue; } else { var lastfilled = GroupedObservationEnum.Result.LastFilled.ToString().ToLower(); lastFilledValue = lastResult.Get(lastfilled)?.AsBsonDocument.Get("value"); } lastFilledValue ??= 0; var newEntry = new BsonDocument { { "_id", new BsonDocument { { "year", expectedDate.Year }, { "month", expectedDate.Month }, { "day", expectedDate.Day }, { "hour", expectedDate.Hour }, { "minute", expectedDate.Minute }, { "name", name } } }, { "time", new BsonDateTime(expectedDate) }, { GroupedObservationEnum.Result.LastFilled.ToString().ToLower(), new BsonDocument { { "value", lastFilledValue.ToString() }, { "min", 0 }, { "max", 0 }, { "time", new BsonDateTime(expectedDate) } } } }; result.Add(newEntry); result = result.OrderBy(e => e.Get("time")?.ToLocalTime()).ToList(); } inverseCounter++; } return result; } public List CalculateShiftObservations(List shiftGroupObservations, GroupedField groupedField) { var shiftGrouped = new List(); if (groupedField.Result.Contains(GroupedObservationEnum.Result.Sum)) { var shiftGroups = shiftGroupObservations.GroupBy(shift => new { V1 = shift.GetElement("shift").ToString(), V = shift.GetElement("day").ToString() }) .Select(s => s.ToList()) .ToList(); shiftGroups.ForEach(shiftGroup => { var shiftSum = shiftGroup.Select(shift => shift.Get("sum")?.ToInt32()).Sum(); var shiftToAdd = shiftGroup.FirstOrDefault(); shiftToAdd?.SetElement(new BsonElement("sum", shiftSum)); if (shiftGroup.Select(shift => shift.Get("isFilled")).Any(s => s == false)) shiftToAdd?.SetElement(new BsonElement("isFilled", false)); if (shiftToAdd != null) shiftGrouped.Add(shiftToAdd); }); return shiftGrouped; } return shiftGroupObservations; } //MIGUEL: EJEMPLO SI LAS 12 TIENEN 4 VALORES 12:15 => 5, 12:20 => 10, 12:45 => 20 Y LAS 13 NO TIENE VALOR. 12 DEBERÍA 5 Y 13 DEBERÍA SER // ACTUALMENTE A LAS 12 MOSTRARIA EL ULTIMO DE LA MEDIA HORA Y A LAS 13? public List CalculateHalfHourObservations(List result) { //HalfHour añadir que se rellenen las horas que no existen for (var i = 0; i < result.Count; i++) { var document = result[i]; BsonDocument? nextDocument = null; if (i + 1 < result.Count) nextDocument = result[i + 1]; document.TryGetValue("all", out var docvalues); List groups = []; if (docvalues != null) groups = docvalues.AsBsonArray.ToList().GroupBy(x => { var stamp = x.AsBsonDocument.Get("time")?.ToLocalTime(); if (!stamp.HasValue) return null; stamp = stamp.Value.AddMinutes(-(stamp.Value.Minute % 30)); stamp = stamp.Value.AddMilliseconds(-stamp.Value.Millisecond - 1000 * stamp.Value.Second); return stamp; }).Select(g => g.OrderByDescending(c => c.AsBsonDocument.Get("time")?.ToLocalTime()).FirstOrDefault()) .ToList(); if (groups.Count > 0) { if (nextDocument == null) { document.Set("halfhour", groups.Last()); break; } if (groups.Count > 1) { var nextDocumentValues = nextDocument.Get("all")?.AsBsonArray.ToList(); if (nextDocumentValues == null || nextDocumentValues.Count == 0 || (nextDocumentValues.Count == 1 && nextDocument.Get("all")?.AsBsonArray.ToList()[0].AsBsonDocument .Get("value")?.ToString() == "0")) //El siguiente registro no tiene tomas { nextDocument.Set("halfhour", groups.LastOrDefault()); document.Set("halfhour", groups.FirstOrDefault()); } else { document.Set("halfhour", groups.LastOrDefault()); } } else { if (document.Get("halfhour")?.AsBsonDocument == null) document.Set("halfhour", groups.LastOrDefault()); } } else { if (!document.TryGetValue("halfhour", out _)) document.Set("halfhour", new BsonDocument()); } } return result; } public List GenerateShiftObservations(List result, GroupedField groupedField) { result.ForEach(item => { DateTime? obsDateTime = null; var id = item.Get("_id")?.AsBsonDocument; if (id == null) return; var timeStr = id.Get("hour")?.ToString()?.PadLeft(2, '0') ?? "00"; if (id.TryGetValue("minute", out _)) { timeStr += ":" + id.Get("minute")?.ToString()?.PadLeft(2, '0'); if (DateTime.TryParseExact(timeStr, "HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out var r1)) obsDateTime = r1; } else { if (DateTime.TryParseExact(timeStr, "HH", CultureInfo.InvariantCulture, DateTimeStyles.None, out var r2)) obsDateTime = r2; } obsDateTime = obsDateTime?.ToLocalTime(); int shift; int day; //Return all dates in shift array before obsDateTime, and take last, that will be the closest shift date. var expectedHour = obsDateTime?.Hour.ToString().Length < 2 ? $"0{obsDateTime.Value.Hour}" : obsDateTime?.Hour.ToString(); var expectedMinutes = obsDateTime?.Minute.ToString().Length < 2 ? $"0{obsDateTime.Value.Minute}" : obsDateTime?.Minute.ToString(); var shiftSelected = groupedField.StartTimeShift.LastOrDefault (i => DateTime.ParseExact(i, "HH:mm", CultureInfo.InvariantCulture).Ticks <= DateTime .ParseExact($"{expectedHour}:{expectedMinutes}", "HH:mm", CultureInfo.InvariantCulture) .Ticks); var havemin = id.TryGetValue("minute", out var minuteV); var year = id.Get("year")?.ToInt32(); var month = id.Get("month")?.ToInt32(); var dayy = id.Get("day")?.ToInt32(); var hour = id.Get("hour")?.ToInt32(); if (!year.HasValue || !month.HasValue || !dayy.HasValue || !hour.HasValue) { _logger.LogError("Error getting dateTime from Bson: {id}", id); return; } var dateUtc = new DateTime(year.Value, month.Value, dayy.Value, hour.Value, havemin ? minuteV.ToInt32() : 0, 0); if (!groupedField.StartTimeShift.Any()) { _logger.LogError( "Error generating shift observations. StartTimeShift List in null or empty. {groupedField}", groupedField); return; } if (shiftSelected == null) { day = dateUtc.ToLocalTime().AddDays(-1).Day; shift = groupedField.StartTimeShift.Count - 1; } else { day = dateUtc.ToLocalTime().Day; shift = groupedField.StartTimeShift.IndexOf(shiftSelected); } item.Add(new BsonElement("day", day)); item.Add(new BsonElement("shift", shift)); }); return result; } // PRIVATE: Incremental item adjustment private List LocateCurrentObsInGroup(List group, PatientObservation obs, GroupedField groupedField) { // Check if we need to modify the current value or add new one var specifyMinutes = groupedField.Regularity is GroupedObservationEnum.Regularity.Minute or GroupedObservationEnum.Regularity.Second; var specifySeconds = groupedField.Regularity == GroupedObservationEnum.Regularity.Second; var obsTime = new DateTime(obs.Time.Year, obs.Time.Month, obs.Time.Day, obs.Time.Hour, specifyMinutes ? obs.Time.Minute : 0, specifySeconds ? obs.Time.Second : 0); var obsIndexInThisHour = group.FindIndex(c => new DateTime(c.Time.Year, c.Time.Month, c.Time.Day, c.Time.Hour, specifyMinutes ? c.Time.Minute : 0, specifySeconds ? c.Time.Second : 0) == obsTime && c.Name == obs.Name); if (obsIndexInThisHour >= 0) { // Obs to modify found var valueIsParsed = double.TryParse(obs.Value.ToString(), out var valueParsed); if (!valueIsParsed) return group; group[obsIndexInThisHour].IsFilled = false; group[obsIndexInThisHour].Time = obsTime; foreach (var result in groupedField.Result) switch (result) { case GroupedObservationEnum.Result.Max: var valueMaxIsParsed = double.TryParse(group[obsIndexInThisHour].Max?.Value.ToString(), out var valueMaxParsed); if (!valueMaxIsParsed) continue; //actual value is higher, is relevant if (group[obsIndexInThisHour].Max?.Value != null && valueParsed > valueMaxParsed) { group[obsIndexInThisHour].Max!.Value = obs.Value; group[obsIndexInThisHour].Max!.Time = obs.Time; group[obsIndexInThisHour].Max!.Type = obs.Status; group[obsIndexInThisHour].Max!.Type = obs.Status; } break; case GroupedObservationEnum.Result.Min: var valueMinIsParsed = double.TryParse(group[obsIndexInThisHour].Max?.Value.ToString(), out var valueMinParsed); if (!valueMinIsParsed) continue; if (group[obsIndexInThisHour].Max?.Value != null && valueParsed < valueMinParsed) { group[obsIndexInThisHour].Min!.Value = obs.Value; group[obsIndexInThisHour].Min!.Time = obs.Time; group[obsIndexInThisHour].Min!.Type = obs.Status; } break; case GroupedObservationEnum.Result.Last: if (group[obsIndexInThisHour].Last?.Time < obs.Time) { group[obsIndexInThisHour].Last!.Value = obs.Value; group[obsIndexInThisHour].Last!.Time = obs.Time; group[obsIndexInThisHour].Last!.Type = obs.Status; } break; case GroupedObservationEnum.Result.LastFilled: if (group[obsIndexInThisHour].LastFilled?.Time < obs.Time) { group[obsIndexInThisHour].LastFilled!.Value = obs.Value; group[obsIndexInThisHour].LastFilled!.Time = obs.Time; group[obsIndexInThisHour].LastFilled!.Type = obs.Status; } break; } } else { // Delete obs autofilled and in time of the new obs var obsIndexInThisHourToRemove = group.FindIndex(c => new DateTime(c.Time.Year, c.Time.Month, c.Time.Day, c.Time.Hour, specifyMinutes ? c.Time.Minute : 0, specifySeconds ? c.Time.Second : 0) == obsTime && c.IsFilled); if (obsIndexInThisHourToRemove >= 0) group.RemoveAt(obsIndexInThisHourToRemove); // Obs to modify not found add it var ngo = new GroupedObservationObs { Name = obs.Name ?? string.Empty, MinAlert = obs.Min, MaxAlert = obs.Max, Time = obsTime, IsFilled = false, ShiftDate = obs.Time, Shift = GetShift(obs.Time, groupedField.StartTimeShift) }; foreach (var result in groupedField.Result) switch (result) { case GroupedObservationEnum.Result.Max: ngo.Max = new GroupedObservationObsValue(obs.Value, obs.Time, obs.Status); break; case GroupedObservationEnum.Result.Min: ngo.Min = new GroupedObservationObsValue(obs.Value, obs.Time, obs.Status); break; case GroupedObservationEnum.Result.Last: ngo.Last = new GroupedObservationObsValue(obs.Value, obs.Time, obs.Status); break; case GroupedObservationEnum.Result.LastFilled: ngo.LastFilled = new GroupedObservationObsValue(obs.Value, obs.Time, obs.Status); break; } group.Add(ngo); } // Check if we have more obs than we require to show DateTime dateLimit; switch (groupedField.Regularity) { case GroupedObservationEnum.Regularity.Minute: dateLimit = new DateTime(obs.Time.Year, obs.Time.Month, obs.Time.Day, obs.Time.Hour, obs.Time.Minute, 0) .AddMinutes(groupedField.Max * -1); break; case GroupedObservationEnum.Regularity.Second: dateLimit = new DateTime(obs.Time.Year, obs.Time.Month, obs.Time.Day, obs.Time.Hour, obs.Time.Minute, obs.Time.Second).AddSeconds(groupedField.Max * -1); break; default: dateLimit = new DateTime(obs.Time.Year, obs.Time.Month, obs.Time.Day, obs.Time.Hour, 0, 0).AddHours( groupedField.Max * -1); break; } return group.Where(c => new DateTime(c.Time.Year, c.Time.Month, c.Time.Day, c.Time.Hour, specifyMinutes ? c.Time.Minute : 0, specifySeconds ? c.Time.Second : 0) >= dateLimit).ToList(); } // PRIVATE: Low-level utilities private int? GetShift(DateTime currentObsDate, List groupedFieldStartTimeShift) { if (groupedFieldStartTimeShift.Count == 0) return null; var listTime = GetTimeSpan(groupedFieldStartTimeShift); var dateTimeSpan = currentObsDate.TimeOfDay; for (var i = 0; i < listTime.Count; i++) { var nextIndex = (i + 1) % listTime.Count; var nextTimeSpan = listTime[nextIndex]; var currentTimeSpan = listTime[i]; if (dateTimeSpan >= currentTimeSpan && dateTimeSpan < nextTimeSpan) return i; } return -1; } private List GetTimeSpan(List groupedFieldStartTimeShift) { List timeSpans = []; timeSpans.AddRange(groupedFieldStartTimeShift.Select(TimeSpan.Parse)); List newTimeSpans = []; for (var i = 0; i < timeSpans.Count; i++) { var nextIndex = (i + 1) % timeSpans.Count; var nextTimeSpan = timeSpans[nextIndex]; var currentTimeSpan = timeSpans[i]; var addedTimeSpan = currentTimeSpan.Add(TimeSpan.FromMinutes(1)); newTimeSpans.Add(addedTimeSpan < nextTimeSpan ? addedTimeSpan : nextTimeSpan); } return newTimeSpans; } private async Task GroupedObservationStatus(GroupedField groupedField, GroupedObservationEnum.Result result, string name, object value, double? min, double? max) { return await _configObservationService.GroupedObservationStatus(groupedField, result, name, value, min, max); } private (int timesLeft, int multiplierFactor) TimesLeftToCreateEmptyObs(GroupedObservationObs obs, GroupedField gf) { var currentDateTime = DateTime.Now; var timeElapsed = currentDateTime - obs.Time; double timeToCurrent; var multipliesFactor = 1; // Calcula el tiempo transcurrido en la unidad deseada switch (gf.Regularity) { case GroupedObservationEnum.Regularity.Second: timeToCurrent = timeElapsed.TotalSeconds; break; case GroupedObservationEnum.Regularity.Minute: timeToCurrent = timeElapsed.TotalMinutes; multipliesFactor = 60; break; case GroupedObservationEnum.Regularity.Day: timeToCurrent = timeElapsed.TotalDays; multipliesFactor = 86400; break; default: timeToCurrent = timeElapsed.TotalHours; multipliesFactor = 3600; break; } var timesLeft = (int)Math.Ceiling(timeToCurrent); return (timesLeft, multipliesFactor); } private GroupedObservationObs CreateNextObs(GroupedObservationObs sourceObs, GroupedField gf, int seconsToAdd) { var lastObs = new GroupedObservationObs { Time = sourceObs.Time.AddSeconds(seconsToAdd), Name = sourceObs.Name, IsFilled = true }; //Log.Debug("Result list:", string.Join(',', Result)); //Se rellena el tiempo con la hora de la obs anterior por que no hay dato de la actual GroupedObservationObsValue defaultValue = new(0, sourceObs.Time); foreach (var r in gf.Result) { //Log.Debug("Processing {r}", r.ToString()); //var props = typeof(GroupedObservationObs).GetProperties(); //Log.Debug("GroupedObservationObs Poperties list: {list}", string.Join(',', props.ToList())); var propInfo = typeof(GroupedObservationObs).GetProperty(r.ToString()); if (propInfo == null) { _logger.LogError("Error Creating Next Obs. Getting property: {R} not found. SourceObs:{Obs}", r.ToString(), sourceObs); continue; } propInfo.SetValue(lastObs, defaultValue); var gObsValue = propInfo.GetValue(sourceObs); if (gObsValue == null) { _logger.LogWarning("Error Creating Next Obs.Getting property value: {R} not found. SourceObs:{Obs}", r.ToString(), sourceObs); continue; } var prevValue = (GroupedObservationObsValue)gObsValue; prevValue.Time = sourceObs.Time; propInfo.SetValue(sourceObs, prevValue); } if (gf.Regularity == GroupedObservationEnum.Regularity.Shift) { lastObs.ShiftDate = lastObs.Time; lastObs.Shift = GetShift(lastObs.ShiftDate, gf.StartTimeShift); } return lastObs; } }