diff --git a/Marco.Pms.Model/Dtos/ServiceProject/ServiceProjectAllocationDto.cs b/Marco.Pms.Model/Dtos/ServiceProject/ServiceProjectAllocationDto.cs new file mode 100644 index 0000000..006557f --- /dev/null +++ b/Marco.Pms.Model/Dtos/ServiceProject/ServiceProjectAllocationDto.cs @@ -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; } + } +} diff --git a/Marco.Pms.Model/ViewModels/ServiceProject/ServiceProjectAllocationVM.cs b/Marco.Pms.Model/ViewModels/ServiceProject/ServiceProjectAllocationVM.cs new file mode 100644 index 0000000..c59a84b --- /dev/null +++ b/Marco.Pms.Model/ViewModels/ServiceProject/ServiceProjectAllocationVM.cs @@ -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; } + } +} diff --git a/Marco.Pms.Services/Controllers/EmployeeController.cs b/Marco.Pms.Services/Controllers/EmployeeController.cs index 04dfa90..aa58e95 100644 --- a/Marco.Pms.Services/Controllers/EmployeeController.cs +++ b/Marco.Pms.Services/Controllers/EmployeeController.cs @@ -327,7 +327,7 @@ namespace MarcoBMS.Services.Controllers } [HttpGet("basic")] - public async Task GetEmployeesByProjectBasic(Guid? projectId, [FromQuery] string? searchString, [FromQuery] bool allEmployee) + public async Task GetEmployeesByProjectBasic(Guid? projectId, [FromQuery] Guid? employeeId, [FromQuery] string? searchString, [FromQuery] bool allEmployee) { var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); 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)); } + if (employeeId.HasValue) + { + employeeQuery = employeeQuery.Where(e => e.Id == employeeId.Value); + } + var query = employeeQuery.OrderBy(e => e.FirstName); if (!allEmployee) diff --git a/Marco.Pms.Services/Controllers/ServiceProjectController.cs b/Marco.Pms.Services/Controllers/ServiceProjectController.cs index 1e3d357..7ec2216 100644 --- a/Marco.Pms.Services/Controllers/ServiceProjectController.cs +++ b/Marco.Pms.Services/Controllers/ServiceProjectController.cs @@ -96,6 +96,29 @@ namespace Marco.Pms.Services.Controllers } #endregion + #region =================================================================== Service Project Allocation Functions =================================================================== + + [HttpPost("get/allocation/list")] + public async Task 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 ManageServiceProjectAllocation([FromBody] List 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 =================================================================== [HttpGet("job/list")] @@ -116,15 +139,6 @@ namespace Marco.Pms.Services.Controllers return StatusCode(response.StatusCode, response); } - [HttpGet("job/comment/list")] - public async Task 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")] public async Task GetJobTagList() { @@ -135,7 +149,7 @@ namespace Marco.Pms.Services.Controllers } [HttpPost("job/create")] - public async Task CreateJobTicket(CreateJobTicketDto model) + public async Task CreateJobTicket([FromBody] CreateJobTicketDto model) { Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _serviceProject.CreateJobTicketAsync(model, loggedInEmploee, tenantId); @@ -148,7 +162,7 @@ namespace Marco.Pms.Services.Controllers } [HttpPost("job/status-change")] - public async Task ChangeJobsStatus(ChangeJobStatusDto model) + public async Task ChangeJobsStatus([FromBody] ChangeJobStatusDto model) { Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _serviceProject.ChangeJobsStatusAsync(model, loggedInEmploee, tenantId); @@ -161,7 +175,7 @@ namespace Marco.Pms.Services.Controllers } [HttpPatch("job/edit/{id}")] - public async Task UpdateJobTicket(Guid id, JsonPatchDocument patchDoc) + public async Task UpdateJobTicket(Guid id, [FromBody] JsonPatchDocument patchDoc) { // Validate incoming patch document if (patchDoc == null) @@ -204,9 +218,21 @@ namespace Marco.Pms.Services.Controllers return StatusCode(response.StatusCode, response); } + #endregion + + #region =================================================================== Job Comments Functions =================================================================== + + [HttpGet("job/comment/list")] + public async Task 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")] - public async Task AddCommentToJobTicket(JobCommentDto model) + public async Task AddCommentToJobTicket([FromBody] JobCommentDto model) { Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync(); var response = await _serviceProject.AddCommentToJobTicketAsync(model, loggedInEmploee, tenantId); @@ -217,6 +243,7 @@ namespace Marco.Pms.Services.Controllers } return StatusCode(response.StatusCode, response); } + #endregion } } diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index 4278a27..1828751 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -198,6 +198,7 @@ namespace Marco.Pms.Services.MappingProfiles CreateMap(); CreateMap(); CreateMap(); + CreateMap(); #region ======================================================= Job Ticket ======================================================= CreateMap(); diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IServiceProject.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IServiceProject.cs index 65773a7..bd40b70 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IServiceProject.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IServiceProject.cs @@ -15,6 +15,10 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> DeActivateServiceProjectAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId); #endregion + #region =================================================================== Service Project Allocation Functions =================================================================== + Task> GetServiceProjectAllocationListAsync(Guid? projectId, Guid? employeeId, Employee loggedInEmployee, Guid tenantId); + Task> ManageServiceProjectAllocationAsync(List model, Employee loggedInEmployee, Guid tenantId); + #endregion #region =================================================================== Job Tickets Functions =================================================================== Task> GetJobTicketsListAsync(Guid? projectId, int pageNumber, int pageSize, bool isActive, Employee loggedInEmployee, Guid tenantId); Task> GetJobTicketDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId); @@ -24,6 +28,10 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> ChangeJobsStatusAsync(ChangeJobStatusDto model, Employee loggedInEmployee, Guid tenantId); Task> UpdateJobTicketAsync(Guid id, JobTicket jobTicket, UpdateJobTicketDto model, Employee loggedInEmployee, Guid tenantId); Task> AddCommentToJobTicketAsync(JobCommentDto model, Employee loggedInEmployee, Guid tenantId); + + #endregion + #region =================================================================== Job Comments Functions =================================================================== + #endregion #region =================================================================== Pubic Helper Functions =================================================================== diff --git a/Marco.Pms.Services/Service/ServiceProjectService.cs b/Marco.Pms.Services/Service/ServiceProjectService.cs index 9815a19..77365b5 100644 --- a/Marco.Pms.Services/Service/ServiceProjectService.cs +++ b/Marco.Pms.Services/Service/ServiceProjectService.cs @@ -362,7 +362,145 @@ namespace Marco.Pms.Services.Service #endregion - #region =================================================================== Expense Functions =================================================================== + #region =================================================================== Service Project Allocation Functions =================================================================== + public async Task> GetServiceProjectAllocationListAsync(Guid? projectId, Guid? employeeId, Employee loggedInEmployee, Guid tenantId) + { + return ApiResponse.SuccessResponse(projectId, "Service project allocation fetched successfully", 200); + } + /// + /// Manages service project allocations by adding new active allocations and removing inactive ones. + /// Validates projects, employees, and team roles exist before applying changes. + /// + /// List of allocation DTOs specifying project, employee, team role, and active status. + /// Employee performing the allocation management (for audit). + /// Tenant identifier for multi-tenant data isolation. + /// ApiResponse containing the updated list of active allocations or error details. + public async Task> ManageServiceProjectAllocationAsync(List model, Employee loggedInEmployee, Guid tenantId) + { + if (tenantId == Guid.Empty) + { + _logger.LogWarning("ManageServiceProjectAllocationAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.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.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(); + var allocationsToRemove = new List(); + + // 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>(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.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.ErrorResponse("Internal Server Error", "An error occurred while managing service project allocations. Please try again later.", 500); + } + } + #endregion #region =================================================================== Job Tickets Functions =================================================================== @@ -924,12 +1062,7 @@ namespace Marco.Pms.Services.Service /// Employee performing the update (used for audit and authorization). /// Tenant identifier for data isolation. /// ApiResponse containing updated job ticket data or error details. - public async Task> UpdateJobTicketAsync( - Guid id, - JobTicket jobTicket, - UpdateJobTicketDto model, - Employee loggedInEmployee, - Guid tenantId) + public async Task> UpdateJobTicketAsync(Guid id, JobTicket jobTicket, UpdateJobTicketDto model, Employee loggedInEmployee, Guid tenantId) { // Validate tenant context early if (tenantId == Guid.Empty) @@ -1166,7 +1299,6 @@ namespace Marco.Pms.Services.Service } } - #endregion #region =================================================================== Job Comments Functions ===================================================================