diff --git a/Marco.Pms.Services/Controllers/DirectoryController.cs b/Marco.Pms.Services/Controllers/DirectoryController.cs index c496297..efa7490 100644 --- a/Marco.Pms.Services/Controllers/DirectoryController.cs +++ b/Marco.Pms.Services/Controllers/DirectoryController.cs @@ -238,7 +238,7 @@ namespace Marco.Pms.Services.Controllers public async Task AssignBucket(Guid bucketId, [FromBody] List assignBuckets) { var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var response = await _directoryService.AssignBucket(bucketId, assignBuckets); + var response = await _directoryService.AssignBucketAsync(bucketId, assignBuckets, tenantId, loggedInEmployee); if (response.Success) { var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Directory_Buckets", Response = response.Data }; @@ -251,7 +251,7 @@ namespace Marco.Pms.Services.Controllers public async Task DeleteBucket(Guid id) { var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var response = await _directoryService.DeleteBucket(id); + var response = await _directoryService.DeleteBucketAsync(id, tenantId, loggedInEmployee); if (response.Success) { var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Directory_Buckets", Response = id }; diff --git a/Marco.Pms.Services/Service/DirectoryService.cs b/Marco.Pms.Services/Service/DirectoryService.cs index 5c85fe9..f6d7193 100644 --- a/Marco.Pms.Services/Service/DirectoryService.cs +++ b/Marco.Pms.Services/Service/DirectoryService.cs @@ -1780,163 +1780,269 @@ namespace Marco.Pms.Services.Service } } - public async Task> AssignBucket(Guid bucketId, List assignBuckets) + public async Task> AssignBucketAsync(Guid bucketId, List assignBuckets, Guid tenantId, Employee loggedInEmployee) { - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - if (assignBuckets != null && bucketId != Guid.Empty) + // Validate input payload + if (assignBuckets == null || bucketId == Guid.Empty) { - var assignedRoleIds = await _context.EmployeeRoleMappings.Where(r => r.EmployeeId == LoggedInEmployee.Id).Select(r => r.RoleId).ToListAsync(); - var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync(); + _logger.LogWarning("Employee with ID {EmployeeId} sent empty or invalid payload.", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("User sent empty or invalid payload", "User sent empty or invalid payload", 400); + } - Bucket? bucket = await _context.Buckets.Include(b => b.CreatedBy).FirstOrDefaultAsync(b => b.Id == bucketId && b.TenantId == tenantId); + // Check permissions of the logged-in employee + var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployee.Id); + + + // Load the bucket with related CreatedBy and validate tenant + var bucket = await _context.Buckets.Include(b => b.CreatedBy) + .FirstOrDefaultAsync(b => b.Id == bucketId && b.TenantId == tenantId); + + if (bucket == null) + { + _logger.LogWarning("Employee ID {EmployeeId} attempted to update bucket {BucketId} but it was not found.", loggedInEmployee.Id, bucketId); + return ApiResponse.ErrorResponse("Bucket not found", "Bucket not found", 404); + } + + // Load EmployeeBucketMappings related to the bucket + var employeeBuckets = await _context.EmployeeBucketMappings + .Where(eb => eb.BucketId == bucketId) + .ToListAsync(); + + var employeeBucketIds = employeeBuckets.Select(eb => eb.EmployeeId).ToHashSet(); + + // Check access permissions to the bucket + bool hasAccess = false; + if (hasAdminPermission) + { + hasAccess = true; + } + else if (hasManagerPermission && employeeBucketIds.Contains(loggedInEmployee.Id)) + { + hasAccess = true; + } + else if (hasUserPermission && bucket.CreatedByID == loggedInEmployee.Id) + { + hasAccess = true; + } + + if (!hasAccess) + { + _logger.LogWarning("Employee {EmployeeId} attempted to access bucket {BucketId} without permission.", loggedInEmployee.Id, bucketId); + return ApiResponse.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 403); + } + + // Load active employees tenant-wide + var activeEmployeeIds = await _context.Employees + .Where(e => e.TenantId == tenantId && e.IsActive) + .Select(e => e.Id) + .ToListAsync(); + + int assignedEmployeesCount = 0; + int removedEmployeesCount = 0; + + // Process each assignment request + foreach (var assignBucket in assignBuckets) + { + if (!activeEmployeeIds.Contains(assignBucket.EmployeeId)) + { + // Skip employee IDs that are not active or not in tenant + _logger.LogWarning("Skipping inactive or non-tenant employee ID {EmployeeId} in assignment.", assignBucket.EmployeeId); + continue; + } + + if (assignBucket.IsActive) + { + // Add mapping if not already exists + if (!employeeBucketIds.Contains(assignBucket.EmployeeId)) + { + var newMapping = new EmployeeBucketMapping + { + EmployeeId = assignBucket.EmployeeId, + BucketId = bucketId + }; + _context.EmployeeBucketMappings.Add(newMapping); + assignedEmployeesCount++; + } + } + else + { + // Remove mapping if it exists + var existingMapping = employeeBuckets.FirstOrDefault(eb => eb.EmployeeId == assignBucket.EmployeeId); + if (existingMapping != null && bucket.CreatedByID != assignBucket.EmployeeId) + { + _context.EmployeeBucketMappings.Remove(existingMapping); + removedEmployeesCount++; + } + } + } + + // Add directory update log + _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog + { + RefereanceId = bucketId, + UpdatedById = loggedInEmployee.Id, + UpdateAt = DateTime.UtcNow + }); + + // Save changes with error handling + try + { + await _context.SaveChangesAsync(); + } + catch (DbUpdateException ex) + { + _logger.LogError(ex, "Failed to save changes while assigning bucket {BucketId} by employee {EmployeeId}.", bucketId, loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Failed to update bucket assignments", "An error occurred while updating bucket assignments", 500); + } + + // Reload mappings and contacts for the updated bucket + var employeeBucketMappingTask = Task.Run(async () => + { + using (var context = _dbContextFactory.CreateDbContext()) + { + return await context.EmployeeBucketMappings.Where(eb => eb.BucketId == bucket.Id).ToListAsync(); + } + }); + var contactBucketMappingTask = Task.Run(async () => + { + using (var context = _dbContextFactory.CreateDbContext()) + { + return await context.ContactBucketMappings.Where(eb => eb.BucketId == bucket.Id).ToListAsync(); + } + }); + await Task.WhenAll(employeeBucketMappingTask, contactBucketMappingTask); + var employeeBucketMappings = employeeBucketMappingTask.Result; + var contactBucketMappings = contactBucketMappingTask.Result; + + // Prepare view model response + var bucketVm = _mapper.Map(bucket); + bucketVm.EmployeeIds = employeeBucketMappings.Select(eb => eb.EmployeeId).ToList(); + bucketVm.NumberOfContacts = contactBucketMappings.Count; + + // Log assignment and removal actions + if (assignedEmployeesCount > 0) + { + _logger.LogInfo("Employee {EmployeeId} assigned bucket {BucketId} to {Count} employees.", loggedInEmployee.Id, bucketId, assignedEmployeesCount); + } + if (removedEmployeesCount > 0) + { + _logger.LogInfo("Employee {EmployeeId} removed {Count} employees from bucket {BucketId}.", loggedInEmployee.Id, removedEmployeesCount, bucketId); + } + + return ApiResponse.SuccessResponse(bucketVm, "Bucket details updated successfully", 200); + } + + public async Task> DeleteBucketAsync(Guid id, Guid tenantId, Employee loggedInEmployee) + { + try + { + // Fetch the bucket in the main context to verify existence and tenant scope + var bucket = await _context.Buckets.FirstOrDefaultAsync(b => b.Id == id && b.TenantId == tenantId); if (bucket == null) { - _logger.LogWarning("Employee ID {LoggedInEmployeeId} attempted to update a bucket but not found in database.", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("Bucket not found", "Bucket not found", 404); - } - var employeeBuckets = await _context.EmployeeBucketMappings.Where(eb => eb.BucketId == bucketId).ToListAsync(); - var bucketIds = employeeBuckets.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).Select(eb => eb.BucketId).ToList(); - var employeeBucketIds = employeeBuckets.Select(eb => eb.EmployeeId).ToList(); - Bucket? accessableBucket = null; - if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) - { - accessableBucket = bucket; - } - else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && bucketIds.Contains(bucketId)) - { - accessableBucket = bucket; - } - else if (permissionIds.Contains(PermissionsMaster.DirectoryUser)) - { - if (bucket.CreatedByID == LoggedInEmployee.Id) - { - accessableBucket = bucket; - } - } - if (accessableBucket == null) - { - _logger.LogWarning("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id); - return ApiResponse.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 403); - } - var employeeIds = await _context.Employees.Where(e => e.TenantId == tenantId && e.IsActive).Select(e => e.Id).ToListAsync(); - int assignedEmployee = 0; - int removededEmployee = 0; - foreach (var assignBucket in assignBuckets) - { - if (employeeIds.Contains(assignBucket.EmployeeId)) - { - if (assignBucket.IsActive && !employeeBucketIds.Contains(assignBucket.EmployeeId)) - { - EmployeeBucketMapping employeeBucketMapping = new EmployeeBucketMapping - { - EmployeeId = assignBucket.EmployeeId, - BucketId = bucketId - }; - _context.EmployeeBucketMappings.Add(employeeBucketMapping); - assignedEmployee += 1; - } - else if (!assignBucket.IsActive) - { - EmployeeBucketMapping? employeeBucketMapping = employeeBuckets.FirstOrDefault(eb => eb.BucketId == bucketId && eb.EmployeeId == assignBucket.EmployeeId); - if (employeeBucketMapping != null) - { - _context.EmployeeBucketMappings.Remove(employeeBucketMapping); - removededEmployee += 1; - } - } - } + _logger.LogWarning("Employee {EmployeeId} attempted to delete bucket {BucketId}, bucket not found for tenant {TenantId}.", + loggedInEmployee.Id, id, tenantId); + + // Returning success response to maintain idempotency + return ApiResponse.SuccessResponse(new { }, "Bucket deleted successfully", 200); } - _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog + // Run parallel tasks to fetch related mappings using separate DbContext instances + var employeeBucketMappingsTask = Task.Run(async () => { - RefereanceId = bucketId, - UpdatedById = LoggedInEmployee.Id, - UpdateAt = DateTime.UtcNow + using var context = _dbContextFactory.CreateDbContext(); + return await context.EmployeeBucketMappings + .Where(eb => eb.BucketId == bucket.Id) + .ToListAsync(); }); - await _context.SaveChangesAsync(); - - AssignBucketVM bucketVM = bucket.ToAssignBucketVMFromBucket(); - List employeeBucketMappings = await _context.EmployeeBucketMappings.Where(eb => eb.BucketId == bucket.Id).ToListAsync(); - List contactBuckets = await _context.ContactBucketMappings.Where(eb => eb.BucketId == bucket.Id).ToListAsync(); - employeeIds = employeeBucketMappings.Select(eb => eb.EmployeeId).ToList(); - bucketVM.EmployeeIds = employeeIds; - bucketVM.NumberOfContacts = contactBuckets.Count; - - if (assignedEmployee > 0) + var contactBucketMappingsTask = Task.Run(async () => { - _logger.LogInfo("Employee {EmployeeId} assigned bucket {BucketId} to {conut} number of employees", LoggedInEmployee.Id, bucketId, assignedEmployee); - } - if (removededEmployee > 0) + using var context = _dbContextFactory.CreateDbContext(); + return await context.ContactBucketMappings + .Where(cb => cb.BucketId == bucket.Id) + .ToListAsync(); + }); + + // Await both tasks concurrently + await Task.WhenAll(employeeBucketMappingsTask, contactBucketMappingsTask); + + var employeeBucketMappings = employeeBucketMappingsTask.Result; + var contactBucketMappings = contactBucketMappingsTask.Result; + + // Check if bucket has any contacts mapped - cannot delete in this state + if (contactBucketMappings.Any()) { - _logger.LogWarning("Employee {EmployeeId} removed {conut} number of employees from bucket {BucketId}", LoggedInEmployee.Id, removededEmployee, bucketId); - } - return ApiResponse.SuccessResponse(bucketVM, "Details updated successfully", 200); - } - _logger.LogWarning("Employee with ID {LoggedInEmployeeId} sended empty payload", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("User Send empty Payload", "User Send empty Payload", 400); - } - public async Task> DeleteBucket(Guid id) - { - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + _logger.LogInfo("Employee {EmployeeId} attempted to delete bucket {BucketId} but bucket contains contacts, deletion blocked.", + loggedInEmployee.Id, bucket.Id); - Bucket? bucket = await _context.Buckets.FirstOrDefaultAsync(n => n.Id == id && n.TenantId == tenantId); - - if (bucket != null) - { - List? employeeBuckets = await _context.EmployeeBucketMappings.Where(eb => eb.BucketId == id).ToListAsync(); - List? contactBuckets = await _context.ContactBucketMappings.Where(eb => eb.BucketId == id).ToListAsync(); - - if (contactBuckets.Any()) - { - _logger.LogInfo("Employee {EmployeeId} attempted to deleted bucket {BucketId},but bucket have contacts in it.", LoggedInEmployee.Id, id); - return ApiResponse.ErrorResponse("This bucket can not be deleted", "This bucket can not be deleted", 400); + return ApiResponse.ErrorResponse("This bucket cannot be deleted because it contains contacts.", + "This bucket cannot be deleted", 400); } - var assignedRoleIds = await _context.EmployeeRoleMappings.Where(r => r.EmployeeId == LoggedInEmployee.Id).Select(r => r.RoleId).ToListAsync(); - var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync(); - var bucketIds = employeeBuckets.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).Select(eb => eb.BucketId).ToList(); + // Check permissions of the logged-in employee + var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployee.Id); - Bucket? accessableBucket = null; - if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) + var accessibleBucket = (Bucket?)null; + + // Get bucket IDs for which the employee has mapping association + var employeeBucketIds = employeeBucketMappings + .Where(eb => eb.EmployeeId == loggedInEmployee.Id) + .Select(eb => eb.BucketId) + .ToList(); + + // Determine if employee has permission to delete this bucket + if (hasAdminPermission) { - accessableBucket = bucket; + accessibleBucket = bucket; } - else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && bucketIds.Contains(id)) + else if (hasManagerPermission && employeeBucketIds.Contains(bucket.Id)) { - accessableBucket = bucket; + accessibleBucket = bucket; } - else if (permissionIds.Contains(PermissionsMaster.DirectoryUser)) + else if (hasUserPermission && bucket.CreatedByID == loggedInEmployee.Id) { - if (bucket.CreatedByID == LoggedInEmployee.Id) - { - accessableBucket = bucket; - } - } - if (accessableBucket == null) - { - _logger.LogWarning("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id); - return ApiResponse.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 403); + accessibleBucket = bucket; } - _context.EmployeeBucketMappings.RemoveRange(employeeBuckets); + if (accessibleBucket == null) + { + _logger.LogWarning("Employee {EmployeeId} attempted to delete bucket {BucketId} without sufficient permissions.", + loggedInEmployee.Id, bucket.Id); + + return ApiResponse.ErrorResponse("You don't have permission to access this bucket.", + "Permission denied", 403); + } + + // Remove related employee bucket mappings + _context.EmployeeBucketMappings.RemoveRange(employeeBucketMappings); + // Remove the bucket itself _context.Buckets.Remove(bucket); + // Log deletion action _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog { - RefereanceId = id, - UpdatedById = LoggedInEmployee.Id, + RefereanceId = bucket.Id, + UpdatedById = loggedInEmployee.Id, UpdateAt = DateTime.UtcNow }); + await _context.SaveChangesAsync(); - _logger.LogInfo("Employee {EmployeeId} deleted bucket {BucketId} and related entries", LoggedInEmployee.Id, id); + + _logger.LogInfo("Employee {EmployeeId} deleted bucket {BucketId} along with related employee bucket mappings.", + loggedInEmployee.Id, bucket.Id); + return ApiResponse.SuccessResponse(new { }, "Bucket deleted successfully", 200); } + catch (Exception ex) + { + // Log the exception with error level + _logger.LogError(ex, "An error occurred while employee {EmployeeId} attempted to delete bucket {BucketId}.", + loggedInEmployee.Id, id); - _logger.LogWarning("Employee {EmployeeId} tries to delete bucket {BucketId} but not found in database", LoggedInEmployee.Id, id); - return ApiResponse.SuccessResponse(new { }, "Bucket deleted successfully", 200); + return ApiResponse.ErrorResponse("An unexpected error occurred while deleting the bucket.", + "Internal Server Error", 500); + } } #endregion diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IDirectoryService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IDirectoryService.cs index ee12e23..5884e42 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IDirectoryService.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IDirectoryService.cs @@ -26,7 +26,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> GetBucketListAsync(Guid tenantId, Employee loggedInEmployee); Task> CreateBucketAsync(CreateBucketDto bucketDto, Guid tenantId, Employee loggedInEmployee); Task> UpdateBucketAsync(Guid id, UpdateBucketDto bucketDto, Guid tenantId, Employee loggedInEmployee); - Task> AssignBucket(Guid bucketId, List assignBuckets); - Task> DeleteBucket(Guid id); + Task> AssignBucketAsync(Guid bucketId, List assignBuckets, Guid tenantId, Employee loggedInEmployee); + Task> DeleteBucketAsync(Guid id, Guid tenantId, Employee loggedInEmployee); } }