Files
adas-core/adas-core.Application/Services/DisplayService.cs
T

966 lines
49 KiB
C#

using adas_core.Application.Exceptions;
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.AppSettings;
using adas_core.Domain.Models.DTO;
using adas_core.Domain.Models.DTO.Display;
using adas_core.Domain.Models.Filter;
using adas_core.Domain.Models.Masters;
using adas_core.Domain.Models.MongoModels;
using adas_core.Domain.Models.Responses;
using adas_core.Domain.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
namespace adas_core.Application.Services;
/// <summary>
/// Implements <see cref="IDisplayService"/>, coordinating display management operations
/// across data access (<see cref="IDisplayRepository"/>), configuration (<see cref="IDisplayConfigService"/>),
/// authentication (<see cref="IAuthService"/>), audit (<see cref="ILocalAuditService"/>),
/// and caching (<see cref="ICacheService"/>) concerns.
/// </summary>
/// <!-- aidoc:v1 sig=010754e -->
public class DisplayService(
IDisplayRepository displayRepository,
IPointOfCareService pointOfCareService,
Lazy<IUnitService> unitService,
IDisplayConfigService displayConfigService,
ISubscribersService subscribersService,
IClientMessageService clientMessageService,
IUserRepository userRepository,
IAuthService authorityService,
ILogger<DisplayService> logger,
IHttpContextAccessor httpContextAccessor,
ILocalAuditService auditService,
Lazy<IPermissionService> permissionService,
ICacheService cacheService,
IOptions<CacheSettings> cacheSettings)
: IDisplayService
{
private readonly CacheSettings? _cacheSettings = cacheSettings.Value;
#region Methods
#region Create
/// <summary>
/// Inserts a new <see cref="Display"/> using the default configuration for its type.
/// If no default configuration exists for the display type, a <see cref="NotFoundException"/> is thrown.
/// An audit log entry is created after the display is persisted.
/// </summary>
/// <param name="display">The display to insert. Its <c>DisplayConfigId</c> is assigned from the resolved default configuration.</param>
/// <returns>The inserted <see cref="Display"/> with its <c>DisplayConfigId</c> populated.</returns>
/// <exception cref="NotFoundException">Thrown when no default configuration is found for the specified display type.</exception>
/// <!-- aidoc:v1 sig=9e0b693 body=032ec82 -->
public async Task<Display> InsertOne(Display display)
{
var defaultConfig = await displayConfigService.GetDefaultConfig(display.Type) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
display.DisplayConfigId = defaultConfig.Id;
await displayRepository.InsertOneAsync(display);
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, display);
return display;
}
/// <summary>
/// Inserts a test Display record into the repository and records a corresponding audit log entry using the current HTTP context user.
/// </summary>
/// <returns>The newly created <see cref="Display"/> entity.</returns>
/// <!-- aidoc:v1 sig=4ec9793 body=0fdb428 -->
public async Task<Display> InsertOneTest()
{
var d = new Display
{
Name = "DisplayTEST",
UnitId = new ObjectId("65ba5f89d5ba8e273cf9cd96"),
DisplayConfigId = new ObjectId("45ba5f89d5ba8e273cf9cd96")
};
await displayRepository.InsertOneAsync(d);
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, d);
return d;
}
#endregion
#region Read
/// <summary>
/// Retrieves all displays from the repository and maps them to a compact representation.
/// </summary>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="DisplayMinimalDto"/> with the mapped display data.</returns>
/// <!-- aidoc:v1 sig=d065c6f body=4d0461d -->
public async Task<List<DisplayMinimalDto>> GetAllCompact()
{
var result = await displayRepository.GetAll();
List<DisplayMinimalDto> listToReturn = [];
foreach (var res in result) listToReturn.Add(new DisplayMinimalDto(res));
return listToReturn;
}
/// <summary>
/// Retrieves all display items, optionally filtered by the specified user name.
/// </summary>
/// <param name="userName">The user name used to filter the display items, or <see langword="null"/> to retrieve all items.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of <see cref="Display"/> items.</returns>
/// <exception cref="NotImplementedException">The method has not been implemented yet.</exception>
/// <!-- aidoc:v1 sig=7a39576 body=bfa6f2f -->
public Task<List<Display>> GetAll(string? userName)
{
throw new NotImplementedException();
}
/// <summary>
/// Retrieves a paginated list of <see cref="Display"/> items along with the total document count, applying page number and page size from the provided filter.
/// </summary>
/// <param name="filter">The pagination filter containing the page number and page size used to determine the slice of results to return.</param>
/// <returns>A <see cref="Task{PaginationResponse{Display}}"/> containing the requested page of displays, the current page number, the page size, and the total number of documents.</returns>
/// <!-- aidoc:v1 sig=5ace491 body=5778d73 -->
public async Task<PaginationResponse<Display>> GetPaginatedDisplays(PaginationFilter filter)
{
var result = displayRepository.GetPaginatedDisplays(filter);
var count = await result.CountDocumentsAsync();
var data = await result.Skip((filter.PageNumber - 1) * filter.PageSize)
.Limit(filter.PageSize)
.ToCursorAsync();
var dataList = await data.ToListAsync();
return new PaginationResponse<Display>(dataList, filter.PageNumber, filter.PageSize, count);
}
/// <summary>
/// Retrieves all displays accessible to the specified user, together with their associated permissions, by resolving the user's authorizations (both unit-scoped and display-scoped).
/// Returns an empty list when the username is null, when the user cannot be found, or when no authorizations are available; throws an exception if permissions for a unit-scoped display cannot be resolved.
/// </summary>
/// <param name="userName">The username whose displays should be retrieved; when null, the method returns an empty list.</param>
/// <returns>A task that yields a list of <see cref="DisplayWithPermissionsDto"/> containing the displays the user can access along with their permissions.</returns>
/// <exception cref="ForbbidenException">Thrown when permissions for a unit-scoped display cannot be obtained for the user.</exception>
/// <!-- aidoc:v1 sig=3460ae9 body=04cbe7e -->
public async Task<List<DisplayWithPermissionsDto>> GetAllByUser(string? userName)
{
var start = DateTime.Now;
var listToReturn = new List<DisplayWithPermissionsDto>();
if (userName == null) return listToReturn;
var user = await userRepository.GetByUserAndAuthoritesName(userName);
if (user == null) return listToReturn;
if (user.Authorization == null || user.Authorization.Count == 0)
user.Authorization = await authorityService.GetUserAuthorities(user.Id);
if (user.Authorization == null)
return listToReturn;
foreach (var e in user.Authorization)
if (e.UnitId != null)
{
var isParsed = ObjectId.TryParse(e.UnitId, out var dId);
if (isParsed)
{
var dis = await displayRepository.GetByUnitId(dId);
foreach (var display in dis)
{
var toAdd = await GetInfo(display.Id, userName, user.Authorization, null, false, false, false, false);
if (toAdd != null)
{
var newDto = new DisplayWithPermissionsDto
{
Display = toAdd,
Permissions =
await permissionService.Value.GetPermissionsForUnit(dId.ToString(), user) ??
throw new ForbbidenException(HttpEnum.ErrorMessage.ForbiddenNoPermission)
};
listToReturn.Add(newDto);
}
}
}
}
else if (e.DisplayId != null && !FindDisplayInPerms(e.DisplayId, listToReturn))
{
var isParsed = ObjectId.TryParse(e.DisplayId, out var dId);
if (isParsed)
{
var toAdd = await GetInfo(dId, userName, user.Authorization, null, false, false, false, false);
if (toAdd != null)
{
var newDto = new DisplayWithPermissionsDto
{
Display = toAdd,
Permissions = await permissionService.Value.GetPermissionsForDisplay(toAdd, user)
};
listToReturn.Add(newDto);
}
}
}
var end = DateTime.Now;
logger.LogDebug("Finished GetAllByUser Displays for user {user} in {TotalSeconds:F1} seconds", userName,
(end - start).TotalSeconds);
return listToReturn;
}
/// <summary>
/// Determines whether a display with the specified identifier exists in the provided collection of display permissions.
/// The check safely skips entries whose <c>Display</c> reference is null before comparing the display identifier.
/// </summary>
/// <param name="displayId">The identifier of the display to look up, compared as a string.</param>
/// <param name="perms">The collection of display-with-permissions entries to search through.</param>
/// <returns><c>true</c> if a non-null display with a matching identifier is found; otherwise, <c>false</c>.</returns>
/// <!-- aidoc:v1 sig=3e72746 body=367999b -->
private static bool FindDisplayInPerms(string displayId, List<DisplayWithPermissionsDto> perms)
{
return perms.Any(p => p.Display != null && p.Display.Id.ToString() == displayId);
}
/// <summary>
/// Retrieves all displays associated with the specified display type by resolving the matching display configurations and loading their corresponding displays.
/// Each returned display is enriched with its parent configuration, and configurations without associated displays are skipped.
/// </summary>
/// <param name="type">The display type used to filter the display configurations.</param>
/// <returns>A list of displays matching the specified type, each with its related configuration assigned; an empty list is returned when no displays are found.</returns>
/// <!-- aidoc:v1 sig=37f36bb body=aac9b72 -->
public async Task<List<Display>> GetByType(DisplayConfigEnums.DisplayType type)
{
var configs = await displayConfigService.GetByType(type);
var listToReturn = new List<Display>();
foreach (var config in configs)
{
var displayToAdd = await displayRepository.GetByConfigId(config.Id);
displayToAdd.ForEach(c => c.DisplayConfig = config);
if (!displayToAdd.IsNullOrEmpty()) listToReturn.AddRange(displayToAdd);
}
return listToReturn;
}
/// <summary>
/// Retrieves a list of displays associated with the specified point of care.
/// </summary>
/// <param name="pointOfCare">The point of care used to filter the displays.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the list of displays matching the specified point of care.</returns>
/// <!-- aidoc:v1 sig=ad8af85 body=9447333 -->
public async Task<List<Display>> GetByPointOfCare(PointOfCare pointOfCare)
{
return await displayRepository.GetByPointOfCare(pointOfCare);
}
/// <summary>
/// Retrieves the list of displays associated with the specified configuration identifier by delegating to the display repository.
/// </summary>
/// <param name="configId">The configuration identifier used to look up the associated displays.</param>
/// <returns>A task that returns the list of <see cref="Display"/> objects matching the given configuration identifier.</returns>
/// <!-- aidoc:v1 sig=1ceff79 body=c343dde -->
public Task<List<Display>> GetByConfigId(ObjectId configId)
{
return displayRepository.GetByConfigId(configId);
}
/// <summary>
/// Retrieves a list of displays associated with the specified card configuration identifier by delegating to the underlying repository.
/// </summary>
/// <param name="configId">The unique identifier of the card configuration used to look up the associated displays.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="Display"/> objects matching the provided card configuration identifier.</returns>
/// <!-- aidoc:v1 sig=c38a1bb body=4696c61 -->
public Task<List<Display>> GetByCardConfigId(ObjectId configId)
{
return displayRepository.GetByCardConfigId(configId);
}
/// <summary>
/// Retrieves a <see cref="Display"/> by its name. Throws a <see cref="NotFoundException"/> if no matching display is found.
/// </summary>
/// <param name="name">The name of the display to look up.</param>
/// <returns>The <see cref="Display"/> that matches the specified name.</returns>
/// <exception cref="NotFoundException">Thrown when no display is found for the given name.</exception>
/// <!-- aidoc:v1 sig=58df1bb body=33a5e00 -->
public async Task<Display?> GetByName(string name)
{
return await displayRepository.GetByName(name) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
}
/// <summary>
/// Retrieves a <see cref="Display"/> by its unique identifier from the repository.
/// </summary>
/// <param name="id">The unique identifier of the display to retrieve.</param>
/// <returns>The matching <see cref="Display"/> if found; otherwise, <c>null</c>.</returns>
/// <!-- aidoc:v1 sig=06fe2a7 body=bf712b4 -->
public async Task<Display?> GetById(ObjectId id)
{
return await displayRepository.GetById(id);
}
/// <summary>
/// Retrieves a display by its identifier, enriches it with localized point-of-care information, its display configuration, and the permissions available to the current user.
/// </summary>
/// <param name="id">The unique identifier of the display to retrieve.</param>
/// <param name="localeEnum">The locale used to localize the related point-of-care information.</param>
/// <returns>A <see cref="DisplayWithPermissionsDto"/> containing the display and its associated permissions.</returns>
/// <exception cref="NotFoundException">Thrown when the current user cannot be identified from the JWT or when no display is found for the specified <paramref name="id"/>.</exception>
/// <!-- aidoc:v1 sig=4aa342c body=756b4f9 -->
public async Task<DisplayWithPermissionsDto> GetByIdWithPermissions(ObjectId id, LocaleEnum localeEnum)
{
var username = JwtHelper.GetUsernameFromPrincipal(httpContextAccessor.HttpContext?.User!) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
var user = await userRepository.GetByUserName(username);
var display = await GetById(id);
if (display == null) throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
foreach (var poc in display.PointOfCareIdList)
{
var c = await pointOfCareService.GetInfo(poc, localeEnum);
if (c != null) display.PointOfCares.Add(c);
}
display.DisplayConfig =
await displayConfigService.GetById(display.DisplayConfigId, display.UnitId, display.Type);
display.DisplayConfig!.DisplaySectionList = [];
try
{
display.DisplayConfig!.DisplaySectionList =
await GetDisplaySectionByUser(display.Type, id, username, user?.Authorization);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
return new DisplayWithPermissionsDto
{
Display = display,
Permissions = await permissionService.Value.GetPermissionsForDisplay(display, user!)
};
}
/// <summary>
/// Asynchronously counts the number of displays associated with the specified unit identifier.
/// </summary>
/// <param name="unitId">The unique identifier of the unit whose displays should be counted.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the total number of displays for the given unit.</returns>
/// <!-- aidoc:v1 sig=db66b12 body=1dd1fbe -->
public async Task<long> CountDisplaysByUnitId(ObjectId unitId)
{
return await displayRepository.CountByUnitId(unitId);
}
/// <summary>
/// Retrieves display information by id, with optional enrichment of point-of-care, patient data, and section list based on the provided flags.
/// Uses cached data when display configuration is requested; otherwise fetches the base display and caches the result.
/// Fetches user authorizations from the user repository when not supplied, and logs an error if the display list cannot be populated due to a missing configuration.
/// </summary>
/// <param name="id">Identifier of the display to retrieve.</param>
/// <param name="userName">Optional user name used to look up authorizations when none are provided.</param>
/// <param name="authorizations">Optional pre-resolved authorizations used to filter the display section list.</param>
/// <param name="locale">Optional locale applied when loading point-of-care data.</param>
/// <param name="fillPointOfCare">If true, populates the point-of-care entries for the display.</param>
/// <param name="fillPatientData">If true, includes patient data when retrieving point-of-care information.</param>
/// <param name="fillDisplayList">If true, populates the display section list filtered by the resolved authorizations.</param>
/// <param name="fillDisplayConfig">If true, retrieves the full display including its configuration (cached); otherwise retrieves the base display.</param>
/// <param name="ct">Cancellation token to cancel the operation.</param>
/// <returns>The requested <see cref="Display"/>, or <c>null</c> if no display is found for the given id.</returns>
/// <!-- aidoc:v1 sig=7a4d82f body=f266015 -->
public async Task<Display?> GetInfo(ObjectId id,
string? userName,
List<Authorization>? authorizations,
LocaleEnum? locale,
bool fillPointOfCare = true,
bool fillPatientData = false,
bool fillDisplayList = true,
bool fillDisplayConfig = true,
CancellationToken ct = default)
{
var start = DateTime.Now;
Display? display;
if (fillDisplayConfig)
{
// Clave: display con configuración
var (key, ttl) = CacheKeys.DisplayWithConfigKeyWithTtl(_cacheSettings, id);
display = await cacheService.GetOrSetObjectAsync(
key,
async () => await BuildDisplayWithConfig(id, ct),
ttl,
ct);
}
else
{
// Clave: display base
var (key, ttl) = CacheKeys.DisplayBaseKeyWithTtl(_cacheSettings, id);
display = await cacheService.GetOrSetObjectAsync(
key,
async () => await displayRepository.GetById(id),
ttl,
ct);
}
if (display == null) return null;
// PointOfCare (cacheado en su propio servicio)
if (fillPointOfCare)
foreach (var poc in display.PointOfCareIdList)
{
var c = await pointOfCareService.GetInfo(poc, locale, fillPatientData);
if (c != null) display.PointOfCares.Add(c);
}
if (authorizations == null && userName != null)
{
var c = await userRepository.GetByUserAndAuthoritesName(userName);
authorizations = c?.Authorization;
}
// DisplayList (depende de autorizaciones → NO cacheable)
if (fillDisplayList)
if (display.DisplayConfig != null)
display.DisplayConfig.DisplaySectionList =
await GetDisplaySectionByUser(display.Type, id, userName, authorizations);
else
logger.LogError("DISPLAY CONFIG NULL on fill display list");
var end = DateTime.Now;
logger.LogDebug("Finished GetInfo Displays for user {user} in {TotalSeconds:F1} seconds", userName,
(end - start).TotalSeconds);
return display;
}
/// <summary>
/// Builds a <see cref="Display"/> enriched with its display configuration. Returns <c>null</c> when the display is not found, and for <see cref="SmartDisplay"/> instances that have a <c>CardRotatingLayout</c>, resolves and assigns each card's configuration, falling back to an empty <see cref="CardConfig"/> when a card configuration cannot be retrieved.
/// </summary>
/// <param name="id">The identifier of the display to load.</param>
/// <param name="ct">The cancellation token used to cancel the asynchronous operation.</param>
/// <returns>The <see cref="Display"/> with its configuration populated, or <c>null</c> if no display exists for the given <paramref name="id"/>.</returns>
/// <!-- aidoc-review:v1 severity=medium kind=wrong_param_role
/// "The ct parameter is documented as 'used to cancel the asynchronous operation', but the method never passes ct to displayRepository.GetById, displayConfigService.GetById, or displayConfigService.GetCardConfigById, so the token has no effect on cancellation." -->
private async Task<Display?> BuildDisplayWithConfig(ObjectId id, CancellationToken ct)
{
var display = await displayRepository.GetById(id);
if (display == null) return null;
var type = display.Type;
// DisplayConfig
display.DisplayConfig =
await displayConfigService.GetById(display.DisplayConfigId, display.UnitId, type);
if (type != DisplayConfigEnums.DisplayType.SmartDisplay ||
display.DisplayConfig is not SmartDisplay { CardRotatingLayout: not null } smart)
return display;
if (smart.CardRotatingLayout == null)
return display;
foreach (var card in smart.CardRotatingLayout)
card.Data = await displayConfigService.GetCardConfigById(card.DataId)
?? new CardConfig();
return display;
}
/// <summary>
/// Retrieves the display sections accessible to a specific user, filtered by display type, based on the user's authorities (either provided or fetched from the authority service).
/// Supports both direct display references and unit-based references, marks the currently selected display, and returns an empty list if the user is not found or no matching sections exist.
/// </summary>
/// <param name="type">The display type used to filter the returned sections.</param>
/// <param name="currentDisplay">The identifier of the currently selected display, which will be flagged as selected in the result; may be null.</param>
/// <param name="userName">The username used to look up the user and their authorities; if null, an empty list is returned.</param>
/// <param name="authorizations">Optional pre-fetched list of user authorities; when null, authorities are retrieved from the authority service.</param>
/// <returns>A task that resolves to a list of <see cref="MinimalDisplaySection"/> items accessible to the user and matching the specified display type.</returns>
/// <!-- aidoc:v1 sig=64d7b66 body=cc129cf -->
public async Task<List<MinimalDisplaySection>> GetDisplaySectionByUser(
DisplayConfigEnums.DisplayType type,
ObjectId? currentDisplay,
string? userName,
List<Authorization>? authorizations)
{
try
{
var listToReturn = new List<MinimalDisplaySection>();
if (userName != null)
{
var user = await userRepository.GetByUserName(userName);
if (user != null)
{
var displayIdByAuthorities = authorizations ?? await authorityService.GetUserAuthorities(user.Id);
foreach (var e in displayIdByAuthorities)
{
var isParsed = ObjectId.TryParse(e.DisplayId, out var dId);
if (isParsed)
{
if (listToReturn.All(c => c.Id != dId))
{
var toAdd = await GetById(dId);
if (toAdd != null)
if (type == toAdd.Type)
{
var minDisSec = new MinimalDisplaySection
{
Name = toAdd.Name,
Id = toAdd.Id,
IsSelected = currentDisplay != null && toAdd.Id == currentDisplay
};
listToReturn.Add(minDisSec);
}
}
}
else
{
var isParsedUnitId = ObjectId.TryParse(e.UnitId, out var uId);
if (isParsedUnitId)
{
var unitDisplays = await GetByUnitId(uId);
foreach (var disp in unitDisplays)
if (type == disp.Type && listToReturn.All(c => c.Id != dId))
{
var minDisSec = new MinimalDisplaySection
{
Name = disp.Name,
Id = disp.Id,
IsSelected = currentDisplay != null && disp.Id == currentDisplay
};
listToReturn.Add(minDisSec);
}
}
}
}
}
}
return listToReturn;
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
/// <summary>
/// Asynchronously retrieves all display configurations of type DisplayNurse and SmartDisplay, mapping them into minimal display sections and grouping them within a display list DTO.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a MinimalDisplayListDto with the populated DisplayNurse and SmartDisplay collections.</returns>
/// <!-- aidoc:v1 sig=208d6c6 body=b473ad8 -->
public async Task<MinimalDisplayListDto> GetAllDisplaySection()
{
var minimalDisplayListDto = new MinimalDisplayListDto();
var listDisplayNurse = await GetByType(DisplayConfigEnums.DisplayType.DisplayNurse);
foreach (var displayForAdmin in listDisplayNurse)
{
var minDisSec = new MinimalDisplaySection
{
Name = displayForAdmin.Name,
Id = displayForAdmin.Id
};
minimalDisplayListDto.DisplayNurse.Add(minDisSec);
}
var listDisplaySmart = await GetByType(DisplayConfigEnums.DisplayType.SmartDisplay);
foreach (var displayForAdmin in listDisplaySmart)
{
var minDisSec = new MinimalDisplaySection
{
Name = displayForAdmin.Name,
Id = displayForAdmin.Id
};
minimalDisplayListDto.SmartDisplay.Add(minDisSec);
}
return minimalDisplayListDto;
}
/// <summary>
/// Retrieves the list of displays associated with the specified unit identifier by delegating to the display repository.
/// </summary>
/// <param name="unitId">The identifier of the unit whose displays should be returned.</param>
/// <returns>A task that represents the asynchronous operation, containing the list of <see cref="Display"/> objects for the given unit.</returns>
/// <!-- aidoc:v1 sig=61f7d68 body=08a2310 -->
public async Task<List<Display>> GetByUnitId(ObjectId unitId)
{
return await displayRepository.GetByUnitId(unitId);
}
/// <summary>
/// Retrieves all available points of care (POC) and their associated unit information for the specified display identifiers. Invalid display ID strings are silently skipped, virtual points of care can optionally be excluded, and a <see cref="NotFoundException"/> is thrown when a resolved unit cannot be found; any unexpected error is logged and an empty result is returned.
/// </summary>
/// <param name="displayIds">A list of display identifier strings used to resolve the related units and their available points of care.</param>
/// <param name="excludeVirtual">When set to <c>true</c>, virtual points of care are excluded from the result; otherwise, they are included.</param>
/// <returns>A <see cref="PocAndUnitDto"/> containing the available points of care and their associated unit details.</returns>
/// <exception cref="NotFoundException">Thrown when a unit associated with one of the resolved display identifiers cannot be found.</exception>
/// <!-- aidoc-review:v1 severity=high kind=wrong_exception
/// "NotFoundException is thrown inside the try block but immediately caught by the generic catch(Exception) handler, which logs the error and returns an empty PocAndUnitDto. The exception never propagates to the caller, so documenting it as thrown is misleading." -->
public async Task<PocAndUnitDto> GetAllAvailablePoc(List<string> displayIds, bool excludeVirtual = false)
{
try
{
var listToReturn = new PocAndUnitDto();
var listObjectId = new List<ObjectId>();
foreach (var displayId in displayIds)
{
var isParsed = ObjectId.TryParse(displayId, out var dId);
if (isParsed)
{
var diplay = await displayRepository.GetById(dId);
if (diplay != null)
listObjectId.Add(diplay.UnitId);
}
}
var distinctObjectIds = listObjectId.Distinct().ToList();
foreach (var distinctObjectId in distinctObjectIds)
{
var unit = await unitService.Value.FindById(distinctObjectId) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
var pocList = await pointOfCareService.FindByUnitAndStatus(distinctObjectId,
StatusEnum.PointOfCare.Available, excludeVirtual);
foreach (var pointOfCare in pocList)
{
var pocAv = new MinimalPocAndUnitDto
{
PocId = pointOfCare.Id,
PocName = pointOfCare.Bed,
UnitId = pointOfCare.UnitId,
UnitName = unit.Name
};
listToReturn.PocList.Add(pocAv);
}
}
return listToReturn;
}
catch (Exception e)
{
logger.LogError("Error GetAllAvailablePoc {Error}", e.Message);
return new PocAndUnitDto();
}
}
/// <summary>
/// Retrieves all points of care associated with the specified display. Returns an empty list when the display is not found, when no associated points of care exist, or when an error occurs during retrieval.
/// </summary>
/// <param name="id">The ObjectId of the display whose points of care should be retrieved.</param>
/// <returns>A list of points of care linked to the display, or an empty list if the display cannot be found or if an error is encountered.</returns>
/// <!-- aidoc:v1 sig=9215df7 body=d5eaca6 -->
public async Task<List<PointOfCare>> GetAllPocsByDisplayId(ObjectId id)
{
try
{
var pocList = new List<PointOfCare>();
var display = await displayRepository.GetById(id);
if (display == null)
return pocList;
foreach (var pocId in display.PointOfCareIdList)
{
var poc = await pointOfCareService.FindById(pocId);
if (poc != null) pocList.Add(poc);
}
return pocList;
}
catch (Exception e)
{
logger.LogError("Error GetAllAvailablePoc {Error}", e.Message);
return [];
}
}
/// <summary>
/// Retrieves the display configuration locations associated with the specified display configuration ID, mapping each display to its corresponding unit name. If a unit cannot be found for a display, the resulting location's unit name will be null.
/// </summary>
/// <param name="displayConfigId">The identifier of the display configuration whose locations are being retrieved.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a list of <see cref="DisplayConfigLocationDto"/> objects with display and unit information.</returns>
/// <!-- aidoc:v1 sig=0895dcd body=e5b010a -->
public async Task<List<DisplayConfigLocationDto>> GetDisplayConfigLocations(ObjectId displayConfigId)
{
var displays = await GetByConfigId(displayConfigId);
var locations = new List<DisplayConfigLocationDto>();
foreach (var display in displays)
{
var unit = await unitService.Value.FindById(display.UnitId);
DisplayConfigLocationDto newLocation = new()
{
DisplayName = display.Name,
UnitName = unit?.Name,
};
locations.Add(newLocation);
}
return locations;
}
/// <summary>
/// Determines whether the specified display configuration is currently in use by checking if it is referenced by any related entity.
/// </summary>
/// <param name="displayConfigId">The unique identifier of the display configuration to check.</param>
/// <returns><c>true</c> if the display configuration is referenced by at least one entity; otherwise, <c>false</c>.</returns>
/// <!-- aidoc:v1 sig=685c5f5 body=f4b630c -->
public async Task<bool> IsDisplayConfigInUse(ObjectId displayConfigId)
{
return await displayRepository.IsDisplayConfigInUse(displayConfigId) > 0;
}
#endregion
#region Update
/*
* En esta actualización se espera una resubscipción al id del display ya que actualizar los PoC conlleva actualizar
* subscrioptor y locations para las observaciones
*/
/// <summary>
/// Updates the point of care list associated with the specified display, invalidating the related cache entries and broadcasting the change to subscribers.
/// </summary>
/// <param name="objectId">The identifier of the display whose point of care list is being updated.</param>
/// <param name="listPocObId">The list of point of care object identifiers to assign to the display.</param>
/// <returns>The updated <see cref="Display"/> instance after the point of care list change.</returns>
/// <exception cref="NotFoundException">Thrown when no display exists for the specified <paramref name="objectId"/>.</exception>
/// <exception cref="ConflictException">Thrown when the point of care list update cannot be persisted.</exception>
/// <!-- aidoc:v1 sig=68b108f body=41d1e6a -->
public async Task<Display?> UpdatePointOfCareList(ObjectId objectId, List<ObjectId> listPocObId)
{
var oldDisplay = await displayRepository.GetById(objectId) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
var displayToReturn = await displayRepository.UpdatePointOfCareList(objectId, listPocObId) ??
throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed);
await cacheService.DeleteByPatternAsync(CacheKeys.DisplayBase(objectId));
SendDisplayBroadcast(displayToReturn, OperationType.UpdateDisplayPoC);
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldDisplay, displayToReturn);
return displayToReturn;
}
/// <summary>
/// Updates the configuration of an existing display by casting the new configuration to its specific type based on <see cref="DisplayConfigEnums.DisplayType"/>, supporting <c>DisplayNurse</c> and <c>SmartDisplay</c>. On a successful update, broadcasts the change, creates an audit log entry, and invalidates the related cache entry. Returns <c>null</c> if the provided configuration type is not supported or the cast results in <c>null</c>.
/// </summary>
/// <param name="oldDisplay">The existing display whose configuration will be updated.</param>
/// <param name="newDisplayConfig">The new configuration to apply, or <c>null</c> if no update is provided.</param>
/// <returns>The updated <see cref="Display"/> if the configuration was successfully applied; otherwise, <c>null</c>.</returns>
/// <!-- aidoc-review:v1 severity=high kind=wrong_summary
/// "The summary states the method returns null 'if the provided configuration type is not supported,' but newDisplayConfigCast is initialized to a non-null default and the code proceeds to the repository call for any non-null cast result, including unsupported types." -->
/// <!-- aidoc-review:v1 severity=high kind=wrong_summary
/// "The summary groups broadcast, audit log creation, and cache invalidation together as happening 'on a successful update,' but cache invalidation occurs whenever a valid config type is provided, even if the repository update returns null." -->
public async Task<Display?> UpdateConfig(Display oldDisplay, DisplayConfig? newDisplayConfig)
{
var newDisplayConfigCast = new DisplayConfig();
switch (newDisplayConfig?.Type)
{
case DisplayConfigEnums.DisplayType.DisplayNurse:
newDisplayConfigCast = newDisplayConfig as DisplayNurse;
break;
case DisplayConfigEnums.DisplayType.SmartDisplay:
newDisplayConfigCast = newDisplayConfig as SmartDisplay;
break;
}
if (newDisplayConfigCast != null)
{
var displayToReturn = await displayRepository.UpdateConfig(oldDisplay.Id, newDisplayConfigCast);
if (displayToReturn != null)
{
SendDisplayBroadcast(displayToReturn, OperationType.UpdateDisplayConfig);
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldDisplay,
displayToReturn);
}
await cacheService.DeleteByPatternAsync(CacheKeys.DisplayBase(oldDisplay.Id));
return displayToReturn;
}
return null;
}
/// <summary>
/// Updates the configuration ID associated with the specified display. On a successful update, the related cache entries are invalidated, a display update broadcast is sent, and an audit log entry is created.
/// </summary>
/// <param name="oldDisplay">The display whose configuration ID is being updated.</param>
/// <param name="configId">The new configuration ID to assign to the display.</param>
/// <returns>The updated display, or <c>null</c> if the display was not found.</returns>
/// <!-- aidoc:v1 sig=a4c5a08 body=1c84331 -->
public async Task<Display?> UpdateConfigId(Display oldDisplay, ObjectId configId)
{
var displayToReturn = await displayRepository.UpdateConfigId(oldDisplay.Id, configId);
if (displayToReturn != null)
{
await cacheService.DeleteByPatternAsync(CacheKeys.DisplayBase(oldDisplay.Id));
SendDisplayBroadcast(displayToReturn, OperationType.UpdateDisplayConfig);
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldDisplay, displayToReturn);
}
return displayToReturn;
}
/// <summary>
/// Updates the configuration preset associated with the specified display, invalidates the display cache, records an audit log entry, and broadcasts a notification to subscribers based on the resolved display type (DisplayNurse, SmartDisplay, or Unknown). Throws a not-found exception when the update result or configuration cannot be resolved, and an invalid-format exception when the configuration type is not one of the handled types.
/// </summary>
/// <param name="objectIdDisplay">The identifier of the display whose configuration preset is being updated.</param>
/// <param name="objectIdConfigDisplay">The identifier of the new configuration preset to apply to the display.</param>
/// <returns>The updated <see cref="Display"/> entity, or <c>null</c> if the update could not be completed.</returns>
/// <exception cref="NotFoundException">Thrown when the update result or the resolved configuration is <c>null</c>.</exception>
/// <exception cref="InvalidFormatException">Thrown when the configuration type is not one of the handled display types.</exception>
/// <!-- aidoc-review:v1 severity=medium kind=wrong_returns
/// "Documentation states the method returns 'null if the update could not be completed', but the code throws NotFoundException when result is null, never actually returning null to the caller." -->
public async Task<Display?> UpdateConfigPreset(ObjectId objectIdDisplay, ObjectId objectIdConfigDisplay)
{
var oldConfig = await displayConfigService.GetById(objectIdConfigDisplay);
var result = await displayRepository.UpdateConfigPreset(objectIdDisplay, objectIdConfigDisplay);
await cacheService.DeleteByPatternAsync(CacheKeys.DisplayBase(objectIdDisplay));
var config = await displayConfigService.GetById(objectIdConfigDisplay);
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldConfig, config);
if (result == null || config == null)
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
var subscribers = subscribersService.GetSubscribers().Where(s => s.DisplayId == objectIdDisplay).ToList();
switch (config.Type)
{
case DisplayConfigEnums.DisplayType.DisplayNurse:
SendNurseDisplayBroadcast(subscribers, config as DisplayNurse);
break;
case DisplayConfigEnums.DisplayType.SmartDisplay:
SendSmartDisplayBroadcast(subscribers, config as SmartDisplay);
break;
case DisplayConfigEnums.DisplayType.Unknown:
break;
default:
throw new InvalidFormatException(HttpEnum.ErrorMessage.BadRequestIncorrectType);
}
return result;
}
/// <summary>
/// Updates the name of an existing display identified by the given identifier, invalidates the related cache entries, and records an audit log entry for the change.
/// </summary>
/// <param name="id">The unique identifier of the display to update.</param>
/// <param name="name">The new name to assign to the display.</param>
/// <returns>The updated <see cref="Display"/> instance, or <c>null</c> if the update could not be performed.</returns>
/// <exception cref="NotFoundException">Thrown when no display is found for the specified <paramref name="id"/>.</exception>
/// <!-- aidoc:v1 sig=18e4a50 body=b0ad3f7 -->
public async Task<Display?> UpdateName(ObjectId id, string name)
{
var display = await displayRepository.GetById(id) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
var newDisplay = await displayRepository.UpdateName(display, name);
await cacheService.DeleteByPatternAsync(CacheKeys.DisplayBase(id));
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, display, newDisplay);
return newDisplay;
}
#endregion
#region Delete
/// <summary>
/// Deletes a display by its identifier, removing the record, invalidating the related cache, clearing associated authorities, and recording an audit log entry.
/// </summary>
/// <param name="id">The unique identifier of the display to delete.</param>
/// <returns>A task that resolves to <c>true</c> when the display has been successfully deleted.</returns>
/// <exception cref="NotFoundException">Thrown when no display is found for the specified <paramref name="id"/>.</exception>
/// <!-- aidoc:v1 sig=328e872 body=46f2b0a -->
public async Task<bool> DeleteDisplay(ObjectId id)
{
var display = await displayRepository.GetById(id) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
await displayRepository.DeleteAsync(id);
// Invalidar CACHE (colección completa)
await cacheService.DeleteByPatternAsync(CacheKeys.DisplayBase(id));
await authorityService.DeleteByDisplayId(id);
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, display, null);
return true;
}
/// <summary>
/// Deletes all displays associated with the specified unit identifier, invalidates the displays cache, and removes related authority data for the unit.
/// </summary>
/// <param name="unitId">The unique identifier of the unit whose displays and related authority data will be removed.</param>
/// <!-- aidoc:v1 sig=bd663aa body=28edafe -->
public async Task DeleteDisplaysByUnitId(ObjectId unitId)
{
await displayRepository.DeleteManyByUnitId(unitId);
// Invalidar CACHE (colección completa)
await cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.Displays));
await authorityService.DeleteByUnitId(unitId);
}
#endregion
#region Send Notification
/// <summary>
/// Sends a smart display configuration update broadcast asynchronously to all specified WebSocket subscribers.
/// </summary>
/// <param name="subscribers">The list of WebSocket subscribers that will receive the smart display configuration update.</param>
/// <param name="config">The smart display configuration to broadcast. May be <c>null</c> if no configuration is provided.</param>
/// <!-- aidoc:v1 sig=57faa95 body=71328cd -->
private void SendSmartDisplayBroadcast(List<WsSubscriber> subscribers, SmartDisplay? config)
{
foreach (var subscriber in subscribers)
_ = clientMessageService.SendAsync(subscriber.Id, OperationType.UpdateSmartDisplayConfig, config);
}
/// <summary>
/// Broadcasts a nurse display configuration update to all specified WebSocket subscribers by sending an asynchronous update message to each one.
/// </summary>
/// <param name="subscribers">The list of WebSocket subscribers that will receive the nurse display configuration update.</param>
/// <param name="config">The nurse display configuration to broadcast, which may be <c>null</c>.</param>
/// <!-- aidoc:v1 sig=6730073 body=809b5d8 -->
private void SendNurseDisplayBroadcast(List<WsSubscriber> subscribers, DisplayNurse? config)
{
foreach (var subscriber in subscribers)
_ = clientMessageService.SendAsync(subscriber.Id, OperationType.UpdateNurseDisplayConfig, config);
}
/// <summary>
/// Sends a broadcast message to all subscribers associated with the specified display.
/// Currently only handles the <see cref="OperationType.UpdateDisplayPoC"/> operation, dispatching an asynchronous notification to each subscriber; other operation types are ignored.
/// </summary>
/// <param name="display">The display whose subscribers will receive the broadcast; used to filter the subscriber list by its identifier.</param>
/// <param name="operation">The type of operation being broadcast, which determines the action taken on matching subscribers.</param>
/// <!-- aidoc:v1 sig=30dad01 body=bbd0711 -->
private void SendDisplayBroadcast(Display display, OperationType operation)
{
var subscribers = subscribersService.GetSubscribers().Where(s =>
s.DisplayId == display.Id).ToList();
switch (operation)
{
case OperationType.UpdateDisplayPoC:
foreach (var subscriber in subscribers)
_ = clientMessageService.SendAsync(subscriber.Id, operation, null);
break;
}
}
#endregion
#endregion
}