diff --git a/Marco.Pms.CacheHelper/EmployeeCache.cs b/Marco.Pms.CacheHelper/EmployeeCache.cs index 2211393..f7b7066 100644 --- a/Marco.Pms.CacheHelper/EmployeeCache.cs +++ b/Marco.Pms.CacheHelper/EmployeeCache.cs @@ -97,7 +97,7 @@ namespace Marco.Pms.CacheHelper var result = await _collection.UpdateOneAsync(filter, update); - if (result.MatchedCount == 0) + if (result.ModifiedCount == 0) return false; return true; diff --git a/Marco.Pms.Model/ViewModels/Projects/ProjectAllocationVM.cs b/Marco.Pms.Model/ViewModels/Projects/ProjectAllocationVM.cs new file mode 100644 index 0000000..6d9138e --- /dev/null +++ b/Marco.Pms.Model/ViewModels/Projects/ProjectAllocationVM.cs @@ -0,0 +1,13 @@ +namespace Marco.Pms.Model.ViewModels.Projects +{ + public class ProjectAllocationVM + { + public Guid Id { get; set; } + public Guid EmployeeId { get; set; } + public Guid? JobRoleId { get; set; } + public bool IsActive { get; set; } = true; + public Guid ProjectId { get; set; } + public DateTime AllocationDate { get; set; } + public DateTime? ReAllocationDate { get; set; } + } +} diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 0122003..b833064 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -252,28 +252,31 @@ namespace MarcoBMS.Services.Controllers return StatusCode(response.StatusCode, response); } - //[HttpPost("allocation")] - //public async Task ManageAllocation(List projectAllocationDot) - //{ - // // --- Step 1: Input Validation --- - // if (!ModelState.IsValid) - // { - // var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); - // _logger.LogWarning("Update project called with invalid model state for ID {ProjectId}. Errors: {Errors}", id, string.Join(", ", errors)); - // return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); - // } + [HttpPost("allocation")] + public async Task ManageAllocation([FromBody] List projectAllocationDot) + { + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) + { + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); + } - // // --- Step 2: Prepare data without I/O --- - // Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - // var response = await _projectServices.UpdateProjectAsync(id, updateProjectDto, tenantId, loggedInEmployee); - // if (response.Success) - // { - // var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeList = employeeIds }; - // await _signalR.SendNotificationAsync(notification); - // } - // return StatusCode(response.StatusCode, response); + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.ManageAllocationAsync(projectAllocationDot, tenantId, loggedInEmployee); + if (response.Success) + { + List employeeIds = response.Data.Select(pa => pa.EmployeeId).ToList(); + List projectIds = response.Data.Select(pa => pa.ProjectId).ToList(); - //} + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeList = employeeIds }; + await _signalR.SendNotificationAsync(notification); + } + return StatusCode(response.StatusCode, response); + + } [HttpGet("assigned-projects/{employeeId}")] public async Task GetProjectsByEmployee([FromRoute] Guid employeeId) diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index 7d627bc..3ca1271 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -43,6 +43,12 @@ namespace Marco.Pms.Services.MappingProfiles CreateMap(); CreateMap(); CreateMap(); + CreateMap() + .ForMember( + dest => dest.EmployeeId, + // Explicitly and safely convert string ProjectStatusId to Guid ProjectStatusId + opt => opt.MapFrom(src => src.EmpID)); + CreateMap(); #endregion #region ======================================================= Projects ======================================================= diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs index 7717584..33df2c0 100644 --- a/Marco.Pms.Services/Service/ProjectServices.cs +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -609,85 +609,112 @@ namespace Marco.Pms.Services.Service } } - //public async Task> ManageAllocation(List projectAllocationDot, Guid tenantId, Employee loggedInEmployee) - //{ - // if (projectAllocationDot != null) - // { - // List? result = new List(); - // List employeeIds = new List(); - // List projectIds = new List(); + /// + /// Manages project allocations for a list of employees, either adding new allocations or deactivating existing ones. + /// This method is optimized to perform all database operations in a single transaction. + /// + /// The list of allocation changes to process. + /// The ID of the current tenant. + /// The current authenticated employee for permission checks. + /// An ApiResponse containing the list of processed allocations. + public async Task>> ManageAllocationAsync(List allocationsDto, Guid tenantId, Employee loggedInEmployee) + { + // --- Step 1: Input Validation --- + if (allocationsDto == null || !allocationsDto.Any()) + { + return ApiResponse>.ErrorResponse("Invalid details.", "Allocation details list cannot be null or empty.", 400); + } - // foreach (var item in projectAllocationDot) - // { - // try - // { - // //ProjectAllocation projectAllocation = item.ToProjectAllocationFromProjectAllocationDto(tenantId); - // ProjectAllocation projectAllocation = item.ToProjectAllocationFromProjectAllocationDto(tenantId); - // ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == projectAllocation.EmployeeId - // && c.ProjectId == projectAllocation.ProjectId - // && c.ReAllocationDate == null - // && c.TenantId == tenantId).SingleOrDefaultAsync(); + _logger.LogInfo("Starting to manage {AllocationCount} allocations for user {UserId}.", allocationsDto.Count, loggedInEmployee.Id); - // if (projectAllocationFromDb != null) - // { - // _context.ProjectAllocations.Attach(projectAllocationFromDb); + // --- (Placeholder) Security Check --- + // In a real application, you would check if the loggedInEmployee has permission + // to manage allocations for ALL projects involved in this batch. + var projectIdsInBatch = allocationsDto.Select(a => a.ProjectId).Distinct().ToList(); + var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageTeam, loggedInEmployee.Id); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} trying to manage allocations for projects.", loggedInEmployee.Id); + return ApiResponse>.ErrorResponse("Access Denied.", "You do not have permission to manage one or more projects in this request.", 403); + } - // if (item.Status) - // { - // projectAllocationFromDb.JobRoleId = projectAllocation.JobRoleId; ; - // projectAllocationFromDb.IsActive = true; - // _context.Entry(projectAllocationFromDb).Property(e => e.JobRoleId).IsModified = true; - // _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; - // } - // else - // { - // projectAllocationFromDb.ReAllocationDate = DateTime.Now; - // projectAllocationFromDb.IsActive = false; - // _context.Entry(projectAllocationFromDb).Property(e => e.ReAllocationDate).IsModified = true; - // _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; + // --- Step 2: Fetch all relevant existing data in ONE database call --- + var employeeProjectPairs = allocationsDto.Select(a => new { a.EmpID, a.ProjectId }).ToList(); + List employeeIds = allocationsDto.Select(a => a.EmpID).Distinct().ToList(); - // employeeIds.Add(projectAllocation.EmployeeId); - // projectIds.Add(projectAllocation.ProjectId); - // } - // await _context.SaveChangesAsync(); - // var result1 = new - // { - // Id = projectAllocationFromDb.Id, - // EmployeeId = projectAllocation.EmployeeId, - // JobRoleId = projectAllocation.JobRoleId, - // IsActive = projectAllocation.IsActive, - // ProjectId = projectAllocation.ProjectId, - // AllocationDate = projectAllocation.AllocationDate, - // ReAllocationDate = projectAllocation.ReAllocationDate, - // TenantId = projectAllocation.TenantId - // }; - // result.Add(result1); - // } - // else - // { - // projectAllocation.AllocationDate = DateTime.Now; - // projectAllocation.IsActive = true; - // _context.ProjectAllocations.Add(projectAllocation); - // await _context.SaveChangesAsync(); + // Fetch all currently active allocations for the employees and projects in this batch. + // We use a dictionary for fast O(1) lookups inside the loop. + var existingAllocations = await _context.ProjectAllocations + .Where(pa => pa.TenantId == tenantId && + employeeIds.Contains(pa.EmployeeId) && + pa.ReAllocationDate == null) + .ToDictionaryAsync(pa => (pa.EmployeeId, pa.ProjectId)); - // employeeIds.Add(projectAllocation.EmployeeId); - // projectIds.Add(projectAllocation.ProjectId); - // } - // await _cache.ClearAllProjectIds(item.EmpID); + var processedAllocations = new List(); - // } - // catch (Exception ex) - // { - // return ApiResponse.ErrorResponse(ex.Message, ex, 400); - // } - // } + // --- Step 3: Process logic IN MEMORY --- + foreach (var dto in allocationsDto) + { + var key = (dto.EmpID, dto.ProjectId); + existingAllocations.TryGetValue(key, out var existingAllocation); - // return ApiResponse.SuccessResponse(result, "Data saved successfully", 200); + if (dto.Status == false) // User wants to DEACTIVATE the allocation + { + if (existingAllocation != null) + { + // Mark the existing allocation for deactivation + existingAllocation.ReAllocationDate = DateTime.UtcNow; // Use UtcNow for servers + existingAllocation.IsActive = false; + _context.ProjectAllocations.Update(existingAllocation); + processedAllocations.Add(existingAllocation); + } + // If it doesn't exist, we do nothing. The desired state is "not allocated". + } + else // User wants to ACTIVATE the allocation + { + if (existingAllocation == null) + { + // Create a new allocation because one doesn't exist + var newAllocation = _mapper.Map(dto); + newAllocation.TenantId = tenantId; + newAllocation.AllocationDate = DateTime.UtcNow; + newAllocation.IsActive = true; + _context.ProjectAllocations.Add(newAllocation); + processedAllocations.Add(newAllocation); + } + // If it already exists and is active, we do nothing. The state is already correct. + } + try + { + await _cache.ClearAllProjectIds(dto.EmpID); + _logger.LogInfo("Successfully completed cache invalidation for employee {EmployeeId}.", dto.EmpID); + } + catch (Exception ex) + { + // Log the error but don't fail the entire request, as the primary DB operation succeeded. + _logger.LogError(ex, "Cache invalidation failed for employees after a successful database update."); + } + } - // } - // return ApiResponse.ErrorResponse("Invalid details.", "Work Item Details are not valid.", 400); + try + { + // --- Step 4: Save all changes in a SINGLE TRANSACTION --- + // All Adds and Updates are sent to the database in one batch. + // If any part fails, the entire transaction is rolled back. + await _context.SaveChangesAsync(); + _logger.LogInfo("Successfully saved {ChangeCount} allocation changes to the database.", processedAllocations.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to save allocation changes to the database."); + return ApiResponse>.ErrorResponse("Database Error.", "An error occurred while saving the changes.", 500); + } - //} + + // --- Step 5: Map results and return success --- + var resultVm = _mapper.Map>(processedAllocations); + return ApiResponse>.SuccessResponse(resultVm, "Allocations managed successfully.", 200); + } #endregion diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs index d0539b0..2552444 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs @@ -1,6 +1,7 @@ using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Utilities; +using Marco.Pms.Model.ViewModels.Projects; namespace Marco.Pms.Services.Service.ServiceInterfaces { @@ -15,5 +16,6 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> UpdateProjectAsync(Guid id, UpdateProjectDto updateProjectDto, Guid tenantId, Employee loggedInEmployee); Task> GetEmployeeByProjectIdAsync(Guid? projectId, bool includeInactive, Guid tenantId, Employee loggedInEmployee); Task> GetProjectAllocationAsync(Guid? projectId, Guid tenantId, Employee loggedInEmployee); + Task>> ManageAllocationAsync(List projectAllocationDots, Guid tenantId, Employee loggedInEmployee); } }