Added an API to allocate employee to service project

This commit is contained in:
ashutosh.nehete 2025-11-14 16:51:01 +05:30
parent fbb8a2261b
commit 62031046a1
7 changed files with 223 additions and 22 deletions

View File

@ -0,0 +1,10 @@
namespace Marco.Pms.Model.Dtos.ServiceProject
{
public class ServiceProjectAllocationDto
{
public Guid ProjectId { get; set; }
public Guid EmployeeId { get; set; }
public Guid TeamRoleId { get; set; }
public bool IsActive { get; set; }
}
}

View File

@ -0,0 +1,18 @@
using Marco.Pms.Model.ServiceProject;
using Marco.Pms.Model.ViewModels.Activities;
namespace Marco.Pms.Model.ViewModels.ServiceProject
{
public class ServiceProjectAllocationVM
{
public Guid Id { get; set; }
public BasicServiceProjectVM? Project { get; set; }
public BasicEmployeeVM? Employee { get; set; }
public TeamRoleMaster? TeamRole { get; set; }
public bool IsActive { get; set; } = true;
public DateTime AssignedAt { get; set; }
public BasicEmployeeVM? AssignedBy { get; set; }
public DateTime? ReAssignedAt { get; set; }
public BasicEmployeeVM? ReAssignedBy { get; set; }
}
}

View File

@ -327,7 +327,7 @@ namespace MarcoBMS.Services.Controllers
} }
[HttpGet("basic")] [HttpGet("basic")]
public async Task<IActionResult> GetEmployeesByProjectBasic(Guid? projectId, [FromQuery] string? searchString, [FromQuery] bool allEmployee) public async Task<IActionResult> GetEmployeesByProjectBasic(Guid? projectId, [FromQuery] Guid? employeeId, [FromQuery] string? searchString, [FromQuery] bool allEmployee)
{ {
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var employeeQuery = _context.Employees.Where(e => e.IsActive); var employeeQuery = _context.Employees.Where(e => e.IsActive);
@ -352,6 +352,11 @@ namespace MarcoBMS.Services.Controllers
employeeQuery = employeeQuery.Where(e => (e.FirstName + " " + e.LastName).ToLower().Contains(searchStringLower)); employeeQuery = employeeQuery.Where(e => (e.FirstName + " " + e.LastName).ToLower().Contains(searchStringLower));
} }
if (employeeId.HasValue)
{
employeeQuery = employeeQuery.Where(e => e.Id == employeeId.Value);
}
var query = employeeQuery.OrderBy(e => e.FirstName); var query = employeeQuery.OrderBy(e => e.FirstName);
if (!allEmployee) if (!allEmployee)

View File

@ -96,6 +96,29 @@ namespace Marco.Pms.Services.Controllers
} }
#endregion #endregion
#region =================================================================== Service Project Allocation Functions ===================================================================
[HttpPost("get/allocation/list")]
public async Task<IActionResult> GetServiceProjectAllocationList([FromQuery] Guid? projectId, [FromQuery] Guid? employeeId)
{
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _serviceProject.GetServiceProjectAllocationListAsync(projectId, employeeId, loggedInEmploee, tenantId);
return StatusCode(response.StatusCode, response);
}
[HttpPost("manage/allocation")]
public async Task<IActionResult> ManageServiceProjectAllocation([FromBody] List<ServiceProjectAllocationDto> model)
{
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _serviceProject.ManageServiceProjectAllocationAsync(model, loggedInEmploee, tenantId);
if (response.Success)
{
var notification = new { LoggedInUserId = loggedInEmploee.Id, Keyword = "Service_Project_Allocation", Response = response.Data };
await _signalR.SendNotificationAsync(notification);
}
return StatusCode(response.StatusCode, response);
}
#endregion
#region =================================================================== Job Tickets Functions =================================================================== #region =================================================================== Job Tickets Functions ===================================================================
[HttpGet("job/list")] [HttpGet("job/list")]
@ -116,15 +139,6 @@ namespace Marco.Pms.Services.Controllers
return StatusCode(response.StatusCode, response); return StatusCode(response.StatusCode, response);
} }
[HttpGet("job/comment/list")]
public async Task<IActionResult> GetCommentListByJobTicket([FromQuery] Guid? jobTicketId, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20)
{
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _serviceProject.GetCommentListByJobTicketAsync(jobTicketId, pageNumber, pageSize, loggedInEmploee, tenantId);
return StatusCode(response.StatusCode, response);
}
[HttpGet("job/tag/list")] [HttpGet("job/tag/list")]
public async Task<IActionResult> GetJobTagList() public async Task<IActionResult> GetJobTagList()
{ {
@ -135,7 +149,7 @@ namespace Marco.Pms.Services.Controllers
} }
[HttpPost("job/create")] [HttpPost("job/create")]
public async Task<IActionResult> CreateJobTicket(CreateJobTicketDto model) public async Task<IActionResult> CreateJobTicket([FromBody] CreateJobTicketDto model)
{ {
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync(); Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _serviceProject.CreateJobTicketAsync(model, loggedInEmploee, tenantId); var response = await _serviceProject.CreateJobTicketAsync(model, loggedInEmploee, tenantId);
@ -148,7 +162,7 @@ namespace Marco.Pms.Services.Controllers
} }
[HttpPost("job/status-change")] [HttpPost("job/status-change")]
public async Task<IActionResult> ChangeJobsStatus(ChangeJobStatusDto model) public async Task<IActionResult> ChangeJobsStatus([FromBody] ChangeJobStatusDto model)
{ {
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync(); Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _serviceProject.ChangeJobsStatusAsync(model, loggedInEmploee, tenantId); var response = await _serviceProject.ChangeJobsStatusAsync(model, loggedInEmploee, tenantId);
@ -161,7 +175,7 @@ namespace Marco.Pms.Services.Controllers
} }
[HttpPatch("job/edit/{id}")] [HttpPatch("job/edit/{id}")]
public async Task<IActionResult> UpdateJobTicket(Guid id, JsonPatchDocument<UpdateJobTicketDto> patchDoc) public async Task<IActionResult> UpdateJobTicket(Guid id, [FromBody] JsonPatchDocument<UpdateJobTicketDto> patchDoc)
{ {
// Validate incoming patch document // Validate incoming patch document
if (patchDoc == null) if (patchDoc == null)
@ -204,9 +218,21 @@ namespace Marco.Pms.Services.Controllers
return StatusCode(response.StatusCode, response); return StatusCode(response.StatusCode, response);
} }
#endregion
#region =================================================================== Job Comments Functions ===================================================================
[HttpGet("job/comment/list")]
public async Task<IActionResult> GetCommentListByJobTicket([FromQuery] Guid? jobTicketId, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20)
{
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _serviceProject.GetCommentListByJobTicketAsync(jobTicketId, pageNumber, pageSize, loggedInEmploee, tenantId);
return StatusCode(response.StatusCode, response);
}
[HttpPost("job/add/comment")] [HttpPost("job/add/comment")]
public async Task<IActionResult> AddCommentToJobTicket(JobCommentDto model) public async Task<IActionResult> AddCommentToJobTicket([FromBody] JobCommentDto model)
{ {
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync(); Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _serviceProject.AddCommentToJobTicketAsync(model, loggedInEmploee, tenantId); var response = await _serviceProject.AddCommentToJobTicketAsync(model, loggedInEmploee, tenantId);
@ -217,6 +243,7 @@ namespace Marco.Pms.Services.Controllers
} }
return StatusCode(response.StatusCode, response); return StatusCode(response.StatusCode, response);
} }
#endregion #endregion
} }
} }

View File

@ -198,6 +198,7 @@ namespace Marco.Pms.Services.MappingProfiles
CreateMap<ServiceProject, ServiceProjectVM>(); CreateMap<ServiceProject, ServiceProjectVM>();
CreateMap<ServiceProject, BasicServiceProjectVM>(); CreateMap<ServiceProject, BasicServiceProjectVM>();
CreateMap<ServiceProject, ServiceProjectDetailsVM>(); CreateMap<ServiceProject, ServiceProjectDetailsVM>();
CreateMap<ServiceProjectAllocation, ServiceProjectAllocationVM>();
#region ======================================================= Job Ticket ======================================================= #region ======================================================= Job Ticket =======================================================
CreateMap<CreateJobTicketDto, JobTicket>(); CreateMap<CreateJobTicketDto, JobTicket>();

View File

@ -15,6 +15,10 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
Task<ApiResponse<object>> DeActivateServiceProjectAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId); Task<ApiResponse<object>> DeActivateServiceProjectAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId);
#endregion #endregion
#region =================================================================== Service Project Allocation Functions ===================================================================
Task<ApiResponse<object>> GetServiceProjectAllocationListAsync(Guid? projectId, Guid? employeeId, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> ManageServiceProjectAllocationAsync(List<ServiceProjectAllocationDto> model, Employee loggedInEmployee, Guid tenantId);
#endregion
#region =================================================================== Job Tickets Functions =================================================================== #region =================================================================== Job Tickets Functions ===================================================================
Task<ApiResponse<object>> GetJobTicketsListAsync(Guid? projectId, int pageNumber, int pageSize, bool isActive, Employee loggedInEmployee, Guid tenantId); Task<ApiResponse<object>> GetJobTicketsListAsync(Guid? projectId, int pageNumber, int pageSize, bool isActive, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> GetJobTicketDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId); Task<ApiResponse<object>> GetJobTicketDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId);
@ -24,6 +28,10 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
Task<ApiResponse<object>> ChangeJobsStatusAsync(ChangeJobStatusDto model, Employee loggedInEmployee, Guid tenantId); Task<ApiResponse<object>> ChangeJobsStatusAsync(ChangeJobStatusDto model, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> UpdateJobTicketAsync(Guid id, JobTicket jobTicket, UpdateJobTicketDto model, Employee loggedInEmployee, Guid tenantId); Task<ApiResponse<object>> UpdateJobTicketAsync(Guid id, JobTicket jobTicket, UpdateJobTicketDto model, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> AddCommentToJobTicketAsync(JobCommentDto model, Employee loggedInEmployee, Guid tenantId); Task<ApiResponse<object>> AddCommentToJobTicketAsync(JobCommentDto model, Employee loggedInEmployee, Guid tenantId);
#endregion
#region =================================================================== Job Comments Functions ===================================================================
#endregion #endregion
#region =================================================================== Pubic Helper Functions =================================================================== #region =================================================================== Pubic Helper Functions ===================================================================

View File

@ -362,7 +362,145 @@ namespace Marco.Pms.Services.Service
#endregion #endregion
#region =================================================================== Expense Functions =================================================================== #region =================================================================== Service Project Allocation Functions ===================================================================
public async Task<ApiResponse<object>> GetServiceProjectAllocationListAsync(Guid? projectId, Guid? employeeId, Employee loggedInEmployee, Guid tenantId)
{
return ApiResponse<object>.SuccessResponse(projectId, "Service project allocation fetched successfully", 200);
}
/// <summary>
/// Manages service project allocations by adding new active allocations and removing inactive ones.
/// Validates projects, employees, and team roles exist before applying changes.
/// </summary>
/// <param name="model">List of allocation DTOs specifying project, employee, team role, and active status.</param>
/// <param name="loggedInEmployee">Employee performing the allocation management (for audit).</param>
/// <param name="tenantId">Tenant identifier for multi-tenant data isolation.</param>
/// <returns>ApiResponse containing the updated list of active allocations or error details.</returns>
public async Task<ApiResponse<object>> ManageServiceProjectAllocationAsync(List<ServiceProjectAllocationDto> model, Employee loggedInEmployee, Guid tenantId)
{
if (tenantId == Guid.Empty)
{
_logger.LogWarning("ManageServiceProjectAllocationAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
}
if (model == null || !model.Any())
{
_logger.LogInfo("Empty allocation model provided by employee {EmployeeId}", loggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("Bad Request", "No allocation data provided.", 400);
}
try
{
// Extract distinct IDs from input for efficient bulk queries
var projectIds = model.Select(spa => spa.ProjectId).Distinct().ToList();
var employeeIds = model.Select(spa => spa.EmployeeId).Distinct().ToList();
var teamRoleIds = model.Select(spa => spa.TeamRoleId).Distinct().ToList();
// Load required reference data concurrently using separate DbContexts
var projectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ServiceProjects.Where(sp => projectIds.Contains(sp.Id) && sp.TenantId == tenantId && sp.IsActive).ToListAsync();
});
var employeeTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Employees.Where(e => employeeIds.Contains(e.Id) && e.IsActive).ToListAsync();
});
var teamRoleTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.TeamRoleMasters.Where(trm => teamRoleIds.Contains(trm.Id)).ToListAsync();
});
var allocationTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ServiceProjectAllocations.Where(spa => projectIds.Contains(spa.ProjectId) && spa.TenantId == tenantId && spa.IsActive).ToListAsync();
});
await Task.WhenAll(projectTask, employeeTask, teamRoleTask, allocationTask);
var projects = projectTask.Result;
var employees = employeeTask.Result;
var teamRoles = teamRoleTask.Result;
var allocations = allocationTask.Result;
var newAllocations = new List<ServiceProjectAllocation>();
var allocationsToRemove = new List<ServiceProjectAllocation>();
// Process each input allocation DTO
foreach (var dto in model)
{
// Validate referenced entities exist
var project = projects.FirstOrDefault(sp => sp.Id == dto.ProjectId);
var employee = employees.FirstOrDefault(e => e.Id == dto.EmployeeId);
var teamRole = teamRoles.FirstOrDefault(tr => tr.Id == dto.TeamRoleId);
if (project == null || employee == null || teamRole == null)
{
_logger.LogWarning("Skipping allocation with invalid references: ProjectId={ProjectId}, EmployeeId={EmployeeId}, TeamRoleId={TeamRoleId}", dto.ProjectId, dto.EmployeeId, dto.TeamRoleId);
continue;
}
var existingAllocation = allocations.FirstOrDefault(spa =>
spa.ProjectId == dto.ProjectId &&
spa.EmployeeId == dto.EmployeeId &&
spa.TeamRoleId == dto.TeamRoleId);
if (dto.IsActive && existingAllocation == null)
{
newAllocations.Add(new ServiceProjectAllocation
{
Id = Guid.NewGuid(),
ProjectId = dto.ProjectId,
EmployeeId = dto.EmployeeId,
TeamRoleId = dto.TeamRoleId,
AssignedAt = DateTime.UtcNow,
AssignedById = loggedInEmployee.Id,
IsActive = true,
TenantId = tenantId
});
}
else if (!dto.IsActive && existingAllocation != null)
{
allocationsToRemove.Add(existingAllocation);
}
}
// Batch changes for efficiency
if (newAllocations.Any()) _context.ServiceProjectAllocations.AddRange(newAllocations);
if (allocationsToRemove.Any()) _context.ServiceProjectAllocations.RemoveRange(allocationsToRemove);
await _context.SaveChangesAsync();
// Reload current active allocations for response with relevant navigation properties
var updatedAllocations = await _context.ServiceProjectAllocations
.Include(spa => spa.Project)
.Include(spa => spa.TeamRole)
.Include(spa => spa.Employee).ThenInclude(e => e!.JobRole)
.Include(spa => spa.AssignedBy).ThenInclude(e => e!.JobRole)
.Where(spa =>
projectIds.Contains(spa.ProjectId) &&
employeeIds.Contains(spa.EmployeeId) &&
teamRoleIds.Contains(spa.TeamRoleId) &&
spa.IsActive &&
spa.TenantId == tenantId)
.ToListAsync();
var response = _mapper.Map<List<ServiceProjectAllocationVM>>(updatedAllocations);
_logger.LogInfo("Service project allocations managed successfully by employee {EmployeeId} for tenant {TenantId}. Added {AddedCount}, Removed {RemovedCount}",
loggedInEmployee.Id, tenantId, newAllocations.Count, allocationsToRemove.Count);
return ApiResponse<object>.SuccessResponse(response, "Service project allocations managed successfully", 200);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error managing service project allocations by employee {EmployeeId} in tenant {TenantId}", loggedInEmployee.Id, tenantId);
return ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while managing service project allocations. Please try again later.", 500);
}
}
#endregion #endregion
#region =================================================================== Job Tickets Functions =================================================================== #region =================================================================== Job Tickets Functions ===================================================================
@ -924,12 +1062,7 @@ namespace Marco.Pms.Services.Service
/// <param name="loggedInEmployee">Employee performing the update (used for audit and authorization).</param> /// <param name="loggedInEmployee">Employee performing the update (used for audit and authorization).</param>
/// <param name="tenantId">Tenant identifier for data isolation.</param> /// <param name="tenantId">Tenant identifier for data isolation.</param>
/// <returns>ApiResponse containing updated job ticket data or error details.</returns> /// <returns>ApiResponse containing updated job ticket data or error details.</returns>
public async Task<ApiResponse<object>> UpdateJobTicketAsync( public async Task<ApiResponse<object>> UpdateJobTicketAsync(Guid id, JobTicket jobTicket, UpdateJobTicketDto model, Employee loggedInEmployee, Guid tenantId)
Guid id,
JobTicket jobTicket,
UpdateJobTicketDto model,
Employee loggedInEmployee,
Guid tenantId)
{ {
// Validate tenant context early // Validate tenant context early
if (tenantId == Guid.Empty) if (tenantId == Guid.Empty)
@ -1166,7 +1299,6 @@ namespace Marco.Pms.Services.Service
} }
} }
#endregion #endregion
#region =================================================================== Job Comments Functions =================================================================== #region =================================================================== Job Comments Functions ===================================================================