diff --git a/Marco.Pms.Helpers/Utility/UtilityMongoDBHelper.cs b/Marco.Pms.Helpers/Utility/UtilityMongoDBHelper.cs index 7159850..c6e93f3 100644 --- a/Marco.Pms.Helpers/Utility/UtilityMongoDBHelper.cs +++ b/Marco.Pms.Helpers/Utility/UtilityMongoDBHelper.cs @@ -25,8 +25,30 @@ namespace Marco.Pms.Helpers.Utility #region =================================================================== Update Log Helper Functions =================================================================== public async Task PushToUpdateLogsAsync(UpdateLogsObject oldObject, string collectionName) { - var collection = _mongoDatabase.GetCollection(collectionName); - await collection.InsertOneAsync(oldObject); + try + { + var collection = _mongoDatabase.GetCollection(collectionName); + await collection.InsertOneAsync(oldObject); + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception occured while saving object of update logs in collection: {Collection}", collectionName); + } + } + public async Task PushListToUpdateLogsAsync(List oldObjects, string collectionName) + { + try + { + var collection = _mongoDatabase.GetCollection(collectionName); + if (oldObjects.Any()) + { + await collection.InsertManyAsync(oldObjects); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception occured while saving list of update logs in collection: {Collection}", collectionName); + } } public async Task> GetFromUpdateLogsByEntityIdAsync(Guid entityId, string collectionName) diff --git a/Marco.Pms.Services/Controllers/DirectoryController.cs b/Marco.Pms.Services/Controllers/DirectoryController.cs index c457065..bdc8598 100644 --- a/Marco.Pms.Services/Controllers/DirectoryController.cs +++ b/Marco.Pms.Services/Controllers/DirectoryController.cs @@ -80,6 +80,13 @@ namespace Marco.Pms.Services.Controllers var response = await _directoryService.GetOrganizationListAsync(tenantId, loggedInEmployee); return Ok(response); } + [HttpGet("designations")] + public async Task GetDesignationList() + { + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _directoryService.GetDesignationListAsync(tenantId, loggedInEmployee); + return Ok(response); + } #endregion diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index 84c32a7..6eec11b 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -251,9 +251,11 @@ namespace Marco.Pms.Services.MappingProfiles CreateMap(); CreateMap(); + CreateMap(); CreateMap(); CreateMap(); + CreateMap(); CreateMap(); diff --git a/Marco.Pms.Services/Service/DirectoryService.cs b/Marco.Pms.Services/Service/DirectoryService.cs index 0a119e3..3aa847b 100644 --- a/Marco.Pms.Services/Service/DirectoryService.cs +++ b/Marco.Pms.Services/Service/DirectoryService.cs @@ -1,10 +1,12 @@ using AutoMapper; using Marco.Pms.DataAccess.Data; +using Marco.Pms.Helpers.Utility; using Marco.Pms.Model.Directory; using Marco.Pms.Model.Dtos.Directory; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Mapper; +using Marco.Pms.Model.MongoDBModels.Utility; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Directory; using Marco.Pms.Model.ViewModels.Master; @@ -26,7 +28,13 @@ namespace Marco.Pms.Services.Service private readonly ILoggingService _logger; private readonly UserHelper _userHelper; private readonly IMapper _mapper; - private readonly PermissionServices _permissionServices; + private readonly UtilityMongoDBHelper _updateLogsHelper; + + + private static readonly string contactCollection = "ContactModificationLog"; + private static readonly string contactPhoneCollection = "ContactPhoneModificationLog"; + private static readonly string contactEmailCollection = "ContactEmailModificationLog"; + public DirectoryService( IDbContextFactory dbContextFactory, @@ -34,8 +42,7 @@ namespace Marco.Pms.Services.Service ILoggingService logger, IServiceScopeFactory serviceScopeFactory, UserHelper userHelper, - IMapper mapper, - PermissionServices permissionServices) + IMapper mapper) { _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); _context = context ?? throw new ArgumentNullException(nameof(context)); @@ -43,7 +50,8 @@ namespace Marco.Pms.Services.Service _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); _userHelper = userHelper ?? throw new ArgumentNullException(nameof(userHelper)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); - _permissionServices = permissionServices ?? throw new ArgumentNullException(nameof(permissionServices)); + using var scope = serviceScopeFactory.CreateScope(); + _updateLogsHelper = scope.ServiceProvider.GetRequiredService(); } #region =================================================================== Contact APIs =================================================================== @@ -747,6 +755,66 @@ namespace Marco.Pms.Services.Service return ApiResponse.ErrorResponse("An internal server error occurred.", ExceptionMapper(ex), 500); } } + public async Task> GetDesignationListAsync(Guid tenantId, Employee loggedInEmployee) + { + // --- Parameter Validation --- + // Fail fast if essential parameters are not provided. + ArgumentNullException.ThrowIfNull(loggedInEmployee); + + var employeeId = loggedInEmployee.Id; + + try + { + // --- 1. Permission Check --- + // Verify that the employee has at least one of the required permissions to view this data. + // This prevents unauthorized data access early in the process. + var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(employeeId); + + if (!hasAdminPermission && !hasManagerPermission && !hasUserPermission) + { + // Log the specific denial reason for security auditing. + _logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to get designations list for tenant {TenantId} due to lack of permissions.", employeeId, tenantId); + // Return a strongly-typed error response. + return ApiResponse.ErrorResponse("Access Denied", "You do not have permission to perform this action.", 403); + } + + // --- 2. Database Query --- + // Build and execute the database query efficiently. + _logger.LogDebug("Fetching designations list for Tenant {TenantId} by Employee {EmployeeId}", tenantId, employeeId); + + var designationsList = await _context.Contacts + // Filter contacts by the specified tenant to ensure data isolation and Filter out contacts that do not have an designations name to ensure data quality. + .Where(c => c.TenantId == tenantId && !string.IsNullOrEmpty(c.Designation)) + + // Project only the 'Designation' column. This is a major performance optimization + // as it avoids loading entire 'Contact' entities into memory. + .Select(c => c.Designation) + // Let the database perform the distinct operation, which is highly efficient. + .Distinct() + // Execute the query asynchronously and materialize the results into a list. + .ToListAsync(); + + // --- 3. Success Response --- + // Log the successful operation with key details. + _logger.LogInfo("Successfully fetched {DesignationsCount} distinct designations for Tenant {TenantId} for employee {EmployeeId}", designationsList.Count, tenantId, employeeId); + + // Return a strongly-typed success response with the data and a descriptive message. + return ApiResponse.SuccessResponse( + designationsList, + $"{designationsList.Count} unique designation(s) found.", + 200 + ); + } + catch (Exception ex) + { + // --- 4. Exception Handling --- + // Log the full exception details for effective debugging, including context. + _logger.LogError(ex, "An unexpected error occurred while fetching designation list for Tenant {TenantId} by Employee {EmployeeId}", tenantId, employeeId); + + // Return a generic, strongly-typed error response to the client to avoid leaking implementation details. + return ApiResponse.ErrorResponse("An internal server error occurred.", ExceptionMapper(ex), 500); + } + } #endregion @@ -968,6 +1036,19 @@ namespace Marco.Pms.Services.Service var allTags = await context.ContactTagMasters.Where(t => t.TenantId == tenantId).ToListAsync(); var tagNameLookup = allTags.ToDictionary(t => t.Name.ToLowerInvariant(), t => t); + var contactObject = _updateLogsHelper.EntityToBsonDocument(contact); + + var contactUpdateLog = new UpdateLogsObject + { + EntityId = contact.Id.ToString(), + UpdatedById = loggedInEmployee.Id.ToString(), + OldObject = contactObject, + UpdatedAt = DateTime.UtcNow + }; + + List phoneUpdateLogs = new List(); + List emailUpdateLogs = new List(); + // ---------------------- Update Phones ----------------------- if (updateContact.ContactPhones != null) { @@ -975,11 +1056,26 @@ namespace Marco.Pms.Services.Service foreach (var phoneDto in updateContact.ContactPhones) { - var phoneEntity = phoneDto.ToContactPhoneFromUpdateContactPhoneDto(tenantId, contact.Id); - if (phoneDto.Id != null && phoneDto.Id != Guid.Empty && phoneIds.Contains(phoneEntity.Id)) + var phoneEntity = _mapper.Map(phoneDto); + phoneEntity.TenantId = tenantId; + phoneEntity.ContactId = contact.Id; + var existingPhone = phones.FirstOrDefault(p => p.Id == phoneEntity.Id); + if (phoneDto.Id != null && phoneDto.Id != Guid.Empty && existingPhone != null) + { + var phoneObject = _updateLogsHelper.EntityToBsonDocument(existingPhone); + phoneUpdateLogs.Add(new UpdateLogsObject + { + EntityId = existingPhone.Id.ToString(), + UpdatedById = loggedInEmployee.Id.ToString(), + OldObject = phoneObject, + UpdatedAt = DateTime.UtcNow + }); context.ContactsPhones.Update(phoneEntity); + } else + { context.ContactsPhones.Add(phoneEntity); + } } // Remove phones not updated in payload @@ -987,6 +1083,14 @@ namespace Marco.Pms.Services.Service { if (!updatedPhoneIds.Contains(phone.Id)) { + var phoneObject = _updateLogsHelper.EntityToBsonDocument(phone); + phoneUpdateLogs.Add(new UpdateLogsObject + { + EntityId = phone.Id.ToString(), + UpdatedById = loggedInEmployee.Id.ToString(), + OldObject = phoneObject, + UpdatedAt = DateTime.UtcNow + }); context.ContactsPhones.Remove(phone); } } @@ -1003,11 +1107,29 @@ namespace Marco.Pms.Services.Service foreach (var emailDto in updateContact.ContactEmails) { - var emailEntity = emailDto.ToContactEmailFromUpdateContactEmailDto(tenantId, contact.Id); - if (emailDto.Id != null && emailDto.Id != Guid.Empty && emailIds.Contains(emailEntity.Id)) + var emailEntity = _mapper.Map(emailDto); + emailEntity.TenantId = tenantId; + emailEntity.ContactId = contact.Id; + + var existingEmail = emails.FirstOrDefault(e => e.Id == emailEntity.Id); + + if (emailDto.Id != null && emailDto.Id != Guid.Empty && existingEmail != null) + { + var emailObject = _updateLogsHelper.EntityToBsonDocument(existingEmail); + emailUpdateLogs.Add(new UpdateLogsObject + { + EntityId = existingEmail.Id.ToString(), + UpdatedById = loggedInEmployee.Id.ToString(), + OldObject = emailObject, + UpdatedAt = DateTime.UtcNow + }); + context.ContactsEmails.Update(emailEntity); + } else + { context.ContactsEmails.Add(emailEntity); + } } // Remove emails not updated in payload @@ -1015,6 +1137,14 @@ namespace Marco.Pms.Services.Service { if (!updatedEmailIds.Contains(email.Id)) { + var emailObject = _updateLogsHelper.EntityToBsonDocument(email); + phoneUpdateLogs.Add(new UpdateLogsObject + { + EntityId = email.Id.ToString(), + UpdatedById = loggedInEmployee.Id.ToString(), + OldObject = emailObject, + UpdatedAt = DateTime.UtcNow + }); context.ContactsEmails.Remove(email); } } @@ -1047,6 +1177,7 @@ namespace Marco.Pms.Services.Service { if (!incomingBucketIds.Contains(contactBucket.BucketId)) { + context.ContactBucketMappings.Remove(contactBucket); } } @@ -1163,34 +1294,56 @@ namespace Marco.Pms.Services.Service } // Reload updated contact and related data for response, using a fresh context - using var responseContext = _dbContextFactory.CreateDbContext(); - - var reloadedContact = await responseContext.Contacts - .Include(c => c.ContactCategory) - .FirstOrDefaultAsync(c => c.Id == id && c.IsActive && c.TenantId == tenantId) ?? new Contact(); - - var responsePhones = await responseContext.ContactsPhones.AsNoTracking().Where(p => p.ContactId == reloadedContact.Id).ToListAsync(); - var responseEmails = await responseContext.ContactsEmails.AsNoTracking().Where(e => e.ContactId == reloadedContact.Id).ToListAsync(); - var responseContactTags = await responseContext.ContactTagMappings.AsNoTracking().Where(t => t.ContactId == reloadedContact.Id).ToListAsync(); - var responseContactBuckets = await responseContext.ContactBucketMappings.AsNoTracking().Where(cb => cb.ContactId == reloadedContact.Id).ToListAsync(); - var responseContactProjects = await responseContext.ContactProjectMappings.AsNoTracking().Where(cp => cp.ContactId == reloadedContact.Id).ToListAsync(); - - var tagIdsForResponse = responseContactTags.Select(t => t.ContactTagId).Distinct().ToList(); - var tagsForResponse = await responseContext.ContactTagMasters.Where(t => tagIdsForResponse.Contains(t.Id)).ToListAsync(); - - // Map entities to view models - var contactVM = reloadedContact.ToContactVMFromContact(); - - contactVM.ContactPhones = responsePhones.Select(p => p.ToContactPhoneVMFromContactPhone()).ToList(); - contactVM.ContactEmails = responseEmails.Select(e => e.ToContactEmailVMFromContactEmail()).ToList(); - contactVM.Tags = responseContactTags.Select(ctm => + var responseTask = Task.Run(async () => { - var tag = tagsForResponse.Find(t => t.Id == ctm.ContactTagId); - return tag != null ? tag.ToContactTagVMFromContactTagMaster() : new ContactTagVM(); - }).ToList(); + using var responseContext = _dbContextFactory.CreateDbContext(); - contactVM.BucketIds = responseContactBuckets.Select(cb => cb.BucketId).ToList(); - contactVM.ProjectIds = responseContactProjects.Select(cp => cp.ProjectId).ToList(); + var reloadedContact = await responseContext.Contacts + .Include(c => c.ContactCategory) + .FirstOrDefaultAsync(c => c.Id == id && c.IsActive && c.TenantId == tenantId) ?? new Contact(); + + var responsePhones = await responseContext.ContactsPhones.AsNoTracking().Where(p => p.ContactId == reloadedContact.Id).ToListAsync(); + var responseEmails = await responseContext.ContactsEmails.AsNoTracking().Where(e => e.ContactId == reloadedContact.Id).ToListAsync(); + var responseContactTags = await responseContext.ContactTagMappings.AsNoTracking().Where(t => t.ContactId == reloadedContact.Id).ToListAsync(); + var responseContactBuckets = await responseContext.ContactBucketMappings.AsNoTracking().Where(cb => cb.ContactId == reloadedContact.Id).ToListAsync(); + var responseContactProjects = await responseContext.ContactProjectMappings.AsNoTracking().Where(cp => cp.ContactId == reloadedContact.Id).ToListAsync(); + + var tagIdsForResponse = responseContactTags.Select(t => t.ContactTagId).Distinct().ToList(); + var tagsForResponse = await responseContext.ContactTagMasters.Where(t => tagIdsForResponse.Contains(t.Id)).ToListAsync(); + + // Map entities to view models + var response = reloadedContact.ToContactVMFromContact(); + + response.ContactPhones = responsePhones.Select(p => p.ToContactPhoneVMFromContactPhone()).ToList(); + response.ContactEmails = responseEmails.Select(e => e.ToContactEmailVMFromContactEmail()).ToList(); + response.Tags = responseContactTags.Select(ctm => + { + var tag = tagsForResponse.Find(t => t.Id == ctm.ContactTagId); + return tag != null ? tag.ToContactTagVMFromContactTagMaster() : new ContactTagVM(); + }).ToList(); + + response.BucketIds = responseContactBuckets.Select(cb => cb.BucketId).ToList(); + response.ProjectIds = responseContactProjects.Select(cp => cp.ProjectId).ToList(); + return response; + }); + + var contactUpdateLogTask = Task.Run(async () => + { + await _updateLogsHelper.PushToUpdateLogsAsync(contactUpdateLog, contactCollection); + }); + var phoneUpdateLogTask = Task.Run(async () => + { + await _updateLogsHelper.PushListToUpdateLogsAsync(phoneUpdateLogs, contactPhoneCollection); + }); + var emailUpdateLogTask = Task.Run(async () => + { + await _updateLogsHelper.PushListToUpdateLogsAsync(emailUpdateLogs, contactEmailCollection); + }); + + + await Task.WhenAll(responseTask, contactUpdateLogTask, phoneUpdateLogTask, emailUpdateLogTask); + + var contactVM = responseTask.Result; _logger.LogInfo("Contact {ContactId} successfully updated by employee {EmployeeId}.", contact.Id, loggedInEmployee.Id); diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IDirectoryService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IDirectoryService.cs index bd8468d..07369a8 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IDirectoryService.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IDirectoryService.cs @@ -11,6 +11,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> GetContactsListByBucketIdAsync(Guid bucketId, Guid tenantId, Employee loggedInEmployee); Task> GetContactProfileAsync(Guid id, Guid tenantId, Employee loggedInEmployee); Task> GetOrganizationListAsync(Guid tenantId, Employee loggedInEmployee); + Task> GetDesignationListAsync(Guid tenantId, Employee loggedInEmployee); Task> CreateContactAsync(CreateContactDto createContact, Guid tenantId, Employee loggedInEmployee); Task> UpdateContactAsync(Guid id, UpdateContactDto updateContact, Guid tenantId, Employee loggedInEmployee); Task> DeleteContactAsync(Guid id, bool active, Guid tenantId, Employee loggedInEmployee);