using adas_core.Application.Exceptions; using adas_core.Application.Services.Interfaces; using adas_core.Domain.Enums; using adas_core.Domain.Models; using adas_core.Domain.Models.AppSettings; using adas_core.Domain.Models.DTO; using adas_core.Domain.Models.MongoModels; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using MongoDB.Bson; using Serilog; using Patient = adas_core.Domain.Models.MongoModels.Patient; namespace adas_core.Application.Services; /// /// Provides the administrative panel operations defined by , integrating patient, admission, discharge, authentication, medicine, point-of-care, unit, display, and configurable observation services. /// /// /// The service receives its collaborators through primary constructor injection, including , , , , , , , , and , along with configuration via and logging through . /// /// public class AdminPanelService( IOptions apiSettings, IPatientService patientService, IConfigObservationService configObservationService, IMedicineService medicineService, IPointOfCareService pocService, IUnitService unitService, ILogger logger, IAdmissionService admissionService, IAuthService authService, IDischargeService dischargeService, IDisplayService displayService) : IAdminPanelService { private readonly List _defaultIdRecord = apiSettings.Value.DefaultIdRecord ?? []; #region Patient /// /// Archives the specified patient via the patient service and logs the operation. Returns true on success; any exception thrown by the underlying service is written to the console and rethrown. /// /// The patient to archive. /// true if the patient was archived successfully. /// Rethrows any exception thrown by the underlying patient service after logging it to the console. /// public async Task ArchivePatient(Patient patient) { try { await patientService.ArchivePatient(patient); logger.LogDebug("archived patientid {patientid} from ADMPanel", patient.Id); return true; } catch (Exception e) { Console.WriteLine(e); throw; } } /// /// Creates a new patient from an ADMPanel request, generating a new identifier and persisting it through the patient service. /// /// The ADMPanel request containing the data used to populate the new patient. /// The newly created with its generated identifier. /// public async Task CreatePatient(AdmPanelRequest admRequest) { var patient = new Patient { Id = ObjectId.GenerateNewId() }; await UpdateNewPatient(patient, admRequest); await patientService.Insert(patient); logger.LogDebug("Inserted {patientid} from ADMPanel", patient.Id); return patient; } /// /// Updates an existing with data from an , applying patient fields, /// person data, default identifier records, and unit/point-of-care (location) assignment, including conflict and not-found validations. /// /// The patient entity to be updated in place. /// The admission panel request containing the new values to apply to the patient. /// Thrown when the target PointOfCareId is already occupied by another patient. /// Thrown when the requested PointOfCareId does not exist. /// The asynchronous representing the update operation. /// private async Task UpdateNewPatient(Patient patient, AdmPanelRequest admRequest) { if (!string.IsNullOrEmpty(admRequest.PatientNumber)) patient.PatientNumber = admRequest.PatientNumber; if (admRequest.AdmTime.HasValue) patient.AdmTime = admRequest.AdmTime; if (admRequest.Patient != null && !admRequest.Patient.IsEmptyDontCheckIds()) patient.Person = admRequest.Patient; if (admRequest.Patient != null && (admRequest.Patient.Ids == null || admRequest.Patient.Ids.Count == 0)) { if (patient.Person is { Ids: null }) patient.Person.Ids = new Dictionary(); foreach (var item in _defaultIdRecord) patient.Person?.Ids?.Add(item, admRequest?.PatientNumber ?? "null"); } patient.Person?.SetIds(patient.Person?.Ids ?? new Dictionary()); if (admRequest is { UnitId: not null }) { var unit = await unitService.FindById(admRequest.UnitId); patient.UnitId = unit?.Id ?? admRequest.UnitId; patient.UnitString = unit?.Name; if (admRequest is { PointOfCareId: not null }) { //El paciente existe en la localizacion no te dejo insertarlo var patientInLocation = await patientService.FindByPointOfCareId(admRequest.PointOfCareId.Value); if (patientInLocation != null) { logger.LogError( "trying to insert patientid {patientid} in to location already in use. PointOfcareId: {poc}", patient.Id, patient.PointOfCareId); throw new Exception($"patient already in location: {patient.Location}"); } var poc = await pocService.FindById(admRequest.PointOfCareId.Value); if (poc == null) { logger.LogError( "trying to insert patientid {patientid} in to location not found. PointOfcareId: {poc}", patient.Id, admRequest.PointOfCareId); throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); } await pocService.SetPointOfCareStatus(poc.Id, StatusEnum.PointOfCare.InUse); patient.Location = new PatientLocation ( bed: poc.Bed, room: poc.Room, unitName: unit?.Name ); patient.PointOfCareId = poc.Id; } else { var pocUnknown = await pocService.FindByBedAndUnitId(VirtualPointOfCare.Unknown.ToString(), patient.UnitId); patient.PointOfCareId = pocUnknown?.Id; } patient.UpdateDate = DateTime.UtcNow; } } /// /// Retrieves a patient by their unique identifier from the patient service. /// Throws a not-found exception when no matching patient exists for the specified identifier. /// /// The unique identifier of the patient to look up. /// The patient matching the provided identifier. /// Thrown when no patient is found for the specified identifier. /// public async Task FindPatientById(ObjectId id) { return await patientService.FindById(id) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); } /// /// Finds a patient at the specified location by delegating to the patient service. /// Returns null when no patient is found at the given location. /// /// The location to search for a patient at. /// A if one is found at the specified location; otherwise, null. /// public async Task FindPatientByLocation(PatientLocation location) { return await patientService.FindByLocation(location); } /// /// Retrieves a patient by their unique patient number by delegating to the patient service. /// Returns null when no matching patient is found. /// /// The unique patient number used to look up the patient. /// A if a match is found; otherwise, null. /// public async Task FindPatientByPatientNumber(string patientNumber) { return await patientService.FindByPatientNumber(patientNumber); } /// /// Updates the patient information based on the provided administrative panel request, persisting only the patient-related fields and detecting whether the patient number has changed. /// /// The administrative panel request containing the new patient number and the updated patient data to apply. /// The existing patient record currently stored in the database that will be updated. /// A task that resolves to true when the patient data is successfully updated. /// Thrown when the request is missing the patient number, the patient payload is null, or the patient payload is empty. /// public async Task UpdatePatientData(AdmPanelRequest request, Patient oldPatient) { //traer el paciente que se quiere actualizar con los valores que tenga en la base de datos a una variable //actualizar unicamente los campos que tengan que ver con los datos del paciente if (request.PatientNumber == null || request.Patient == null || request.Patient.IsEmptyDontCheckIds()) { logger.LogError("Patient Data not updated. Old Patient:{oldPatient}. Api Request {request}", oldPatient, request); throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); } var patientNumberChanged = oldPatient.PatientNumber != request.PatientNumber; await patientService.UpdatePatientData(oldPatient.Id, request.PatientNumber, request.Patient, patientNumberChanged); return true; } /// /// Updates the location of a patient identified by the source location in , relocating any occupant of the target location to the point of care and creating a new record when no matching patient is found. /// /// The containing the original and the target values used to find and reassign the patient. /// A that resolves to true once the update or insertion has completed. /// public async Task UpdatePatientLocation(AdmPanelRequest request) { var patient = await patientService.FindByLocation(request.OldLocation); var patientExistsInLocation = await patientService.FindByLocation(request.Location); //Si ya hay un paciente en esa localización if (patientExistsInLocation != null) //Si hay un paciente distinto en la nueva localización movemos al anterior if (patient != null && patientExistsInLocation.PatientNumber != patient.PatientNumber) await patientService.UpdateLocation(patientExistsInLocation.Id, new PatientLocation(VirtualPointOfCare.Pushed.ToString(), ObjectId.GenerateNewId().ToString())); if (patient != null) { if (request.Location?.Bed == null || string.IsNullOrEmpty(request.Location.Bed)) request.Location = new PatientLocation(VirtualPointOfCare.Pushed.ToString(), ObjectId.GenerateNewId().ToString()); //to solve problems when empty beds with not scapped "" request.Location.Bed = request.Location?.Bed?.Replace("\"", ""); await patientService.UpdateLocation(patient.Id, request.Location); patient = await patientService.FindById(patient.Id); if (patient != null) await patientService.Update(patient); } else { patient = new Patient { Id = ObjectId.GenerateNewId() }; await patientService.Insert(patient); } return true; } /// /// Asynchronously retrieves a based on the provided admission panel request, using location-aware lookup when the location is not fully empty. /// When a location is present, the search is performed including location criteria; otherwise the location parameter is ignored. /// /// The admission panel request containing the patient identification data and optional location used to locate the patient. /// The matching if found. /// Thrown when no patient matches the provided request criteria. /// public async Task FindPatient(AdmPanelRequest request) { var findByLocation = !request.Location?.IsFullEmpty(); return await patientService.FindPatient(request.PatientId, request.PatientNumber, request.Location, findByLocation ?? false) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); } /// /// Retrieves a patient associated with the specified location, throwing a not-found exception if no patient is found. /// /// The location used to look up the patient. /// The patient found at the specified location. /// Thrown when no patient is found for the given location. /// public async Task FindByLocation(PatientLocation location) { return await patientService.FindByLocation(location) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); } #endregion #region ConfigObservations /// /// Creates a new configuration based on the provided observation data. /// Throws a conflict exception when the underlying creation operation fails (returns null), otherwise returns true. /// /// The configuration observation containing the data to persist. /// A task that resolves to true when the configuration is created successfully. /// Thrown when the configuration creation fails, indicated by a null result from the service call. /// public async Task CreateConfig(ConfigObservation configObservation) { _ = await configObservationService.CreateConfig(configObservation) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed); return true; } /// /// Updates a configuration observation by delegating to the configuration service. If the service returns a null result, indicating a failure to update, a conflict exception is thrown. /// /// The configuration observation to update. /// A task that resolves to true when the configuration is successfully updated. /// Thrown when the underlying update operation fails, as indicated by a null result from the service. /// public async Task UpdateConfig(ConfigObservation configObservation) { _ = await configObservationService.UpdateConfig(configObservation) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); return true; } /// /// Deletes a config observation item by its identifier. Throws a conflict exception if the underlying removal service returns a null result, indicating the delete could not be completed. /// /// The unique identifier of the config observation item to delete. /// true when the config observation item is successfully removed. /// Thrown when the removal operation returns a null result, signaling a conflict with the delete request. /// public async Task DeleteConfigObservationItem(ObjectId id) { _ = await configObservationService.RemoveConfigItem(id) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); return true; } #endregion #region Unit /// /// Inserts a new unit and automatically creates the default Points of Care associated with it, one for each value of the enum. /// Each created Point of Care is set to Available status, using the enum value name for both Room and Bed, and its identifier is added to the unit's PointOfCareIds before updating the unit. Returns null if the initial unit insertion fails. /// /// The unit to be inserted. /// The inserted unit with its associated Point of Care identifiers, or null if the unit could not be inserted. /// public async Task InsertUnit(Unit unit) { //Insertamos la unidad y creamos los PoCs por defecto para esa unidad var result = await unitService.InsertOne(unit); if (result != null) { // Por cada valor del enum VirtualPointOfCare, creamos un PointOfCare asociado a la unidad foreach (var pocEnum in Enum.GetValues(typeof(VirtualPointOfCare)).Cast()) { var poc = new PointOfCare { UnitId = result.Id, Status = StatusEnum.PointOfCare.Available, Room = pocEnum.ToString(), Bed = pocEnum.ToString() }; var insertedPoc = await pocService.InsertPointOfCare(poc); if (insertedPoc != null) { result.PointOfCareIds ??= []; result.PointOfCareIds.Add(insertedPoc.Id); } } // Actualizamos la unidad con los nuevos PointOfCareIds await unitService.UpdateUnit(result); } return result; } /// /// Asynchronously creates a for the specified unit, populated with counts of related entities such as admissions, discharges, displays, patients, points of care, and virtual points of care. Returns null if any of the underlying count operations fail, logging the error. /// /// The unit for which to build the dependency information DTO. /// A task that resolves to a containing the unit's dependency counts, or null if an error occurs while retrieving the counts. /// public async Task GetUnitDependencyDto(Unit unit) { try { return new UnitInfoDto(unit) { Admissions = await admissionService.CountAdmissionsByUnitId(unit.Id), Discharges = await dischargeService.CountDischargesByUnitId(unit.Id), Displays = await displayService.CountDisplaysByUnitId(unit.Id), Patients = await patientService.CountPatientsByUnitId(unit.Id), PointOfCares = await pocService.CountPoCsByUnitId(unit.Id), VirtualPointOfCares = await pocService.CountVirtualPoCsByUnitId(unit.Id) }; } catch (Exception e) { logger.LogError(e.Message); return null; } } /// /// Deletes a unit identified by its identifier, cascading the removal to all associated resources /// (admissions, discharges, authorizations, displays, and points of care). Throws a /// if the unit does not exist, and a if the unit still has patients assigned to it. /// /// The identifier of the unit to delete. /// true if the unit and its related resources were successfully deleted; otherwise, false when an error is caught and logged. /// Thrown when no unit is found for the specified . /// Thrown when the unit has patients assigned to it, preventing deletion. /// /// /// public async Task DeleteUnitById(ObjectId unitId) { try { var unit = await unitService.FindById(unitId); if (unit == null) throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); //No se puede borrar una unidad que est� en uso var patients = await patientService.CountPatientsByUnitId(unitId); if (patients > 0) //Si tiene pacientes no se permite borrado throw new ConflictException(HttpEnum.ErrorMessage.ConflictResourceInUse); //Borramos admisiones await admissionService.DeleteAdmissionsByUnitId(unitId); //discharges await dischargeService.DeleteDischargesByUnitId(unitId); //Borramos Authorizations await authService.DeleteByUnitId(unitId); //Displays await displayService.DeleteDisplaysByUnitId(unitId); //PointOfCares await pocService.DeletePoCsByUnitId(unitId); //Una vez borrados los recursos asociados a la unidad, borramos la unidad await unitService.DeleteUnitById(unit); return true; } catch (Exception e) { Log.Error(e.Message); return false; } } #endregion #region Medicine /// /// Retrieves a medicine by its unique identifier, or throws an exception if the medicine cannot be found. /// /// The unique identifier of the medicine to retrieve. /// The medicine matching the specified identifier. /// Thrown when no medicine is found with the specified identifier. /// public async Task GetMedicineById(ObjectId medicineId) { return await medicineService.GetMedicineById(medicineId) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); } /// /// Creates a new medicine by forwarding the request to the medicine service. Throws a conflict exception if the service is unable to create the medicine. /// /// The medicine to be created. /// The created . /// Thrown when the medicine service fails to create the medicine. /// public async Task PostMedicine(Medicine medicine) { var newMedicine = await medicineService.PostMedicine(medicine) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed); return newMedicine; } /// /// Updates an existing by delegating to the medicine service. /// Throws a when the service returns a null result, indicating the update could not be applied. /// /// The medicine entity containing the updated information to persist. /// The updated returned by the service. /// Thrown when the update operation fails and the service returns a null result. /// public async Task UpdateMedicine(Medicine medicine) { var updatedMedicine = await medicineService.UpdateMedicine(medicine) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); return updatedMedicine; } /// /// Deletes a medicine record by its identifier, validating the identifier format and confirming successful removal. /// /// The string representation of the medicine's ObjectId to delete. /// A task that resolves to true when the medicine has been successfully deleted. /// Thrown when is not a valid ObjectId format. /// Thrown when the medicine still exists after the delete operation, indicating the deletion failed. /// public async Task DeleteMedicineById(string medicineId) { if (!ObjectId.TryParse(medicineId, out var objectId)) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); await medicineService.DeleteMedicineById(objectId); _ = await medicineService.GetMedicineById(ObjectId.Parse(medicineId)) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); return true; } #endregion }