Optimized the Project Allocation API

This commit is contained in:
ashutosh.nehete 2025-07-15 15:30:41 +05:30
parent f406a15508
commit 823deb17cc
6 changed files with 142 additions and 91 deletions

View File

@ -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;

View File

@ -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; }
}
}

View File

@ -252,28 +252,31 @@ namespace MarcoBMS.Services.Controllers
return StatusCode(response.StatusCode, response);
}
//[HttpPost("allocation")]
//public async Task<IActionResult> ManageAllocation(List<ProjectAllocationDot> 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<object>.ErrorResponse("Invalid request data provided.", errors, 400));
// }
[HttpPost("allocation")]
public async Task<IActionResult> ManageAllocation([FromBody] List<ProjectAllocationDot> 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<object>.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<Guid> employeeIds = response.Data.Select(pa => pa.EmployeeId).ToList();
List<Guid> 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<IActionResult> GetProjectsByEmployee([FromRoute] Guid employeeId)

View File

@ -43,6 +43,12 @@ namespace Marco.Pms.Services.MappingProfiles
CreateMap<StatusMasterMongoDB, StatusMaster>();
CreateMap<ProjectVM, Project>();
CreateMap<CreateProjectDto, Project>();
CreateMap<ProjectAllocationDot, ProjectAllocation>()
.ForMember(
dest => dest.EmployeeId,
// Explicitly and safely convert string ProjectStatusId to Guid ProjectStatusId
opt => opt.MapFrom(src => src.EmpID));
CreateMap<ProjectAllocation, ProjectAllocationVM>();
#endregion
#region ======================================================= Projects =======================================================

View File

@ -609,85 +609,112 @@ namespace Marco.Pms.Services.Service
}
}
//public async Task<ApiResponse<object>> ManageAllocation(List<ProjectAllocationDot> projectAllocationDot, Guid tenantId, Employee loggedInEmployee)
//{
// if (projectAllocationDot != null)
// {
// List<object>? result = new List<object>();
// List<Guid> employeeIds = new List<Guid>();
// List<Guid> projectIds = new List<Guid>();
/// <summary>
/// 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.
/// </summary>
/// <param name="allocationsDto">The list of allocation changes to process.</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>>> ManageAllocationAsync(List<ProjectAllocationDot> allocationsDto, Guid tenantId, Employee loggedInEmployee)
{
// --- Step 1: Input Validation ---
if (allocationsDto == null || !allocationsDto.Any())
{
return ApiResponse<List<ProjectAllocationVM>>.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<List<ProjectAllocationVM>>.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<Guid> 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<ProjectAllocation>();
// }
// catch (Exception ex)
// {
// return ApiResponse<object>.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<object>.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<ProjectAllocation>(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<object>.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<List<ProjectAllocationVM>>.ErrorResponse("Database Error.", "An error occurred while saving the changes.", 500);
}
//}
// --- Step 5: Map results and return success ---
var resultVm = _mapper.Map<List<ProjectAllocationVM>>(processedAllocations);
return ApiResponse<List<ProjectAllocationVM>>.SuccessResponse(resultVm, "Allocations managed successfully.", 200);
}
#endregion

View File

@ -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<ApiResponse<object>> UpdateProjectAsync(Guid id, UpdateProjectDto updateProjectDto, 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<List<ProjectAllocationVM>>> ManageAllocationAsync(List<ProjectAllocationDot> projectAllocationDots, Guid tenantId, Employee loggedInEmployee);
}
}