diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index b833064..82ce0dd 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -17,7 +17,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.CodeAnalysis; using Microsoft.EntityFrameworkCore; using MongoDB.Driver; -using Project = Marco.Pms.Model.Projects.Project; namespace MarcoBMS.Services.Controllers { @@ -281,123 +280,42 @@ namespace MarcoBMS.Services.Controllers [HttpGet("assigned-projects/{employeeId}")] public async Task GetProjectsByEmployee([FromRoute] Guid employeeId) { - if (employeeId == Guid.Empty) + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) { - return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Employee id not valid.", 400)); + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("Get project list by employee Id called with invalid model state \n Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - List projectList = await _context.ProjectAllocations - .Where(c => c.TenantId == tenantId && c.EmployeeId == employeeId && c.IsActive) - .Select(c => c.ProjectId).Distinct() - .ToListAsync(); - - if (!projectList.Any()) - { - return NotFound(ApiResponse.SuccessResponse(new List(), "No projects found.", 200)); - } - - - List projectlist = await _context.Projects - .Where(p => projectList.Contains(p.Id)) - .ToListAsync(); - - List projects = new List(); - - - foreach (var project in projectlist) - { - - projects.Add(project.ToProjectListVMFromProject()); - } - - - - return Ok(ApiResponse.SuccessResponse(projects, "Success.", 200)); + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetProjectsByEmployeeAsync(employeeId, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } [HttpPost("assign-projects/{employeeId}")] - public async Task AssigneProjectsToEmployee([FromBody] List projectAllocationDtos, [FromRoute] Guid employeeId) + public async Task AssigneProjectsToEmployee([FromBody] List projectAllocationDtos, [FromRoute] Guid employeeId) { - if (projectAllocationDtos != null && employeeId != Guid.Empty) + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) { - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - List? result = new List(); - List projectIds = new List(); + 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)); + } - foreach (var projectAllocationDto in projectAllocationDtos) - { - try - { - ProjectAllocation projectAllocation = projectAllocationDto.ToProjectAllocationFromProjectsAllocationDto(tenantId, employeeId); - ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.ProjectId == projectAllocationDto.ProjectId && c.ReAllocationDate == null && c.TenantId == tenantId).SingleOrDefaultAsync(); - - if (projectAllocationFromDb != null) - { - - - _context.ProjectAllocations.Attach(projectAllocationFromDb); - - if (projectAllocationDto.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.UtcNow; - projectAllocationFromDb.IsActive = false; - _context.Entry(projectAllocationFromDb).Property(e => e.ReAllocationDate).IsModified = true; - _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; - - 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(); - - projectIds.Add(projectAllocation.ProjectId); - - } - - - } - catch (Exception ex) - { - - return Ok(ApiResponse.ErrorResponse(ex.Message, ex, 400)); - } - } - await _cache.ClearAllProjectIds(employeeId); - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeId = employeeId }; + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.AssigneProjectsToEmployeeAsync(projectAllocationDtos, employeeId, tenantId, loggedInEmployee); + if (response.Success) + { + List projectIds = response.Data.Select(pa => pa.ProjectId).ToList(); + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeId = employeeId }; await _signalR.SendNotificationAsync(notification); - - return Ok(ApiResponse.SuccessResponse(result, "Data saved successfully", 200)); } - else - { - return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "All Field is required", 400)); - } - + return StatusCode(response.StatusCode, response); } #endregion diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index 3ca1271..ea42d16 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -48,6 +48,7 @@ namespace Marco.Pms.Services.MappingProfiles dest => dest.EmployeeId, // Explicitly and safely convert string ProjectStatusId to Guid ProjectStatusId opt => opt.MapFrom(src => src.EmpID)); + CreateMap(); CreateMap(); #endregion diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs index 33df2c0..9024112 100644 --- a/Marco.Pms.Services/Service/ProjectServices.cs +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -716,6 +716,186 @@ namespace Marco.Pms.Services.Service return ApiResponse>.SuccessResponse(resultVm, "Allocations managed successfully.", 200); } + /// + /// Retrieves a list of active projects assigned to a specific employee. + /// + /// The ID of the employee whose projects are being requested. + /// The ID of the current tenant. + /// The current authenticated employee for permission checks. + /// An ApiResponse containing a list of basic project details or an error. + public async Task> GetProjectsByEmployeeAsync(Guid employeeId, Guid tenantId, Employee loggedInEmployee) + { + // --- Step 1: Input Validation --- + if (employeeId == Guid.Empty) + { + return ApiResponse.ErrorResponse("Invalid details.", "A valid employee ID is required.", 400); + } + + _logger.LogInfo("Fetching projects for Employee {EmployeeId} by User {UserId}", employeeId, loggedInEmployee.Id); + + try + { + // --- Step 2: Clarified Security Check --- + // The permission should be about viewing another employee's assignments, not a generic "Manage Team". + // This is a placeholder for your actual, more specific permission logic. + // It should also handle the case where a user is requesting their own projects (employeeId == loggedInEmployee.Id). + var hasPermission = await _permission.HasPermission(PermissionsMaster.ViewProject, loggedInEmployee.Id); + var projectIds = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} trying to view projects for employee {TargetEmployeeId}.", loggedInEmployee.Id, employeeId); + return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to view this employee's projects.", 403); + } + + // --- Step 3: Execute a Single, Highly Efficient Database Query --- + // This query projects directly to the ViewModel on the database server. + var projects = await _context.ProjectAllocations + // 1. Filter the linking table down to the relevant records. + .Where(pa => + pa.TenantId == tenantId && + pa.EmployeeId == employeeId && // Target the specified employee + pa.IsActive && // Only active assignments + projectIds.Contains(pa.ProjectId) && + pa.Project != null) // Safety check for data integrity + + // 2. Navigate to the Project entity. + .Select(pa => pa.Project) + + // 3. Ensure the final result set is unique (in case of multiple active allocations to the same project). + .Distinct() + + // 4. Project directly to the ViewModel using AutoMapper's IQueryable Extensions. + // This generates an efficient SQL "SELECT Id, Name, Code FROM..." statement. + .ProjectTo(_mapper.ConfigurationProvider) + + // 5. Execute the query. + .ToListAsync(); + + _logger.LogInfo("Successfully retrieved {ProjectCount} projects for employee {EmployeeId}.", projects.Count, employeeId); + + // The original check for an empty list is still good practice. + if (!projects.Any()) + { + return ApiResponse.SuccessResponse(new List(), "No active projects found for this employee.", 200); + } + + return ApiResponse.SuccessResponse(projects, "Projects retrieved successfully.", 200); + } + catch (Exception ex) + { + // --- Step 4: Graceful Error Handling --- + _logger.LogError(ex, "An error occurred while fetching projects for employee {EmployeeId}.", employeeId); + return ApiResponse.ErrorResponse("An internal server error occurred.", "Database query failed.", 500); + } + } + + /// + /// Manages project assignments for a single employee, processing a batch of projects to activate or deactivate. + /// This method is optimized to perform all database operations in a single, atomic transaction. + /// + /// A list of projects to assign or un-assign. + /// The ID of the employee whose assignments are being managed. + /// The ID of the current tenant. + /// The current authenticated employee for permission checks. + /// An ApiResponse containing the list of processed allocations. + public async Task>> AssigneProjectsToEmployeeAsync(List allocationsDto, Guid employeeId, Guid tenantId, Employee loggedInEmployee) + { + // --- Step 1: Input Validation --- + if (allocationsDto == null || !allocationsDto.Any() || employeeId == Guid.Empty) + { + return ApiResponse>.ErrorResponse("Invalid details.", "A valid employee ID and a list of projects are required.", 400); + } + + _logger.LogInfo("Starting to manage {AllocationCount} project assignments for Employee {EmployeeId}.", allocationsDto.Count, employeeId); + + // --- (Placeholder) Security Check --- + // You MUST verify that the loggedInEmployee has permission to modify the assignments for the target employeeId. + var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageTeam, loggedInEmployee.Id); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} trying to manage assignments for employee {TargetEmployeeId}.", loggedInEmployee.Id, employeeId); + return ApiResponse>.ErrorResponse("Access Denied.", "You do not have permission to manage this employee's assignments.", 403); + } + + // --- Step 2: Fetch all relevant existing data in ONE database call --- + var projectIdsInDto = allocationsDto.Select(p => p.ProjectId).ToList(); + + // Fetch all currently active allocations for this employee for the projects in the request. + // We use a dictionary keyed by ProjectId for fast O(1) lookups inside the loop. + var existingActiveAllocations = await _context.ProjectAllocations + .Where(pa => pa.TenantId == tenantId && + pa.EmployeeId == employeeId && + projectIdsInDto.Contains(pa.ProjectId) && + pa.ReAllocationDate == null) // Only fetch active ones + .ToDictionaryAsync(pa => pa.ProjectId); + + var processedAllocations = new List(); + + // --- Step 3: Process all logic IN MEMORY, tracking changes --- + foreach (var dto in allocationsDto) + { + existingActiveAllocations.TryGetValue(dto.ProjectId, out var existingAllocation); + + if (dto.Status == false) // DEACTIVATE this project assignment + { + if (existingAllocation != null) + { + // Correct Update Pattern: Modify the fetched entity directly. + existingAllocation.ReAllocationDate = DateTime.UtcNow; // Use UTC for servers + existingAllocation.IsActive = false; + _context.ProjectAllocations.Update(existingAllocation); + processedAllocations.Add(existingAllocation); + } + // If it's not in our dictionary, it's already inactive. Do nothing. + } + else // ACTIVATE this project assignment + { + if (existingAllocation == null) + { + // Create a new allocation because an active one doesn't exist. + var newAllocation = _mapper.Map(dto); + newAllocation.EmployeeId = employeeId; + newAllocation.TenantId = tenantId; + newAllocation.AllocationDate = DateTime.UtcNow; + newAllocation.IsActive = true; + _context.ProjectAllocations.Add(newAllocation); + processedAllocations.Add(newAllocation); + } + // If it already exists in our dictionary, it's already active. Do nothing. + } + } + + try + { + // --- Step 4: Save all Adds and Updates in a SINGLE ATOMIC TRANSACTION --- + if (processedAllocations.Any()) + { + await _context.SaveChangesAsync(); + _logger.LogInfo("Successfully saved {ChangeCount} assignment changes for employee {EmployeeId}.", processedAllocations.Count, employeeId); + } + } + catch (DbUpdateException ex) + { + _logger.LogError(ex, "Failed to save assignment changes for employee {EmployeeId}.", employeeId); + return ApiResponse>.ErrorResponse("Database Error.", "An error occurred while saving the changes.", 500); + } + + // --- Step 5: Invalidate Cache ONCE after successful save --- + try + { + await _cache.ClearAllProjectIds(employeeId); + _logger.LogInfo("Successfully queued cache invalidation for employee {EmployeeId}.", employeeId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Background cache invalidation failed for employee {EmployeeId}", employeeId); + } + + // --- Step 6: Map results using AutoMapper and return success --- + var resultVm = _mapper.Map>(processedAllocations); + return ApiResponse>.SuccessResponse(resultVm, "Assignments managed successfully.", 200); + } + #endregion #region =================================================================== Helper Functions =================================================================== diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs index 2552444..bafa582 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs @@ -17,5 +17,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces 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); + Task> GetProjectsByEmployeeAsync(Guid employeeId, Guid tenantId, Employee loggedInEmployee); + Task>> AssigneProjectsToEmployeeAsync(List projectAllocationDtos, Guid employeeId, Guid tenantId, Employee loggedInEmployee); } }