From ed16c0f1027b530d614780e962d8962111f071ee Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Fri, 14 Nov 2025 11:16:57 +0530 Subject: [PATCH] Added an API to change the status of the job ticket --- .../Dtos/ServiceProject/ChangeJobStatusDto.cs | 9 ++ .../Controllers/ServiceProjectController.cs | 13 ++ .../ServiceInterfaces/IServiceProject.cs | 1 + .../Service/ServiceProjectService.cs | 118 ++++++++++++++++++ 4 files changed, 141 insertions(+) create mode 100644 Marco.Pms.Model/Dtos/ServiceProject/ChangeJobStatusDto.cs diff --git a/Marco.Pms.Model/Dtos/ServiceProject/ChangeJobStatusDto.cs b/Marco.Pms.Model/Dtos/ServiceProject/ChangeJobStatusDto.cs new file mode 100644 index 0000000..7278bc1 --- /dev/null +++ b/Marco.Pms.Model/Dtos/ServiceProject/ChangeJobStatusDto.cs @@ -0,0 +1,9 @@ +namespace Marco.Pms.Model.Dtos.ServiceProject +{ + public class ChangeJobStatusDto + { + public required Guid JobTicketId { get; set; } + public required Guid StatusId { get; set; } + public required string Comment { get; set; } + } +} diff --git a/Marco.Pms.Services/Controllers/ServiceProjectController.cs b/Marco.Pms.Services/Controllers/ServiceProjectController.cs index afdc616..bdeb2bc 100644 --- a/Marco.Pms.Services/Controllers/ServiceProjectController.cs +++ b/Marco.Pms.Services/Controllers/ServiceProjectController.cs @@ -143,6 +143,19 @@ namespace Marco.Pms.Services.Controllers return StatusCode(response.StatusCode, response); } + [HttpPost("job/status-change")] + public async Task ChangeJobsStatus(ChangeJobStatusDto model) + { + Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _serviceProject.ChangeJobsStatusAsync(model, loggedInEmploee, tenantId); + if (response.Success) + { + var notification = new { LoggedInUserId = loggedInEmploee.Id, Keyword = "Job_Ticket", Response = response.Data }; + await _signalR.SendNotificationAsync(notification); + } + return StatusCode(response.StatusCode, response); + } + [HttpPost("job/add/comment")] public async Task AddCommentToJobTicket(JobCommentDto model) { diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IServiceProject.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IServiceProject.cs index b5eea4d..1118483 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IServiceProject.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IServiceProject.cs @@ -21,6 +21,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> GetCommentListByJobTicketAsync(Guid? jobTicketId, int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId); Task> GetJobTagListAsync(Employee loggedInEmployee, Guid tenantId); Task> CreateJobTicketAsync(CreateJobTicketDto model, Employee loggedInEmployee, Guid tenantId); + Task> ChangeJobsStatusAsync(ChangeJobStatusDto model, Employee loggedInEmployee, Guid tenantId); Task> AddCommentToJobTicketAsync(JobCommentDto model, Employee loggedInEmployee, Guid tenantId); #endregion } diff --git a/Marco.Pms.Services/Service/ServiceProjectService.cs b/Marco.Pms.Services/Service/ServiceProjectService.cs index ab8454a..f531ff3 100644 --- a/Marco.Pms.Services/Service/ServiceProjectService.cs +++ b/Marco.Pms.Services/Service/ServiceProjectService.cs @@ -828,6 +828,124 @@ namespace Marco.Pms.Services.Service } } + /// + /// Changes the status of a specified job ticket, recording the change in a status update log. + /// Ensures team-role-aware status transitions if applicable. + /// + /// DTO containing target status ID, job ticket ID, and optional comment. + /// Employee performing the status change (for audit and permissions). + /// Tenant context for multi-tenancy. + /// ApiResponse with updated job ticket or error info. + public async Task> ChangeJobsStatusAsync(ChangeJobStatusDto model, Employee loggedInEmployee, Guid tenantId) + { + if (tenantId == Guid.Empty) + { + _logger.LogWarning("ChangeJobsStatusAsync: Invalid (empty) tenantId for employee {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Access Denied", "Invalid tenant context.", 403); + } + + if (model == null || model.JobTicketId == Guid.Empty || model.StatusId == Guid.Empty) + { + _logger.LogInfo("ChangeJobsStatusAsync: Invalid parameters submitted by employee {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Bad Request", "Job or status ID is missing.", 400); + } + + try + { + _logger.LogInfo("Attempting to change status for job {JobTicketId} to {NewStatusId} by employee {EmployeeId}", + model.JobTicketId, model.StatusId, loggedInEmployee.Id); + + // Load the job ticket and key navigation properties + var jobTicket = await _context.JobTickets + .Include(jt => jt.Project) + .Include(jt => jt.CreatedBy).ThenInclude(e => e!.JobRole) + .FirstOrDefaultAsync(jt => jt.Id == model.JobTicketId && jt.TenantId == tenantId); + + if (jobTicket == null) + { + _logger.LogWarning("Job ticket {JobTicketId} not found for status change in tenant {TenantId}", model.JobTicketId, tenantId); + return ApiResponse.ErrorResponse("Job Not Found", "Job ticket not found.", 404); + } + + // Find transition mappings for the current status and desired status, considering team role if allocation exists + var statusMappingQuery = _context.JobStatusMappings + .Include(jsm => jsm.Status) + .Include(jsm => jsm.NextStatus) + .Where(jsm => + jsm.StatusId == jobTicket.StatusId && + jsm.NextStatusId == model.StatusId && + jsm.Status != null && + jsm.NextStatus != null && + jsm.TenantId == tenantId); + + // Find allocation for current employee (to determine team role for advanced mapping) + var projectAllocation = await _context.ServiceProjectAllocations + .Where(spa => spa.EmployeeId == loggedInEmployee.Id && + spa.ProjectId == jobTicket.ProjectId && + spa.TenantId == tenantId && + spa.IsActive) + .OrderByDescending(spa => spa.AssignedAt) + .FirstOrDefaultAsync(); + + var teamRoleId = projectAllocation?.TeamRoleId; + var hasTeamRoleMapping = projectAllocation != null + && await statusMappingQuery.AnyAsync(jsm => jsm.TeamRoleId == teamRoleId); + + // Filter by team role or fallback to global (null team role) + if (hasTeamRoleMapping) + { + statusMappingQuery = statusMappingQuery.Where(jsm => jsm.TeamRoleId == teamRoleId); + } + else + { + statusMappingQuery = statusMappingQuery.Where(jsm => jsm.TeamRoleId == null); + } + + var jobStatusMapping = await statusMappingQuery.FirstOrDefaultAsync(); + + if (jobStatusMapping == null) + { + _logger.LogWarning("Invalid status transition requested: current={CurrentStatusId}, desired={DesiredStatusId}, tenant={TenantId}", + jobTicket.StatusId, model.StatusId, tenantId); + return ApiResponse.ErrorResponse("Invalid Status", "Selected status transition is not allowed.", 400); + } + + // Apply the new status and metadata + jobTicket.StatusId = model.StatusId; + jobTicket.UpdatedAt = DateTime.UtcNow; + jobTicket.UpdatedById = loggedInEmployee.Id; + + // Write status change log + var updateLog = new StatusUpdateLog + { + Id = Guid.NewGuid(), + EntityId = jobTicket.Id, + StatusId = jobStatusMapping.StatusId, + NextStatusId = jobStatusMapping.NextStatusId, + Comment = model.Comment, + UpdatedAt = DateTime.UtcNow, + UpdatedById = loggedInEmployee.Id, + TenantId = tenantId + }; + _context.StatusUpdateLogs.Add(updateLog); + + await _context.SaveChangesAsync(); + + // Prepare response VM + var responseVm = _mapper.Map(jobTicket); + responseVm.Status = jobStatusMapping.NextStatus; + + _logger.LogInfo("Job {JobTicketId} status changed to {NewStatusId} by employee {EmployeeId}", jobTicket.Id, model.StatusId, loggedInEmployee.Id); + + return ApiResponse.SuccessResponse(responseVm, "Job status changed successfully.", 200); + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception during ChangeJobsStatusAsync for job {JobTicketId} by employee {EmployeeId} in tenant {TenantId}", model.JobTicketId, loggedInEmployee.Id, tenantId); + return ApiResponse.ErrorResponse("Internal Server Error", "Failed to change job status. Please try again later.", 500); + } + } + #endregion #region =================================================================== Job Comments Functions ===================================================================