diff --git a/Marco.Pms.Model/ViewModels/Directory/ContactProfileVM.cs b/Marco.Pms.Model/ViewModels/Directory/ContactProfileVM.cs index 9e8f4cb..4969dfe 100644 --- a/Marco.Pms.Model/ViewModels/Directory/ContactProfileVM.cs +++ b/Marco.Pms.Model/ViewModels/Directory/ContactProfileVM.cs @@ -21,6 +21,6 @@ namespace Marco.Pms.Model.ViewModels.Directory public List Projects { get; set; } = new List(); public List Buckets { get; set; } = new List(); public List Tags { get; set; } = new List(); - public List Notes { get; set; } = new List(); + //public List Notes { get; set; } = new List(); } } diff --git a/Marco.Pms.Services/Controllers/DirectoryController.cs b/Marco.Pms.Services/Controllers/DirectoryController.cs index 16da931..f63907b 100644 --- a/Marco.Pms.Services/Controllers/DirectoryController.cs +++ b/Marco.Pms.Services/Controllers/DirectoryController.cs @@ -41,6 +41,7 @@ namespace Marco.Pms.Services.Controllers return StatusCode(response.StatusCode, response); } + [HttpGet] public async Task GetContactList([FromQuery] string? search, [FromQuery] List? bucketIds, [FromQuery] List? categoryIds, [FromQuery] Guid? projectId, [FromQuery] bool active = true) { @@ -88,19 +89,9 @@ namespace Marco.Pms.Services.Controllers [HttpGet("profile/{id}")] public async Task GetContactProfile(Guid id) { - var response = await _directoryService.GetContactProfile(id); - if (response.StatusCode == 200) - { - return Ok(response); - } - else if (response.StatusCode == 404) - { - return NotFound(response); - } - else - { - return BadRequest(response); - } + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _directoryService.GetContactProfileAsync(id, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } [HttpGet("organization")] diff --git a/Marco.Pms.Services/Helpers/DirectoryHelper.cs b/Marco.Pms.Services/Helpers/DirectoryHelper.cs index 3dd578e..b2e9f73 100644 --- a/Marco.Pms.Services/Helpers/DirectoryHelper.cs +++ b/Marco.Pms.Services/Helpers/DirectoryHelper.cs @@ -825,25 +825,25 @@ namespace Marco.Pms.Services.Helpers } contactVM.Tags = tagVMs; } - List? notes = await _context.ContactNotes.Where(n => n.ContactId == contact.Id && n.IsActive).ToListAsync(); - if (notes.Any()) - { - List? noteIds = notes.Select(n => n.Id).ToList(); - List? noteUpdateLogs = await _context.DirectoryUpdateLogs.Include(l => l.Employee).Where(l => noteIds.Contains(l.RefereanceId)).OrderByDescending(l => l.UpdateAt).ToListAsync(); - List? noteVMs = new List(); - foreach (var note in notes) - { - DirectoryUpdateLog? noteUpdateLog = noteUpdateLogs.Where(n => n.RefereanceId == note.Id).OrderByDescending(l => l.UpdateAt).FirstOrDefault(); - ContactNoteVM noteVM = note.ToContactNoteVMFromContactNote(); - if (noteUpdateLog != null) - { - noteVM.UpdatedAt = noteUpdateLog.UpdateAt; - noteVM.UpdatedBy = noteUpdateLog.Employee != null ? noteUpdateLog.Employee.ToBasicEmployeeVMFromEmployee() : null; - } - noteVMs.Add(noteVM); - } - contactVM.Notes = noteVMs; - } + //List? notes = await _context.ContactNotes.Where(n => n.ContactId == contact.Id && n.IsActive).ToListAsync(); + //if (notes.Any()) + //{ + // List? noteIds = notes.Select(n => n.Id).ToList(); + // List? noteUpdateLogs = await _context.DirectoryUpdateLogs.Include(l => l.Employee).Where(l => noteIds.Contains(l.RefereanceId)).OrderByDescending(l => l.UpdateAt).ToListAsync(); + // List? noteVMs = new List(); + // foreach (var note in notes) + // { + // DirectoryUpdateLog? noteUpdateLog = noteUpdateLogs.Where(n => n.RefereanceId == note.Id).OrderByDescending(l => l.UpdateAt).FirstOrDefault(); + // ContactNoteVM noteVM = note.ToContactNoteVMFromContactNote(); + // if (noteUpdateLog != null) + // { + // noteVM.UpdatedAt = noteUpdateLog.UpdateAt; + // noteVM.UpdatedBy = noteUpdateLog.Employee != null ? noteUpdateLog.Employee.ToBasicEmployeeVMFromEmployee() : null; + // } + // noteVMs.Add(noteVM); + // } + // contactVM.Notes = noteVMs; + //} _logger.LogInfo("Employee ID {EmployeeId} fetched profile of contact {COntactId}", LoggedInEmployee.Id, contact.Id); return ApiResponse.SuccessResponse(contactVM, "Contact profile fetched successfully"); diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index 46b119b..0fe8d22 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -5,6 +5,7 @@ using Marco.Pms.Model.Employees; using Marco.Pms.Model.Master; using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.Projects; +using Marco.Pms.Model.ViewModels.Activities; using Marco.Pms.Model.ViewModels.Directory; using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Model.ViewModels.Master; @@ -66,18 +67,21 @@ namespace Marco.Pms.Services.MappingProfiles #region ======================================================= Employee ======================================================= CreateMap(); + CreateMap(); #endregion #region ======================================================= Directory ======================================================= CreateMap(); + CreateMap(); CreateMap(); CreateMap(); CreateMap(); CreateMap(); + CreateMap(); #endregion diff --git a/Marco.Pms.Services/Service/DirectoryService.cs b/Marco.Pms.Services/Service/DirectoryService.cs index 06883c6..ebcdbec 100644 --- a/Marco.Pms.Services/Service/DirectoryService.cs +++ b/Marco.Pms.Services/Service/DirectoryService.cs @@ -5,7 +5,6 @@ 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.Projects; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Directory; using Marco.Pms.Model.ViewModels.Master; @@ -74,16 +73,7 @@ namespace Marco.Pms.Services.Service await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); // Step 1: Perform initial permission checks in parallel. - using var scope = _serviceScopeFactory.CreateScope(); - var permissionService = scope.ServiceProvider.GetRequiredService(); - var hasAdminPermissionTask = permissionService.HasPermission(PermissionsMaster.DirectoryAdmin, loggedInEmployee.Id); - var hasManagerPermissionTask = permissionService.HasPermission(PermissionsMaster.DirectoryManager, loggedInEmployee.Id); - var hasUserPermissionTask = permissionService.HasPermission(PermissionsMaster.DirectoryUser, loggedInEmployee.Id); - await Task.WhenAll(hasAdminPermissionTask, hasManagerPermissionTask, hasUserPermissionTask); - - var hasAdminPermission = hasAdminPermissionTask.Result; - var hasManagerPermission = hasManagerPermissionTask.Result; - var hasUserPermission = hasUserPermissionTask.Result; + var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployeeId); // Step 2: Build the core IQueryable with all filtering logic applied on the server. // This is the most critical optimization. @@ -170,7 +160,7 @@ namespace Marco.Pms.Services.Service { await using var taskDbContext = await _dbContextFactory.CreateDbContextAsync(); return await taskDbContext.ContactsPhones - .Where(p => finalContactIds.Contains(p.ContactId)) + .Where(p => finalContactIds.Contains(p.ContactId) && p.TenantId == tenantId) .ToListAsync(); }); @@ -178,7 +168,7 @@ namespace Marco.Pms.Services.Service { await using var taskDbContext = await _dbContextFactory.CreateDbContextAsync(); return await taskDbContext.ContactsEmails - .Where(e => finalContactIds.Contains(e.ContactId)) + .Where(e => finalContactIds.Contains(e.ContactId) && e.TenantId == tenantId) .ToListAsync(); }); @@ -512,120 +502,129 @@ namespace Marco.Pms.Services.Service _logger.LogInfo("Employee ID {EmployeeId} sent an empty Bucket id", LoggedInEmployee.Id); return ApiResponse.ErrorResponse("Bucket ID is empty", "Bucket ID is empty", 400); } - public async Task> GetContactProfile(Guid id) + public async Task> GetContactProfileAsync(Guid id, Guid tenantId, Employee loggedInEmployee) { - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - if (id != Guid.Empty) + Guid loggedInEmployeeId = loggedInEmployee.Id; + if (id == Guid.Empty) { - Contact? contact = await _context.Contacts.Include(c => c.ContactCategory).Include(c => c.CreatedBy).FirstOrDefaultAsync(c => c.Id == id && c.IsActive); + _logger.LogInfo("Employee ID {EmployeeId} sent an empty contact id", loggedInEmployeeId); + return ApiResponse.ErrorResponse("Contact ID is empty", "Contact ID is empty", 400); + } + try + { + // Use a single DbContext for the entire operation to ensure consistency. + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + + // Step 1: Perform initial permission checks in parallel. + var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployeeId); + + if (!hasAdminPermission && !hasManagerPermission && !hasUserPermission) + { + _logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to get contact profile due to lack of permissions.", loggedInEmployeeId); + return ApiResponse.ErrorResponse("Access Denied", "You do not have permission to view contact.", 403); + } + + Contact? contact = await dbContext.Contacts + .AsNoTracking() // Use AsNoTracking for read-only operations to improve performance. + .Include(c => c.ContactCategory) + .Include(c => c.CreatedBy) + .Include(c => c.UpdatedBy) + .FirstOrDefaultAsync(c => c.Id == id && c.IsActive && c.TenantId == tenantId); if (contact == null) { - _logger.LogWarning("Employee with ID {LoggedInEmployeeId} tries to update contact with ID {ContactId} is not found in database", LoggedInEmployee.Id); + _logger.LogWarning("Employee with ID {LoggedInEmployeeId} tries to update contact with ID {ContactId} is not found in database", loggedInEmployeeId); return ApiResponse.ErrorResponse("Contact not found", "Contact not found", 404); } - ContactProfileVM contactVM = contact.ToContactProfileVMFromContact(); - DirectoryUpdateLog? updateLog = await _context.DirectoryUpdateLogs.Include(l => l.Employee).Where(l => l.RefereanceId == contact.Id).OrderByDescending(l => l.UpdateAt).FirstOrDefaultAsync(); - if (updateLog != null) - { - contactVM.UpdatedAt = updateLog.UpdateAt; - contactVM.UpdatedBy = updateLog.Employee != null ? updateLog.Employee.ToBasicEmployeeVMFromEmployee() : null; - } + ContactProfileVM contactVM = _mapper.Map(contact); - List? phones = await _context.ContactsPhones.Where(p => p.ContactId == contact.Id).ToListAsync(); - if (phones.Any()) + var phonesTask = Task.Run(async () => { - List? phoneVMs = new List(); - foreach (var phone in phones) - { - ContactPhoneVM phoneVM = phone.ToContactPhoneVMFromContactPhone(); - phoneVMs.Add(phoneVM); - } - contactVM.ContactPhones = phoneVMs; - } + await using var taskDbContext = await _dbContextFactory.CreateDbContextAsync(); + return await taskDbContext.ContactsPhones + .AsNoTracking() + .Where(p => p.ContactId == contact.Id && p.TenantId == tenantId) + .Select(p => _mapper.Map(p)) + .ToListAsync(); + }); - List? emails = await _context.ContactsEmails.Where(e => e.ContactId == contact.Id).ToListAsync(); - if (emails.Any()) + var emailsTask = Task.Run(async () => { - List? emailVMs = new List(); - foreach (var email in emails) - { - ContactEmailVM emailVM = email.ToContactEmailVMFromContactEmail(); - emailVMs.Add(emailVM); - } - contactVM.ContactEmails = emailVMs; - } + await using var taskDbContext = await _dbContextFactory.CreateDbContextAsync(); + return await taskDbContext.ContactsEmails + .AsNoTracking() + .Where(e => e.ContactId == contact.Id && e.TenantId == tenantId) + .Select(e => _mapper.Map(e)) + .ToListAsync(); + }); - List? contactProjects = await _context.ContactProjectMappings.Where(cp => cp.ContactId == contact.Id).ToListAsync(); - if (contactProjects.Any()) + var contactProjectsTask = Task.Run(async () => { - List projectIds = contactProjects.Select(cp => cp.ProjectId).ToList(); - List? projects = await _context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync(); - List? projectVMs = new List(); - foreach (var project in projects) - { - BasicProjectVM projectVM = new BasicProjectVM + await using var taskDbContext = await _dbContextFactory.CreateDbContextAsync(); + return await taskDbContext.ContactProjectMappings + .AsNoTracking() + .Include(cp => cp.Project) + .Where(cp => cp.ContactId == contact.Id && cp.Project != null && cp.Project.TenantId == tenantId) + .Select(cp => new BasicProjectVM { - Id = project.Id, - Name = project.Name - }; - projectVMs.Add(projectVM); - } - contactVM.Projects = projectVMs; - } - List? contactBuckets = await _context.ContactBucketMappings.Where(cb => cb.ContactId == contact.Id).ToListAsync(); - List? employeeBuckets = await _context.EmployeeBucketMappings.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).ToListAsync(); - if (contactBuckets.Any() && employeeBuckets.Any()) + Id = cp.Project!.Id, + Name = cp.Project.Name + }) + .ToListAsync(); + }); + + var contactBucketsTask = Task.Run(async () => { - List contactBucketIds = contactBuckets.Select(cb => cb.BucketId).ToList(); - List employeeBucketIds = employeeBuckets.Select(eb => eb.BucketId).ToList(); - List? buckets = await _context.Buckets.Where(b => contactBucketIds.Contains(b.Id) && employeeBucketIds.Contains(b.Id)).ToListAsync(); - List? bucketVMs = new List(); - foreach (var bucket in buckets) + await using var taskDbContext = await _dbContextFactory.CreateDbContextAsync(); + var bucketQuery = taskDbContext.ContactBucketMappings + .AsNoTracking() + .Include(cb => cb.Bucket) + .ThenInclude(b => b!.CreatedBy) + .Where(cb => cb.ContactId == contact.Id && cb.Bucket != null && cb.Bucket.TenantId == tenantId); + + if (hasAdminPermission) { - BucketVM bucketVM = bucket.ToBucketVMFromBucket(); - bucketVMs.Add(bucketVM); + return await bucketQuery + .Select(cb => _mapper.Map(cb.Bucket)) + .ToListAsync(); } - contactVM.Buckets = bucketVMs; - } - List? contactTags = await _context.ContactTagMappings.Where(ct => ct.ContactId == contact.Id).ToListAsync(); - if (contactTags.Any()) + List employeeBucketIds = await taskDbContext.EmployeeBucketMappings + .AsNoTracking() + .Where(eb => eb.EmployeeId == loggedInEmployeeId) + .Select(eb => eb.BucketId) + .ToListAsync(); + + return await bucketQuery + .Where(cb => employeeBucketIds.Contains(cb.BucketId)) + .Select(cb => _mapper.Map(cb.Bucket)) + .ToListAsync(); + + }); + + var contactTagsTask = Task.Run(async () => { - List tagIds = contactTags.Select(ct => ct.ContactTagId).ToList(); - List tagMasters = await _context.ContactTagMasters.Where(t => tagIds.Contains(t.Id)).ToListAsync(); - List tagVMs = new List(); - foreach (var tagMaster in tagMasters) - { - ContactTagVM tagVM = tagMaster.ToContactTagVMFromContactTagMaster(); - tagVMs.Add(tagVM); - } - contactVM.Tags = tagVMs; - } - List? notes = await _context.ContactNotes.Where(n => n.ContactId == contact.Id && n.IsActive).ToListAsync(); - if (notes.Any()) - { - List? noteIds = notes.Select(n => n.Id).ToList(); - List? noteUpdateLogs = await _context.DirectoryUpdateLogs.Include(l => l.Employee).Where(l => noteIds.Contains(l.RefereanceId)).OrderByDescending(l => l.UpdateAt).ToListAsync(); - List? noteVMs = new List(); - foreach (var note in notes) - { - DirectoryUpdateLog? noteUpdateLog = noteUpdateLogs.Where(n => n.RefereanceId == note.Id).OrderByDescending(l => l.UpdateAt).FirstOrDefault(); - ContactNoteVM noteVM = note.ToContactNoteVMFromContactNote(); - if (noteUpdateLog != null) - { - noteVM.UpdatedAt = noteUpdateLog.UpdateAt; - noteVM.UpdatedBy = noteUpdateLog.Employee != null ? noteUpdateLog.Employee.ToBasicEmployeeVMFromEmployee() : null; - } - noteVMs.Add(noteVM); - } - contactVM.Notes = noteVMs; - } - _logger.LogInfo("Employee ID {EmployeeId} fetched profile of contact {COntactId}", LoggedInEmployee.Id, contact.Id); + await using var taskDbContext = await _dbContextFactory.CreateDbContextAsync(); + return await taskDbContext.ContactTagMappings + .AsNoTracking() + .Include(ct => ct.ContactTag) + .Where(ct => ct.ContactId == contact.Id && ct.ContactTag != null && ct.ContactTag.TenantId == tenantId) + .Select(ct => _mapper.Map(ct.ContactTag)) + .ToListAsync(); + }); + + await Task.WhenAll(phonesTask, emailsTask, contactProjectsTask, contactBucketsTask, contactTagsTask); + contactVM.ContactPhones = phonesTask.Result; + contactVM.ContactEmails = emailsTask.Result; + contactVM.Tags = contactTagsTask.Result; + contactVM.Buckets = contactBucketsTask.Result; + contactVM.Projects = contactProjectsTask.Result; + _logger.LogInfo("Employee ID {EmployeeId} fetched profile of contact {COntactId}", loggedInEmployeeId, contact.Id); return ApiResponse.SuccessResponse(contactVM, "Contact profile fetched successfully"); - } - _logger.LogInfo("Employee ID {EmployeeId} sent an empty contact id", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("Contact ID is empty", "Contact ID is empty", 400); + catch (Exception ex) + { + _logger.LogError(ex, "An unexpected error occurred while fetching contact list for Tenant {TenantId} by Employee {EmployeeId}", tenantId, loggedInEmployeeId); + return ApiResponse.ErrorResponse("An internal error occurred.", ExceptionMapper(ex), 500); + } } public async Task> GetOrganizationList() { @@ -1708,6 +1707,21 @@ namespace Marco.Pms.Services.Service #region =================================================================== Helper Functions =================================================================== + private async Task<(bool hasAdmin, bool hasManager, bool hasUser)> CheckPermissionsAsync(Guid employeeId) + { + // Scoping the service provider ensures services are disposed of correctly. + using var scope = _serviceScopeFactory.CreateScope(); + var permissionService = scope.ServiceProvider.GetRequiredService(); + + // Run all permission checks in parallel. + var hasAdminTask = permissionService.HasPermission(PermissionsMaster.DirectoryAdmin, employeeId); + var hasManagerTask = permissionService.HasPermission(PermissionsMaster.DirectoryManager, employeeId); + var hasUserTask = permissionService.HasPermission(PermissionsMaster.DirectoryUser, employeeId); + + await Task.WhenAll(hasAdminTask, hasManagerTask, hasUserTask); + + return (hasAdminTask.Result, hasManagerTask.Result, hasUserTask.Result); + } private bool Compare(string sentence, string search) { sentence = sentence.Trim().ToLower(); diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IDirectoryService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IDirectoryService.cs index 1465e15..b9e1be2 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IDirectoryService.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IDirectoryService.cs @@ -9,7 +9,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> GetListOfContactsAsync(string? search, string? filter, Guid? projectId, bool active, int pageSize, int pageNumber, Guid tenantId, Employee loggedInEmployee); Task> GetListOfContactsOld(string? search, bool active, ContactFilterDto? filterDto, Guid? projectId); Task> GetContactsListByBucketId(Guid id); - Task> GetContactProfile(Guid id); + Task> GetContactProfileAsync(Guid id, Guid tenantId, Employee loggedInEmployee); Task> GetOrganizationList(); Task> CreateContact(CreateContactDto createContact); Task> UpdateContact(Guid id, UpdateContactDto updateContact);