Optimized the project allocation by employee Id Apis
This commit is contained in:
parent
9d0c16b887
commit
c3da83d165
@ -17,7 +17,6 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Microsoft.CodeAnalysis;
|
using Microsoft.CodeAnalysis;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
using Project = Marco.Pms.Model.Projects.Project;
|
|
||||||
|
|
||||||
namespace MarcoBMS.Services.Controllers
|
namespace MarcoBMS.Services.Controllers
|
||||||
{
|
{
|
||||||
@ -281,123 +280,42 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
[HttpGet("assigned-projects/{employeeId}")]
|
[HttpGet("assigned-projects/{employeeId}")]
|
||||||
public async Task<IActionResult> GetProjectsByEmployee([FromRoute] Guid employeeId)
|
public async Task<IActionResult> GetProjectsByEmployee([FromRoute] Guid employeeId)
|
||||||
{
|
{
|
||||||
if (employeeId == Guid.Empty)
|
// --- Step 1: Input Validation ---
|
||||||
|
if (!ModelState.IsValid)
|
||||||
{
|
{
|
||||||
return BadRequest(ApiResponse<object>.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<object>.ErrorResponse("Invalid request data provided.", errors, 400));
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Guid> projectList = await _context.ProjectAllocations
|
// --- Step 2: Prepare data without I/O ---
|
||||||
.Where(c => c.TenantId == tenantId && c.EmployeeId == employeeId && c.IsActive)
|
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
.Select(c => c.ProjectId).Distinct()
|
var response = await _projectServices.GetProjectsByEmployeeAsync(employeeId, tenantId, loggedInEmployee);
|
||||||
.ToListAsync();
|
return StatusCode(response.StatusCode, response);
|
||||||
|
|
||||||
if (!projectList.Any())
|
|
||||||
{
|
|
||||||
return NotFound(ApiResponse<object>.SuccessResponse(new List<object>(), "No projects found.", 200));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
List<Project> projectlist = await _context.Projects
|
|
||||||
.Where(p => projectList.Contains(p.Id))
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
List<ProjectListVM> projects = new List<ProjectListVM>();
|
|
||||||
|
|
||||||
|
|
||||||
foreach (var project in projectlist)
|
|
||||||
{
|
|
||||||
|
|
||||||
projects.Add(project.ToProjectListVMFromProject());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return Ok(ApiResponse<object>.SuccessResponse(projects, "Success.", 200));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("assign-projects/{employeeId}")]
|
[HttpPost("assign-projects/{employeeId}")]
|
||||||
public async Task<ActionResult> AssigneProjectsToEmployee([FromBody] List<ProjectsAllocationDto> projectAllocationDtos, [FromRoute] Guid employeeId)
|
public async Task<IActionResult> AssigneProjectsToEmployee([FromBody] List<ProjectsAllocationDto> projectAllocationDtos, [FromRoute] Guid employeeId)
|
||||||
{
|
{
|
||||||
if (projectAllocationDtos != null && employeeId != Guid.Empty)
|
// --- Step 1: Input Validation ---
|
||||||
|
if (!ModelState.IsValid)
|
||||||
{
|
{
|
||||||
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList();
|
||||||
List<object>? result = new List<object>();
|
_logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors));
|
||||||
List<Guid> projectIds = new List<Guid>();
|
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request data provided.", errors, 400));
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var projectAllocationDto in projectAllocationDtos)
|
// --- Step 2: Prepare data without I/O ---
|
||||||
{
|
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
try
|
var response = await _projectServices.AssigneProjectsToEmployeeAsync(projectAllocationDtos, employeeId, tenantId, loggedInEmployee);
|
||||||
{
|
if (response.Success)
|
||||||
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();
|
List<Guid> projectIds = response.Data.Select(pa => pa.ProjectId).ToList();
|
||||||
|
|
||||||
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<object>.ErrorResponse(ex.Message, ex, 400));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await _cache.ClearAllProjectIds(employeeId);
|
|
||||||
var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeId = employeeId };
|
|
||||||
|
|
||||||
|
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeId = employeeId };
|
||||||
await _signalR.SendNotificationAsync(notification);
|
await _signalR.SendNotificationAsync(notification);
|
||||||
|
|
||||||
return Ok(ApiResponse<object>.SuccessResponse(result, "Data saved successfully", 200));
|
|
||||||
}
|
}
|
||||||
else
|
return StatusCode(response.StatusCode, response);
|
||||||
{
|
|
||||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid details.", "All Field is required", 400));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
@ -48,6 +48,7 @@ namespace Marco.Pms.Services.MappingProfiles
|
|||||||
dest => dest.EmployeeId,
|
dest => dest.EmployeeId,
|
||||||
// Explicitly and safely convert string ProjectStatusId to Guid ProjectStatusId
|
// Explicitly and safely convert string ProjectStatusId to Guid ProjectStatusId
|
||||||
opt => opt.MapFrom(src => src.EmpID));
|
opt => opt.MapFrom(src => src.EmpID));
|
||||||
|
CreateMap<ProjectsAllocationDto, ProjectAllocation>();
|
||||||
CreateMap<ProjectAllocation, ProjectAllocationVM>();
|
CreateMap<ProjectAllocation, ProjectAllocationVM>();
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
@ -716,6 +716,186 @@ namespace Marco.Pms.Services.Service
|
|||||||
return ApiResponse<List<ProjectAllocationVM>>.SuccessResponse(resultVm, "Allocations managed successfully.", 200);
|
return ApiResponse<List<ProjectAllocationVM>>.SuccessResponse(resultVm, "Allocations managed successfully.", 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a list of active projects assigned to a specific employee.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="employeeId">The ID of the employee whose projects are being requested.</param>
|
||||||
|
/// <param name="tenantId">The ID of the current tenant.</param>
|
||||||
|
/// <param name="loggedInEmployee">The current authenticated employee for permission checks.</param>
|
||||||
|
/// <returns>An ApiResponse containing a list of basic project details or an error.</returns>
|
||||||
|
public async Task<ApiResponse<object>> GetProjectsByEmployeeAsync(Guid employeeId, Guid tenantId, Employee loggedInEmployee)
|
||||||
|
{
|
||||||
|
// --- Step 1: Input Validation ---
|
||||||
|
if (employeeId == Guid.Empty)
|
||||||
|
{
|
||||||
|
return ApiResponse<object>.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<object>.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<ProjectInfoVM>(_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<object>.SuccessResponse(new List<ProjectInfoVM>(), "No active projects found for this employee.", 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse<object>.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<object>.ErrorResponse("An internal server error occurred.", "Database query failed.", 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="allocationsDto">A list of projects to assign or un-assign.</param>
|
||||||
|
/// <param name="employeeId">The ID of the employee whose assignments are being managed.</param>
|
||||||
|
/// <param name="tenantId">The ID of the current tenant.</param>
|
||||||
|
/// <param name="loggedInEmployee">The current authenticated employee for permission checks.</param>
|
||||||
|
/// <returns>An ApiResponse containing the list of processed allocations.</returns>
|
||||||
|
public async Task<ApiResponse<List<ProjectAllocationVM>>> AssigneProjectsToEmployeeAsync(List<ProjectsAllocationDto> allocationsDto, Guid employeeId, Guid tenantId, Employee loggedInEmployee)
|
||||||
|
{
|
||||||
|
// --- Step 1: Input Validation ---
|
||||||
|
if (allocationsDto == null || !allocationsDto.Any() || employeeId == Guid.Empty)
|
||||||
|
{
|
||||||
|
return ApiResponse<List<ProjectAllocationVM>>.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<List<ProjectAllocationVM>>.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<ProjectAllocation>();
|
||||||
|
|
||||||
|
// --- 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<ProjectAllocation>(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<List<ProjectAllocationVM>>.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<List<ProjectAllocationVM>>(processedAllocations);
|
||||||
|
return ApiResponse<List<ProjectAllocationVM>>.SuccessResponse(resultVm, "Assignments managed successfully.", 200);
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region =================================================================== Helper Functions ===================================================================
|
#region =================================================================== Helper Functions ===================================================================
|
||||||
|
@ -17,5 +17,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
|||||||
Task<ApiResponse<object>> GetEmployeeByProjectIdAsync(Guid? projectId, bool includeInactive, Guid tenantId, Employee loggedInEmployee);
|
Task<ApiResponse<object>> GetEmployeeByProjectIdAsync(Guid? projectId, bool includeInactive, Guid tenantId, Employee loggedInEmployee);
|
||||||
Task<ApiResponse<object>> GetProjectAllocationAsync(Guid? projectId, Guid tenantId, Employee loggedInEmployee);
|
Task<ApiResponse<object>> GetProjectAllocationAsync(Guid? projectId, Guid tenantId, Employee loggedInEmployee);
|
||||||
Task<ApiResponse<List<ProjectAllocationVM>>> ManageAllocationAsync(List<ProjectAllocationDot> projectAllocationDots, Guid tenantId, Employee loggedInEmployee);
|
Task<ApiResponse<List<ProjectAllocationVM>>> ManageAllocationAsync(List<ProjectAllocationDot> projectAllocationDots, Guid tenantId, Employee loggedInEmployee);
|
||||||
|
Task<ApiResponse<object>> GetProjectsByEmployeeAsync(Guid employeeId, Guid tenantId, Employee loggedInEmployee);
|
||||||
|
Task<ApiResponse<List<ProjectAllocationVM>>> AssigneProjectsToEmployeeAsync(List<ProjectsAllocationDto> projectAllocationDtos, Guid employeeId, Guid tenantId, Employee loggedInEmployee);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user