890 lines
43 KiB
C#
890 lines
43 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;
|
|
using adas_core.Domain.Models.DTO;
|
|
using adas_core.Domain.Models.Masters;
|
|
using adas_core.Domain.Models.MongoModels;
|
|
using adas_core.Domain.Models.Responses;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.Extensions.Logging;
|
|
using MongoDB.Bson;
|
|
|
|
namespace adas_core.Application.Services;
|
|
|
|
public class AdmissionService(
|
|
ILogger<AdmissionService> logger,
|
|
ISubscribersService subscribersService,
|
|
IAdmissionRepository admissionRepository,
|
|
IClientMessageService clientMessageService,
|
|
IUnitService unitService,
|
|
IPatientService patientService,
|
|
IPointOfCareService pointOfCareService,
|
|
IDisplayService displayService,
|
|
IDischargeService dischargeService,
|
|
IPatientArchiveRepository patientArchiveRepository,
|
|
IHttpContextAccessor httpContextAccessor,
|
|
ILocalAuditService auditService,
|
|
IMasterListServiceFactory masterListServiceFactory)
|
|
: IAdmissionService
|
|
{
|
|
// Auditory logs
|
|
|
|
|
|
/// <summary>
|
|
/// Deletes the specified admission by delegating to the delete operation using the admission's identifier.
|
|
/// </summary>
|
|
/// <param name="admission">The admission entity to delete, identified by its <see cref="Admission.Id"/>.</param>
|
|
public async Task DeleteAdmissionAsync(Admission admission)
|
|
{
|
|
await DeleteAdmissionByIdAsync(admission.Id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes an admission identified by the given id. If the admission is not found, the operation is skipped and logged; otherwise the admission is removed, any associated point of care is detached (clearing its <c>AdmissionId</c> and <c>Admission</c>) and set to <c>Available</c> when not currently <c>Locked</c> or <c>InUse</c>, a delete broadcast is sent, and an audit log entry is created.
|
|
/// </summary>
|
|
/// <param name="admissionId">The identifier of the admission to delete.</param>
|
|
public async Task DeleteAdmissionByIdAsync(ObjectId admissionId)
|
|
{
|
|
var admissionAux = await admissionRepository.FindById(admissionId);
|
|
if (admissionAux == null)
|
|
{
|
|
logger.LogInformation("Error deleting Admission not found, id: {AdmissionId} ", admissionId);
|
|
return;
|
|
}
|
|
|
|
await admissionRepository.Delete(admissionId);
|
|
|
|
if (admissionAux.PointOfCareId.HasValue)
|
|
{
|
|
var poc = await pointOfCareService.GetInfo(admissionAux.PointOfCareId.Value);
|
|
if (poc != null && poc.AdmissionId == admissionId)
|
|
{
|
|
poc.AdmissionId = null;
|
|
poc.Admission = null;
|
|
await pointOfCareService.Update(poc);
|
|
|
|
|
|
if (poc.Status != StatusEnum.PointOfCare.Locked && poc.Status != StatusEnum.PointOfCare.InUse)
|
|
await pointOfCareService.SetPointOfCareStatus(poc.Id, StatusEnum.PointOfCare.Available);
|
|
}
|
|
}
|
|
|
|
logger.LogInformation("Admission id: {AdmissionId} DELETED ", admissionId);
|
|
|
|
SendAdmissionBroadcast(admissionAux, OperationType.DeleteAdmission);
|
|
|
|
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, admissionAux, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asynchronously deletes all admissions associated with the specified unit identifier by delegating the operation to the admission repository.
|
|
/// </summary>
|
|
/// <param name="unitId">The unique identifier of the unit whose admissions should be removed.</param>
|
|
public async Task DeleteAdmissionsByUnitId(ObjectId unitId)
|
|
{
|
|
_ = await admissionRepository.DeleteAdmissionsByUnitId(unitId);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Retrieves an admission by its identifier and, when a point of care is associated, enriches the result with the patient's location (unit, bed, and room) obtained from the point of care service. Returns null if the admission cannot be found.
|
|
/// </summary>
|
|
/// <param name="admissionId">The unique identifier of the admission to retrieve.</param>
|
|
/// <returns>The matching <see cref="Admission"/> with its <see cref="Admission.PatientLocation"/> populated when applicable, or null if no admission is found.</returns>
|
|
public async Task<Admission?> GetAdmissionByIdAsync(ObjectId admissionId)
|
|
{
|
|
var result = await admissionRepository.FindById(admissionId);
|
|
if (result?.PointOfCareId != null)
|
|
{
|
|
var poc = await pointOfCareService.GetInfo(result.PointOfCareId.Value, null, false);
|
|
result.PatientLocation = new PatientLocation(poc?.UnitName, poc?.Bed, poc?.Room);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asynchronously retrieves all admissions and enriches each one with its associated point of care information (unit, bed, and room) when available.
|
|
/// </summary>
|
|
/// <returns>A task that represents the asynchronous operation. The task result contains a collection of <see cref="Admission"/> objects with patient location details populated for those linked to a point of care.</returns>
|
|
/// <exception cref="NotFoundException">Thrown when the admission repository returns no results.</exception>
|
|
public async Task<IEnumerable<Admission>> GetAdmissionsAsync()
|
|
{
|
|
var resultList = await admissionRepository.FindAll() ??
|
|
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
|
|
var admissionsAsync = resultList.ToList();
|
|
foreach (var admission in admissionsAsync)
|
|
if (admission.PointOfCareId != null)
|
|
{
|
|
var poc = await pointOfCareService.GetInfo(admission.PointOfCareId.Value, null, false);
|
|
admission.PatientLocation = new PatientLocation(poc?.UnitName, poc?.Bed, poc?.Room);
|
|
}
|
|
|
|
return admissionsAsync;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inserts a new admission, preventing duplicates by NHC and optionally linking it to a Point of Care.
|
|
/// When a Point of Care is assigned, its information is used to populate the patient location and, if free, it is reserved and associated with the newly created admission.
|
|
/// </summary>
|
|
/// <param name="admission">The admission to insert, optionally including a PointOfCareId to associate with a care location.</param>
|
|
/// <returns>The newly inserted <see cref="Admission"/>, or <c>null</c> if no result is produced.</returns>
|
|
/// <exception cref="ConflictException">Thrown when an admission with the same NHC already exists, or when the insertion fails to return a result.</exception>
|
|
/// <exception cref="NotFoundException">Thrown when the specified Point of Care does not exist.</exception>
|
|
public async Task<Admission?> InsertAdmission(Admission admission)
|
|
{
|
|
var admissionAux = await admissionRepository.FindByNhc(admission.Nhc);
|
|
if (admissionAux != null) throw new ConflictException(HttpEnum.ErrorMessage.BadRequestDuplicateData);
|
|
PointOfCare? pointOfCare = null;
|
|
//Bloqueamos el pointOfCare en el caso de que lo tenga asignado
|
|
if (admission.PointOfCareId.HasValue)
|
|
{
|
|
pointOfCare = await pointOfCareService.GetInfo(admission.PointOfCareId.Value) ??
|
|
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
|
|
|
|
//pointOfCare.AdmissionId = admission.Id;
|
|
//pointOfCare.Admission = admission;
|
|
admission.PatientLocation = new PatientLocation(pointOfCare.UnitName, pointOfCare.Bed, pointOfCare.Room);
|
|
}
|
|
|
|
var insertedAdmission = await admissionRepository.InsertOneAsyncAndReturn(admission) ??
|
|
throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed);
|
|
|
|
if (pointOfCare is { AdmissionId: null })
|
|
{
|
|
pointOfCare.Admission = insertedAdmission;
|
|
pointOfCare.AdmissionId = insertedAdmission.Id;
|
|
if (pointOfCare.Status == StatusEnum.PointOfCare.Available)
|
|
pointOfCare.Status = StatusEnum.PointOfCare.Reserved;
|
|
|
|
await pointOfCareService.Update(pointOfCare);
|
|
}
|
|
|
|
SendAdmissionBroadcast(insertedAdmission, OperationType.NewAdmission);
|
|
// Obtener información el usuario autenticado
|
|
|
|
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, insertedAdmission);
|
|
|
|
return insertedAdmission;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates an existing admission record, enriching its patient location details from the linked point of care on both the prior and incoming states, and records the change via audit log and broadcast.
|
|
/// If the admission is not found, the method returns without making changes; point of care lookups are only applied when a <c>PointOfCareId</c> is present and yields a result.
|
|
/// </summary>
|
|
/// <param name="admission">The admission entity containing the updated information to persist.</param>
|
|
public async Task UpdateAdmissionAsync(Admission admission)
|
|
{
|
|
var oldAdmission = await admissionRepository.FindById(admission.Id);
|
|
if (oldAdmission == null) return;
|
|
if (oldAdmission.PointOfCareId.HasValue)
|
|
{
|
|
var pocOld = await pointOfCareService.GetInfo(oldAdmission.PointOfCareId.Value, null, false);
|
|
if (pocOld != null)
|
|
oldAdmission.PatientLocation = new PatientLocation(pocOld.UnitName, pocOld.Bed, pocOld.Room);
|
|
}
|
|
|
|
if (admission.PointOfCareId.HasValue)
|
|
{
|
|
var poc = await pointOfCareService.GetInfo(admission.PointOfCareId.Value, null, false);
|
|
if (poc != null) admission.PatientLocation = new PatientLocation(poc.UnitName, poc.Bed, poc.Room);
|
|
}
|
|
|
|
await admissionRepository.Update(admission);
|
|
|
|
await HandlePointOfCareChange(admission, oldAdmission);
|
|
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldAdmission, admission);
|
|
SendAdmissionBroadcast(admission, OperationType.UpdateAdmission);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Admits a patient based on the provided <see cref="Admission"/>, creating a new <see cref="Patient"/> assigned to the specified point of care, marking the point of care as in use, and updating related master lists (insulation, allergies, diagnosis, origin, language barrier, passive sitting) when present. If the point of care id, unit, or point of care cannot be resolved, the operation is skipped after logging an error. When <paramref name="isNew"/> is <c>false</c>, the originating admission record is deleted after the patient is inserted.
|
|
/// </summary>
|
|
/// <param name="admission">The admission data used to create the patient and populate location, diagnosis, allergies, and other attributes.</param>
|
|
/// <param name="isNew">When <c>false</c>, the admission record is deleted after a successful patient insertion; when <c>true</c>, the admission is retained.</param>
|
|
public async Task AdmitPatient(Admission admission, bool isNew = false)
|
|
{
|
|
if (admission.PointOfCareId == null)
|
|
{
|
|
logger.LogError("PointOfCare is required. Admission: {Admission}", admission);
|
|
return;
|
|
}
|
|
|
|
var unit = await unitService.FindById(admission.UnitId);
|
|
if (unit == null)
|
|
{
|
|
logger.LogError("Unit {Name} not found. Unit Id: ", admission.UnitId);
|
|
return;
|
|
}
|
|
|
|
var pointOfCare = await pointOfCareService.FindById(admission.PointOfCareId.Value);
|
|
if (pointOfCare == null)
|
|
{
|
|
logger.LogError("Point of Care not found. Patient not created. {Admission}", admission);
|
|
return;
|
|
}
|
|
|
|
|
|
Patient patient = new()
|
|
{
|
|
PointOfCareId = pointOfCare.Id,
|
|
UnitId = pointOfCare.UnitId,
|
|
PointOfCare = pointOfCare,
|
|
UnitString = pointOfCare.UnitName,
|
|
Bed = pointOfCare.Bed,
|
|
Room = pointOfCare.Room,
|
|
PatientNumber = admission.Nhc,
|
|
AdmTime = DateTime.UtcNow,
|
|
Person = admission.Person,
|
|
CreationDate = DateTime.UtcNow,
|
|
DischargeStatus = pointOfCare.Unit?.DischargeStatusList?.Options.FirstOrDefault(),
|
|
Origin = admission.Origin,
|
|
OriginAux = admission.OriginAux,
|
|
Diagnosis = admission.Diagnosis,
|
|
DiagnosisAux = admission.DiagnosisAux,
|
|
Allergies = admission.Allergies,
|
|
Insulation = admission.Insulation,
|
|
LanguageBarrier = admission.LanguageBarrier,
|
|
PassiveSitting = admission.PassiveSitting,
|
|
Altable = new OptionList
|
|
{
|
|
Name = "NotAltable",
|
|
IconDefault = "icNotAltable",
|
|
OptionType = "NotAltable"
|
|
},
|
|
Visits = pointOfCare.Unit?.VisitOptionList?.Options.FirstOrDefault(),
|
|
AccessControl = pointOfCare.Unit?.AccessControlList?.Options.FirstOrDefault(),
|
|
Location = new PatientLocation
|
|
(
|
|
unit.Name,
|
|
pointOfCare.Bed,
|
|
pointOfCare.Room
|
|
)
|
|
};
|
|
if (unit.AltableOptionListId is not null)
|
|
{
|
|
var list = await masterListServiceFactory.GetMasterListById(MasterListType.AltableOptionList,
|
|
unit.AltableOptionListId.Value, LocaleEnum.Default) as AltableOptionList;
|
|
var listOpt = list?.Options.FirstOrDefault(c => c.OptionType == "NotAltable");
|
|
if (listOpt != null)
|
|
patient.Altable = listOpt;
|
|
}
|
|
|
|
await patientService.Insert(patient);
|
|
await SetPointOfCareStatus(pointOfCare.Id, StatusEnum.PointOfCare.InUse);
|
|
|
|
if (!isNew)
|
|
await DeleteAdmissionAsync(admission);
|
|
|
|
if (admission.Insulation != null)
|
|
await patientService.UpdatePatientMasterList(
|
|
patient.Id,
|
|
MasterListType.InsulationList,
|
|
[admission.Insulation],
|
|
null, null);
|
|
|
|
if (admission.Allergies != null)
|
|
await patientService.UpdatePatientMasterList(
|
|
patient.Id,
|
|
MasterListType.AllergyList,
|
|
admission.Allergies,
|
|
null, null);
|
|
if (admission.Diagnosis != null)
|
|
await patientService.UpdatePatientMasterList(
|
|
patient.Id,
|
|
MasterListType.DiagnosisList,
|
|
[admission.Diagnosis],
|
|
null, null);
|
|
if (admission.Origin != null)
|
|
await patientService.UpdatePatientMasterList(
|
|
patient.Id,
|
|
MasterListType.OriginList,
|
|
[admission.Origin],
|
|
null, null);
|
|
if (admission.LanguageBarrier != null)
|
|
await patientService.UpdatePatientMasterList(
|
|
patient.Id,
|
|
MasterListType.LanguageBarrierList,
|
|
admission.LanguageBarrier,
|
|
null, null);
|
|
if (admission.PassiveSitting != null)
|
|
await patientService.UpdatePatientMasterList(
|
|
patient.Id,
|
|
MasterListType.PassiveSittingList,
|
|
[admission.PassiveSitting],
|
|
null, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a patient to the admissions workflow by creating a new admission record, removing any existing discharge, and archiving the patient. Validates that the patient and its associated unit exist before proceeding, and only builds the admission when a point of care is assigned.
|
|
/// </summary>
|
|
/// <param name="patientId">The identifier of the patient to be returned to admissions.</param>
|
|
public async Task ReturnPatientToAdmissions(ObjectId patientId)
|
|
{
|
|
var patient = await patientService.FindById(patientId);
|
|
if (patient == null)
|
|
{
|
|
logger.LogError("Error returning the patient Id: {Id} to admission", patientId);
|
|
return;
|
|
}
|
|
|
|
var unit = await unitService.FindById(patient.UnitId);
|
|
if (unit == null)
|
|
{
|
|
logger.LogError("Unit not found by Id. {Name}", patient.UnitId);
|
|
return;
|
|
}
|
|
|
|
if (patient.PointOfCareId != null)
|
|
{
|
|
var poc = await pointOfCareService.FindById(patient.PointOfCareId.Value);
|
|
Admission admission = new()
|
|
{
|
|
Nhc = patient.PatientNumber ?? string.Empty,
|
|
PointOfCareId = poc?.Id,
|
|
UnitId = unit.Id,
|
|
Person = patient.Person ??
|
|
new Person(), //No debería ser null en este punto, pero así quito el warning
|
|
Origin = patient.Origin,
|
|
OriginAux = patient.OriginAux,
|
|
Diagnosis = patient.Diagnosis,
|
|
PassiveSitting = patient.PassiveSitting,
|
|
DiagnosisAux = patient.DiagnosisAux,
|
|
Allergies = patient.Allergies,
|
|
Insulation = patient.Insulation,
|
|
LanguageBarrier = patient.LanguageBarrier,
|
|
AdmissionDate = patient.AdmTime ?? DateTime.UtcNow,
|
|
PatientLocation = patient.Location
|
|
};
|
|
|
|
await InsertAdmission(admission);
|
|
}
|
|
|
|
var dis = await dischargeService.GetDischargeByPatientId(patient.Id);
|
|
if (dis != null) await dischargeService.DeleteDischargeByIdAsync(dis.Id);
|
|
await patientService.ArchivePatient(patient);
|
|
|
|
pointOfCareService.CheckNextAdmission(patient.PointOfCareId);
|
|
}
|
|
|
|
// Used for temporal beds like PUSHED
|
|
/// <summary>
|
|
/// Returns a patient to the admissions flow by creating a new admission record from the patient's existing data, removing any prior discharge, and archiving the patient.
|
|
/// </summary>
|
|
/// <param name="patientId">The unique identifier of the patient to be returned to admissions.</param>
|
|
/// <param name="adm">The admission context used to resolve the unit and point of care for the new admission record.</param>
|
|
public async Task ReturnPatientToAdmissions(ObjectId patientId, Admission adm)
|
|
{
|
|
var patient = await patientService.FindById(patientId);
|
|
if (patient == null)
|
|
{
|
|
logger.LogError("Error returning the patient Id: {Id} to admission", patientId);
|
|
return;
|
|
}
|
|
|
|
|
|
var unit = await unitService.FindById(adm.UnitId);
|
|
if (unit == null)
|
|
{
|
|
logger.LogError("Unit not found by Id. {Name}", patient.UnitId);
|
|
return;
|
|
}
|
|
|
|
if (adm.PointOfCareId != null)
|
|
{
|
|
var poc = await pointOfCareService.FindById(adm.PointOfCareId.Value);
|
|
Admission admission = new()
|
|
{
|
|
Nhc = patient.PatientNumber ?? string.Empty,
|
|
PointOfCareId = poc?.Id,
|
|
UnitId = unit.Id,
|
|
Person = patient.Person ??
|
|
new Person(), //No debería ser null en este punto, pero así quito el warning
|
|
Origin = patient.Origin,
|
|
OriginAux = patient.OriginAux,
|
|
Diagnosis = patient.Diagnosis,
|
|
DiagnosisAux = patient.DiagnosisAux,
|
|
Allergies = patient.Allergies,
|
|
PassiveSitting = patient.PassiveSitting,
|
|
Insulation = patient.Insulation,
|
|
LanguageBarrier = patient.LanguageBarrier,
|
|
AdmissionDate = patient.AdmTime ?? DateTime.UtcNow,
|
|
PatientLocation = patient.Location
|
|
};
|
|
|
|
await InsertAdmission(admission);
|
|
}
|
|
|
|
var dis = await dischargeService.GetDischargeByPatientId(patient.Id);
|
|
if (dis != null) await dischargeService.DeleteDischargeByIdAsync(dis.Id);
|
|
await patientService.ArchivePatient(patient);
|
|
|
|
pointOfCareService.CheckNextAdmission(patient.PointOfCareId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves a list of admissions for the specified patient location. If an error occurs during retrieval, the error is logged and an empty list is returned.
|
|
/// </summary>
|
|
/// <param name="location">The patient location used to filter admissions.</param>
|
|
/// <returns>A task that represents the asynchronous operation. The task result contains a list of admissions matching the specified location, or an empty list if an error occurs.</returns>
|
|
public async Task<List<Admission>> GetAdmissionByLocation(PatientLocation location)
|
|
{
|
|
try
|
|
{
|
|
return await admissionRepository.FindByLocation(location);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
logger.LogError("Error getting admission by patient location {Location} exception: {Ex}", location,
|
|
e.Message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves a list of admissions associated with the specified point of care identifier, enriching each admission with its patient location information when available.
|
|
/// </summary>
|
|
/// <param name="pocId">The identifier of the point of care whose admissions should be retrieved.</param>
|
|
/// <returns>A task representing the asynchronous operation, containing the list of admissions for the given point of care, or an empty list if an error occurs.</returns>
|
|
public async Task<List<Admission>> GetAdmissionByPointOfCareId(ObjectId pocId)
|
|
{
|
|
try
|
|
{
|
|
var result = await admissionRepository.FindByPointOfCareId(pocId);
|
|
foreach (var admission in result)
|
|
{
|
|
var poc = await pointOfCareService.GetInfo(pocId, null, false);
|
|
if (poc != null) admission.PatientLocation = new PatientLocation(poc.UnitName, poc.Bed, poc.Room);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
logger.LogError("Error getting admission by patient PocId {PocId} exception: {Ex}", pocId, e.Message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves all admissions associated with the specified point of care and applies translations according to the given locale in parallel.
|
|
/// </summary>
|
|
/// <param name="pocId">The identifier of the point of care whose admissions will be retrieved.</param>
|
|
/// <param name="locale">The locale used to translate the admission fields.</param>
|
|
/// <returns>A task that represents the asynchronous operation, containing a list of admissions with their fields translated to the specified locale.</returns>
|
|
public async Task<List<Admission>> GetAdmissionByPointOfCareIdAndLocale(ObjectId pocId, LocaleEnum locale)
|
|
{
|
|
var admissions = await GetAdmissionByPointOfCareId(pocId);
|
|
|
|
// Ejecutar todas las traducciones en paralelo
|
|
var translatedAdmissions = await Task.WhenAll(
|
|
admissions.Select(adm => GetAdmissionWithLocale(adm, locale))
|
|
);
|
|
|
|
return translatedAdmissions.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves admissions associated with the specified unit, excluding those linked to a Point of Care (PoC).
|
|
/// If an error occurs during retrieval, the exception is logged and an empty list is returned as a fallback.
|
|
/// </summary>
|
|
/// <param name="unitId">The identifier of the unit whose admissions (without PoC) are being requested.</param>
|
|
/// <returns>A task that returns a list of <see cref="Admission"/> objects for the given unit, or an empty list if an error occurs.</returns>
|
|
public async Task<List<Admission>> GetAdmissionByUnitIdWithOutPoC(ObjectId unitId)
|
|
{
|
|
try
|
|
{
|
|
return await admissionRepository.GetAdmissionByUnitIdWithOutPoC(unitId);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
logger.LogError("Error getting admission by unitId with out PoCId {UnitId} exception: {Ex}", unitId,
|
|
e.Message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asynchronously counts the number of admissions associated with the specified unit identifier by delegating to the admission repository.
|
|
/// Returns 0 and logs the error if the repository operation fails, ensuring the method does not propagate exceptions to the caller.
|
|
/// </summary>
|
|
/// <param name="unitId">The identifier of the unit whose admissions should be counted.</param>
|
|
/// <returns>A task that represents the asynchronous operation. The task result contains the number of admissions for the given unit, or 0 if an error occurs.</returns>
|
|
public async Task<long> CountAdmissionsByUnitId(ObjectId unitId)
|
|
{
|
|
try
|
|
{
|
|
return await admissionRepository.CountByUnitId(unitId);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
logger.LogError("Error getting admission by unitId with out PoCId {UnitId} exception: {Ex}", unitId,
|
|
e.Message);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Searches for a patient by patient number, enriching the current patient record with its point of care and unit name when available, and combines it with archived patient and admission lookups scoped to the specified unit.
|
|
/// </summary>
|
|
/// <param name="patientNumber">The unique patient number used as the primary search key.</param>
|
|
/// <param name="unitId">The identifier of the unit used to filter the archived patient and admission searches.</param>
|
|
/// <returns>A <see cref="PatientSearch"/> aggregating the current patient, archived patient, and admission data, including flags indicating whether the patient exists only in the archive and whether any of the three sources returned a result.</returns>
|
|
public async Task<PatientSearch?> SearchByPatientNumberAndDistinctUnit(string patientNumber, ObjectId unitId)
|
|
{
|
|
var patient = await patientService.FindByPatientNumber(patientNumber);
|
|
if (patient != null)
|
|
{
|
|
if (patient.PointOfCareId.HasValue)
|
|
patient.PointOfCare = await pointOfCareService.FindById(patient.PointOfCareId.Value);
|
|
if (patient.UnitId.HasValue)
|
|
{
|
|
var uni = await unitService.FindById(patient.UnitId.Value);
|
|
if (uni != null)
|
|
patient.UnitString = uni.Name;
|
|
}
|
|
}
|
|
|
|
var archivePatient =
|
|
await patientArchiveRepository.SearchByPatientNumberAndDistinctUnit(patientNumber, unitId);
|
|
var admission = await admissionRepository.SearchByPatientNumberAndDistinctUnit(patientNumber, unitId);
|
|
|
|
var result = new PatientSearch(patient, archivePatient, admission,
|
|
patient == null && archivePatient != null,
|
|
patient != null || archivePatient != null || admission != null);
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves the admission record associated with the specified patient clinical record number (NHC) from the admission repository.
|
|
/// </summary>
|
|
/// <param name="patientNumber">The patient's clinical record number (NHC) used to look up the admission.</param>
|
|
/// <returns>A task that resolves to the matching <see cref="Admission"/> if found, or <c>null</c> when no admission exists for the given patient number.</returns>
|
|
public Task<Admission?> GetAdmissionByPatientNumber(string patientNumber)
|
|
{
|
|
return admissionRepository.FindByNhc(patientNumber);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the master list option for admissions associated with the specified units, records an audit log entry for each modified admission, and broadcasts the updates.
|
|
/// </summary>
|
|
/// <param name="opt">The master list update options to apply to the matching admissions.</param>
|
|
/// <param name="unitList">The collection of units whose admissions are affected by the update.</param>
|
|
/// <param name="typeName">The name of the master list type being modified.</param>
|
|
public async Task UpdatePatientMasterListItemChange(UpdateOptionMasterListDto opt, IEnumerable<Unit> unitList,
|
|
string typeName)
|
|
{
|
|
var unitIds = unitList.Select(x => x.Id).ToList();
|
|
var oldAdmissionList = await admissionRepository.FindByUnitIds(unitIds);
|
|
var admissionUpdatedList = await admissionRepository.UpdateMasterListOption(unitIds, opt, typeName);
|
|
foreach (var admission in admissionUpdatedList)
|
|
{
|
|
var oldAdmission = oldAdmissionList.Find(adm => adm.Id == admission.Id);
|
|
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldAdmission!, admission);
|
|
SendAdmissionBroadcast(admission, OperationType.UpdateAdmission);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes a master list option from patient admissions associated with the specified units and type, records an audit log entry for each affected admission, and broadcasts the admission update when the updated admission is found.
|
|
/// </summary>
|
|
/// <param name="opt">The master list option to remove from the admissions.</param>
|
|
/// <param name="unitList">The collection of units whose admissions will be processed for the deletion.</param>
|
|
/// <param name="typeName">The name of the option type being deleted.</param>
|
|
public async Task DeletePatientMasterListItem(OptionList opt, IEnumerable<Unit> unitList, string typeName)
|
|
{
|
|
var unitIds = unitList.Select(x => x.Id).ToList();
|
|
var oldAdmissionList = await admissionRepository.FindByUnitIds(unitIds);
|
|
var admissionUpdatedList = await admissionRepository.DeleteMasterListOption(unitIds, opt, typeName);
|
|
foreach (var admission in admissionUpdatedList)
|
|
{
|
|
var admissionUpdated = await GetAdmissionByIdAsync(admission.Id);
|
|
var oldAdmission = oldAdmissionList.Find(adm => adm.Id == admission.Id);
|
|
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldAdmission!,
|
|
admissionUpdated);
|
|
if (admissionUpdated != null)
|
|
SendAdmissionBroadcast(admissionUpdated, OperationType.UpdateAdmission);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes an admission API request by performing the appropriate action based on the request type: inserts a new admission, updates an existing one, or deletes it.
|
|
/// Required fields (Nhc, Origin, and Diagnosis) are validated before insert and update operations, and the method exits early when the admission or any required value is missing.
|
|
/// </summary>
|
|
/// <param name="apiRequest">The API request containing the admission payload and the operation type to execute.</param>
|
|
public async Task SaveRequest(ApiRequest apiRequest)
|
|
{
|
|
try
|
|
{
|
|
if (apiRequest.Admission == null)
|
|
return;
|
|
|
|
switch (apiRequest.Type)
|
|
{
|
|
case "NewAdmission":
|
|
{
|
|
if (string.IsNullOrEmpty(apiRequest.Admission.Nhc) ||
|
|
apiRequest.Admission.Origin == null ||
|
|
apiRequest.Admission.Diagnosis == null)
|
|
{
|
|
logger.LogDebug(
|
|
"Error saving admission api request. Some values are required. Admission: {Admission}",
|
|
apiRequest.Admission);
|
|
return;
|
|
}
|
|
|
|
await InsertAdmission(apiRequest.Admission);
|
|
break;
|
|
}
|
|
case "UpdateAdmission":
|
|
{
|
|
if (string.IsNullOrEmpty(apiRequest.Admission.Nhc) ||
|
|
apiRequest.Admission.Origin == null ||
|
|
apiRequest.Admission.Diagnosis == null)
|
|
{
|
|
logger.LogDebug(
|
|
"Error updating admission api request. Some values are required. Admission: {Admission}",
|
|
apiRequest.Admission);
|
|
return;
|
|
}
|
|
|
|
await UpdateAdmissionAsync(apiRequest.Admission);
|
|
break;
|
|
}
|
|
case "DeleteAdmission":
|
|
{
|
|
await DeleteAdmissionAsync(apiRequest.Admission);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError("Exception updating admission {Admission} . Exception: {Ex}", apiRequest.Admission,
|
|
ex.Message);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asynchronously saves the specified API request by scheduling the underlying save operation on a background task.
|
|
/// </summary>
|
|
/// <param name="apiRequest">The API request to persist.</param>
|
|
/// <returns>A task that represents the asynchronous save operation.</returns>
|
|
public Task SaveRequestAsync(ApiRequest apiRequest)
|
|
{
|
|
return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest); }));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles the PointOfCare change when an admission is updated, transferring the assignment from the old PointOfCare to the new one. Updates the status of both PointOfCares (e.g., Reserved, InUse, Available) based on patient occupancy, locks, and admission association, and checks for the next pending admission whenever a PointOfCare becomes available.
|
|
/// </summary>
|
|
/// <param name="admission">The current admission containing the updated PointOfCare identifier.</param>
|
|
/// <param name="oldAdmission">The previous admission state used to identify the original PointOfCare to release.</param>
|
|
private async Task HandlePointOfCareChange(Admission admission, Admission oldAdmission)
|
|
{
|
|
// Check if PointOfCare has changed.
|
|
if (admission.PointOfCareId == oldAdmission.PointOfCareId) return;
|
|
|
|
// Disable new PointOfCare if it exists.
|
|
if (admission.PointOfCareId.HasValue)
|
|
{
|
|
var newPoc = await pointOfCareService.GetInfo(admission.PointOfCareId.Value);
|
|
if (newPoc == null) return;
|
|
|
|
var patientOnNewPoc = await patientService.FindByPointOfCareId(newPoc.Id);
|
|
if (newPoc.AdmissionId == null && newPoc.Status != StatusEnum.PointOfCare.Locked)
|
|
{
|
|
newPoc.Admission = admission;
|
|
newPoc.AdmissionId = admission.Id;
|
|
newPoc.Status = patientOnNewPoc != null
|
|
? StatusEnum.PointOfCare.InUse
|
|
: StatusEnum.PointOfCare.Reserved;
|
|
|
|
await pointOfCareService.Update(newPoc);
|
|
}
|
|
|
|
if (newPoc.Status == StatusEnum.PointOfCare.Available)
|
|
pointOfCareService.CheckNextAdmission(newPoc.Id);
|
|
}
|
|
|
|
// Enable previous PointOfCare if it exists.
|
|
if (oldAdmission.PointOfCareId.HasValue)
|
|
{
|
|
var oldPoc = await pointOfCareService.GetInfo(oldAdmission.PointOfCareId.Value);
|
|
if (oldPoc != null)
|
|
{
|
|
var patientOnOldPoc = await patientService.FindByPointOfCareId(oldPoc.Id);
|
|
if (oldPoc.AdmissionId == oldAdmission.Id && oldPoc.Status != StatusEnum.PointOfCare.Locked)
|
|
{
|
|
oldPoc.Admission = null;
|
|
oldPoc.AdmissionId = null;
|
|
oldPoc.Status = patientOnOldPoc != null
|
|
? StatusEnum.PointOfCare.InUse
|
|
: StatusEnum.PointOfCare.Available;
|
|
|
|
await pointOfCareService.Update(oldPoc);
|
|
}
|
|
}
|
|
|
|
if (oldPoc is { Status: StatusEnum.PointOfCare.Available })
|
|
pointOfCareService.CheckNextAdmission(oldPoc.Id);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the status of the specified point of care, performing a lookup by identifier first. If no point of care is found, the method returns without applying any change.
|
|
/// </summary>
|
|
/// <param name="pointOfCareId">The identifier of the point of care whose status will be updated.</param>
|
|
/// <param name="status">The new status to assign to the point of care.</param>
|
|
private async Task SetPointOfCareStatus(ObjectId pointOfCareId, StatusEnum.PointOfCare status)
|
|
{
|
|
var pointOfCare = await pointOfCareService.FindById(pointOfCareId);
|
|
if (pointOfCare == null) return;
|
|
|
|
await pointOfCareService.SetPointOfCareStatus(pointOfCareId, status);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends an admission broadcast message by routing to the appropriate sender based on whether a point-of-care identifier is set.
|
|
/// Falls back to unit-based delivery when no point-of-care is available; logs and swallows any errors encountered during dispatch.
|
|
/// </summary>
|
|
/// <param name="admission">The admission record to broadcast.</param>
|
|
/// <param name="operation">The operation type associated with the broadcast.</param>
|
|
private async void SendAdmissionBroadcast(Admission admission, OperationType operation)
|
|
{
|
|
try
|
|
{
|
|
if (admission.PointOfCareId == null)
|
|
await SendAdmissionByUnitId(admission, operation);
|
|
else
|
|
await SendAdmissionBroadcastByPoC(admission, operation);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.LogError("Exception sending admission broadcast. Operation type: {Op}. Exception: {Ex}",
|
|
operation.ToString(), ex.Message);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends an admission broadcast to subscribers associated with the admission's Point of Care, grouped and translated by locale.
|
|
/// Logs an error and returns early if the admission has no Point of Care id or the Point of Care cannot be found.
|
|
/// </summary>
|
|
/// <param name="admission">The admission whose broadcast is being sent; its Point of Care is used to select subscribers and locale-specific content.</param>
|
|
/// <param name="operation">The type of operation to send to the subscribers.</param>
|
|
private async Task SendAdmissionBroadcastByPoC(Admission admission, OperationType operation)
|
|
{
|
|
if (!admission.PointOfCareId.HasValue)
|
|
{
|
|
logger.LogError("Error sending admission broadcast. PointOfCare id {Id} not found",
|
|
admission.PointOfCareId);
|
|
return;
|
|
}
|
|
|
|
var pointOfCare = await pointOfCareService.FindById(admission.PointOfCareId.Value);
|
|
if (pointOfCare == null)
|
|
{
|
|
logger.LogError("Error sending admission broadcast. PointOfCare id {Id} not found",
|
|
admission.PointOfCareId);
|
|
return;
|
|
}
|
|
|
|
var pocSubscribers = subscribersService.GetSubscribers().Where(s =>
|
|
s.LocationIds.Contains(admission.PointOfCareId.Value)).GroupBy(h => h.Locale);
|
|
|
|
foreach (var group in pocSubscribers)
|
|
{
|
|
var locale = group.Key ?? LocaleEnum.Default;
|
|
IEnumerable<WsSubscriber> subscribers = group;
|
|
|
|
foreach (var subscriber in subscribers)
|
|
{
|
|
var admissionWithLocale = await GetAdmissionWithLocale(admission, locale);
|
|
_ = clientMessageService.SendAsync(subscriber.Id, operation, admissionWithLocale);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends an admission broadcast to all WebSocket subscribers associated with displays in the admission's unit, grouped by locale so each subscriber receives a localized copy. If the admission has an empty unit id, the broadcast is skipped and an error is logged.
|
|
/// </summary>
|
|
/// <param name="admission">The admission to broadcast, which supplies the target unit identifier.</param>
|
|
/// <param name="operation">The operation type associated with the broadcast message.</param>
|
|
private async Task SendAdmissionByUnitId(Admission admission, OperationType operation)
|
|
{
|
|
if (admission.UnitId == ObjectId.Empty)
|
|
{
|
|
logger.LogError("Error sending admission broadcast. UnitId is null or empty {Admission}", admission);
|
|
return;
|
|
}
|
|
|
|
var displays = await displayService.GetByUnitId(admission.UnitId);
|
|
var displayIds = displays.Select(c => c.Id).ToList();
|
|
var unitSubscribers = subscribersService.GetSubscribers().Where(s =>
|
|
s.DisplayId != null && displayIds.Contains(s.DisplayId.Value)).GroupBy(h => h.Locale);
|
|
foreach (var group in unitSubscribers)
|
|
{
|
|
var locale = group.Key ?? LocaleEnum.Default;
|
|
IEnumerable<WsSubscriber> subscribers = group;
|
|
|
|
foreach (var subscriber in subscribers)
|
|
{
|
|
var admissionWithLocale = await GetAdmissionWithLocale(admission, locale);
|
|
_ = clientMessageService.SendAsync(subscriber.Id, operation, admissionWithLocale);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Localizes the <see cref="Admission"/>'s Origin, Diagnosis, and Insulation names by resolving them
|
|
/// against locale-specific master lists associated with the admission's unit. If the unit is not found,
|
|
/// or any of the referenced master list lookups fail or contain no matching option, the original
|
|
/// admission values are preserved as a fallback.
|
|
/// </summary>
|
|
/// <param name="admission">The admission whose reference names will be updated with localized values.</param>
|
|
/// <param name="locale">The locale used to retrieve the appropriate master list translations.</param>
|
|
/// <returns>The same <see cref="Admission"/> instance with its localized reference names applied when available.</returns>
|
|
private async Task<Admission> GetAdmissionWithLocale(Admission admission, LocaleEnum locale)
|
|
{
|
|
var unit = await unitService.FindById(admission.UnitId);
|
|
if (unit == null) return admission;
|
|
if (admission.Origin?.Name != null && unit.OriginListId.HasValue)
|
|
{
|
|
if (await masterListServiceFactory.GetMasterListById(MasterListType.OriginList,
|
|
unit.OriginListId.Value, locale) is OriginList list)
|
|
{
|
|
var listOrigin = list.Options.FirstOrDefault(c => c.Id == admission.Origin.Id)?.Name;
|
|
if (listOrigin != null)
|
|
admission.Origin.Name = listOrigin;
|
|
}
|
|
}
|
|
|
|
if (admission.Diagnosis?.Name != null && unit.DiagnosisListId.HasValue)
|
|
{
|
|
if (await masterListServiceFactory.GetMasterListById(MasterListType.DiagnosisList,
|
|
unit.DiagnosisListId.Value, locale) is DiagnosisList list)
|
|
{
|
|
var listDiagnosis = list.Options.FirstOrDefault(c => c.Id == admission.Diagnosis.Id)?.Name;
|
|
if (listDiagnosis != null)
|
|
admission.Diagnosis.Name = listDiagnosis;
|
|
}
|
|
}
|
|
|
|
if (admission.Insulation?.Name != null && unit.InsulationListId.HasValue)
|
|
{
|
|
if (await masterListServiceFactory.GetMasterListById(MasterListType.InsulationList,
|
|
unit.InsulationListId.Value, locale) is InsulationList list)
|
|
{
|
|
var listInsulation = list.Options.FirstOrDefault(c => c.Id == admission.Insulation.Id)?.Name;
|
|
if (listInsulation != null)
|
|
admission.Insulation.Name = listInsulation;
|
|
}
|
|
}
|
|
|
|
return admission;
|
|
}
|
|
} |