Added an API to update the job ticket
This commit is contained in:
parent
ed16c0f102
commit
fbb8a2261b
@ -1,7 +1,7 @@
|
|||||||
using Marco.Pms.Model.Dtos.Employees;
|
using Marco.Pms.Model.Dtos.Employees;
|
||||||
using Marco.Pms.Model.Dtos.Master;
|
using Marco.Pms.Model.Dtos.Master;
|
||||||
|
|
||||||
namespace Marco.Pms.Model.ViewModels.ServiceProject
|
namespace Marco.Pms.Model.Dtos.ServiceProject
|
||||||
{
|
{
|
||||||
public class CreateJobTicketDto
|
public class CreateJobTicketDto
|
||||||
{
|
{
|
||||||
17
Marco.Pms.Model/Dtos/ServiceProject/UpdateJobTicketDto.cs
Normal file
17
Marco.Pms.Model/Dtos/ServiceProject/UpdateJobTicketDto.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using Marco.Pms.Model.Dtos.Employees;
|
||||||
|
using Marco.Pms.Model.Dtos.Master;
|
||||||
|
|
||||||
|
namespace Marco.Pms.Model.Dtos.ServiceProject
|
||||||
|
{
|
||||||
|
public class UpdateJobTicketDto
|
||||||
|
{
|
||||||
|
public string? Title { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public Guid ProjectId { get; set; }
|
||||||
|
public Guid StatusId { get; set; }
|
||||||
|
public List<BasicEmployeeDto>? Assignees { get; set; }
|
||||||
|
public DateTime StartDate { get; set; }
|
||||||
|
public DateTime DueDate { get; set; }
|
||||||
|
public List<TagDto>? Tags { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,10 +1,11 @@
|
|||||||
using Marco.Pms.Model.Dtos.ServiceProject;
|
using AutoMapper;
|
||||||
|
using Marco.Pms.Model.Dtos.ServiceProject;
|
||||||
using Marco.Pms.Model.Employees;
|
using Marco.Pms.Model.Employees;
|
||||||
using Marco.Pms.Model.ViewModels.ServiceProject;
|
using Marco.Pms.Model.Utilities;
|
||||||
using Marco.Pms.Services.Service.ServiceInterfaces;
|
using Marco.Pms.Services.Service.ServiceInterfaces;
|
||||||
using MarcoBMS.Services.Helpers;
|
using MarcoBMS.Services.Helpers;
|
||||||
using MarcoBMS.Services.Service;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.JsonPatch;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Marco.Pms.Services.Controllers
|
namespace Marco.Pms.Services.Controllers
|
||||||
@ -15,15 +16,18 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
public class ServiceProjectController : Controller
|
public class ServiceProjectController : Controller
|
||||||
{
|
{
|
||||||
private readonly IServiceProject _serviceProject;
|
private readonly IServiceProject _serviceProject;
|
||||||
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
private readonly UserHelper _userHelper;
|
private readonly UserHelper _userHelper;
|
||||||
private readonly ILoggingService _logger;
|
|
||||||
private readonly ISignalRService _signalR;
|
private readonly ISignalRService _signalR;
|
||||||
private readonly Guid tenantId;
|
private readonly Guid tenantId;
|
||||||
public ServiceProjectController(IServiceProject serviceProject, UserHelper userHelper, ILoggingService logger, ISignalRService signalR)
|
public ServiceProjectController(IServiceProject serviceProject,
|
||||||
|
IServiceScopeFactory serviceScopeFactory,
|
||||||
|
UserHelper userHelper,
|
||||||
|
ISignalRService signalR)
|
||||||
{
|
{
|
||||||
_serviceProject = serviceProject;
|
_serviceProject = serviceProject;
|
||||||
|
_serviceScopeFactory = serviceScopeFactory;
|
||||||
_userHelper = userHelper;
|
_userHelper = userHelper;
|
||||||
_logger = logger;
|
|
||||||
_signalR = signalR;
|
_signalR = signalR;
|
||||||
tenantId = userHelper.GetTenantId();
|
tenantId = userHelper.GetTenantId();
|
||||||
|
|
||||||
@ -156,6 +160,51 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
return StatusCode(response.StatusCode, response);
|
return StatusCode(response.StatusCode, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPatch("job/edit/{id}")]
|
||||||
|
public async Task<IActionResult> UpdateJobTicket(Guid id, JsonPatchDocument<UpdateJobTicketDto> patchDoc)
|
||||||
|
{
|
||||||
|
// Validate incoming patch document
|
||||||
|
if (patchDoc == null)
|
||||||
|
{
|
||||||
|
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request", "Patch document cannot be null", 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the currently logged in employee
|
||||||
|
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
|
|
||||||
|
// Retrieve the job ticket by id and tenant context
|
||||||
|
var jobTicket = await _serviceProject.GetJobTicketByIdAsync(id, tenantId);
|
||||||
|
if (jobTicket == null)
|
||||||
|
{
|
||||||
|
return NotFound(ApiResponse<object>.ErrorResponse("Job ticket not found", $"No active job ticket found with id {id}", 404));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a scoped Mapper instance to map the entity to the DTO for patching
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var mapper = scope.ServiceProvider.GetRequiredService<IMapper>();
|
||||||
|
var modelToPatch = mapper.Map<UpdateJobTicketDto>(jobTicket);
|
||||||
|
|
||||||
|
// Apply the JSON Patch document to the DTO and check model state validity
|
||||||
|
patchDoc.ApplyTo(modelToPatch, ModelState);
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
return BadRequest(ApiResponse<object>.ErrorResponse("Validation failed", "Provided patch document values are invalid", 400));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the job ticket with the patched model and logged-in user info
|
||||||
|
var response = await _serviceProject.UpdateJobTicketAsync(id, jobTicket, modelToPatch, loggedInEmployee, tenantId);
|
||||||
|
if (response.Success)
|
||||||
|
{
|
||||||
|
// If update successful, send SignalR notification with relevant info
|
||||||
|
var notificationPayload = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Job_Ticket", Response = response.Data };
|
||||||
|
await _signalR.SendNotificationAsync(notificationPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return status codes with appropriate response
|
||||||
|
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(JobCommentDto model)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -198,8 +198,11 @@ namespace Marco.Pms.Services.MappingProfiles
|
|||||||
CreateMap<ServiceProject, ServiceProjectVM>();
|
CreateMap<ServiceProject, ServiceProjectVM>();
|
||||||
CreateMap<ServiceProject, BasicServiceProjectVM>();
|
CreateMap<ServiceProject, BasicServiceProjectVM>();
|
||||||
CreateMap<ServiceProject, ServiceProjectDetailsVM>();
|
CreateMap<ServiceProject, ServiceProjectDetailsVM>();
|
||||||
|
|
||||||
#region ======================================================= Job Ticket =======================================================
|
#region ======================================================= Job Ticket =======================================================
|
||||||
CreateMap<CreateJobTicketDto, JobTicket>();
|
CreateMap<CreateJobTicketDto, JobTicket>();
|
||||||
|
CreateMap<UpdateJobTicketDto, JobTicket>();
|
||||||
|
CreateMap<JobTicket, UpdateJobTicketDto>();
|
||||||
CreateMap<JobTicket, JobTicketVM>();
|
CreateMap<JobTicket, JobTicketVM>();
|
||||||
CreateMap<JobTicket, JobTicketDetailsVM>();
|
CreateMap<JobTicket, JobTicketDetailsVM>();
|
||||||
CreateMap<JobTicket, BasicJobTicketVM>()
|
CreateMap<JobTicket, BasicJobTicketVM>()
|
||||||
@ -210,6 +213,7 @@ namespace Marco.Pms.Services.MappingProfiles
|
|||||||
CreateMap<JobComment, JobCommentVM>();
|
CreateMap<JobComment, JobCommentVM>();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region ======================================================= Employee =======================================================
|
#region ======================================================= Employee =======================================================
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.20" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.20" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.12" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.12" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.12" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.12" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.22" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.7" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.7" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.12">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.12">
|
||||||
|
|||||||
@ -72,7 +72,7 @@ builder.Services.AddCors(options =>
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Core Web & Framework Services
|
#region Core Web & Framework Services
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers().AddNewtonsoftJson();
|
||||||
builder.Services.AddSignalR();
|
builder.Services.AddSignalR();
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddHttpContextAccessor();
|
builder.Services.AddHttpContextAccessor();
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
using Marco.Pms.Model.Dtos.ServiceProject;
|
using Marco.Pms.Model.Dtos.ServiceProject;
|
||||||
using Marco.Pms.Model.Employees;
|
using Marco.Pms.Model.Employees;
|
||||||
|
using Marco.Pms.Model.ServiceProject;
|
||||||
using Marco.Pms.Model.Utilities;
|
using Marco.Pms.Model.Utilities;
|
||||||
using Marco.Pms.Model.ViewModels.ServiceProject;
|
|
||||||
|
|
||||||
namespace Marco.Pms.Services.Service.ServiceInterfaces
|
namespace Marco.Pms.Services.Service.ServiceInterfaces
|
||||||
{
|
{
|
||||||
@ -22,8 +22,13 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
|||||||
Task<ApiResponse<object>> GetJobTagListAsync(Employee loggedInEmployee, Guid tenantId);
|
Task<ApiResponse<object>> GetJobTagListAsync(Employee loggedInEmployee, Guid tenantId);
|
||||||
Task<ApiResponse<object>> CreateJobTicketAsync(CreateJobTicketDto model, Employee loggedInEmployee, Guid tenantId);
|
Task<ApiResponse<object>> CreateJobTicketAsync(CreateJobTicketDto model, Employee loggedInEmployee, Guid tenantId);
|
||||||
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>> AddCommentToJobTicketAsync(JobCommentDto model, Employee loggedInEmployee, Guid tenantId);
|
Task<ApiResponse<object>> AddCommentToJobTicketAsync(JobCommentDto model, Employee loggedInEmployee, Guid tenantId);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region =================================================================== Pubic Helper Functions ===================================================================
|
||||||
|
Task<JobTicket?> GetJobTicketByIdAsync(Guid id, Guid tenantId);
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Marco.Pms.DataAccess.Data;
|
using Marco.Pms.DataAccess.Data;
|
||||||
|
using Marco.Pms.Helpers.Utility;
|
||||||
using Marco.Pms.Model.DocumentManager;
|
using Marco.Pms.Model.DocumentManager;
|
||||||
using Marco.Pms.Model.Dtos.ServiceProject;
|
using Marco.Pms.Model.Dtos.ServiceProject;
|
||||||
using Marco.Pms.Model.Employees;
|
using Marco.Pms.Model.Employees;
|
||||||
using Marco.Pms.Model.Master;
|
using Marco.Pms.Model.Master;
|
||||||
|
using Marco.Pms.Model.MongoDBModels.Utility;
|
||||||
using Marco.Pms.Model.ServiceProject;
|
using Marco.Pms.Model.ServiceProject;
|
||||||
using Marco.Pms.Model.Utilities;
|
using Marco.Pms.Model.Utilities;
|
||||||
using Marco.Pms.Model.ViewModels.Activities;
|
using Marco.Pms.Model.ViewModels.Activities;
|
||||||
@ -14,6 +16,7 @@ using Marco.Pms.Model.ViewModels.ServiceProject;
|
|||||||
using Marco.Pms.Services.Service.ServiceInterfaces;
|
using Marco.Pms.Services.Service.ServiceInterfaces;
|
||||||
using MarcoBMS.Services.Service;
|
using MarcoBMS.Services.Service;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using MongoDB.Bson;
|
||||||
|
|
||||||
namespace Marco.Pms.Services.Service
|
namespace Marco.Pms.Services.Service
|
||||||
{
|
{
|
||||||
@ -867,41 +870,7 @@ namespace Marco.Pms.Services.Service
|
|||||||
return ApiResponse<object>.ErrorResponse("Job Not Found", "Job ticket not found.", 404);
|
return ApiResponse<object>.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 jobStatusMapping = await GetJobStatusMappingAsync(jobTicket.StatusId, model.StatusId, jobTicket.ProjectId, loggedInEmployee.Id, tenantId);
|
||||||
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)
|
if (jobStatusMapping == null)
|
||||||
{
|
{
|
||||||
@ -946,6 +915,258 @@ namespace Marco.Pms.Services.Service
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates a job ticket including its core details, assignees, tags, and manages status transitions with audit logs.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">ID of the job ticket to update.</param>
|
||||||
|
/// <param name="jobTicket">Existing job ticket entity to update.</param>
|
||||||
|
/// <param name="model">DTO containing updated job ticket values.</param>
|
||||||
|
/// <param name="loggedInEmployee">Employee performing the update (used for audit and authorization).</param>
|
||||||
|
/// <param name="tenantId">Tenant identifier for data isolation.</param>
|
||||||
|
/// <returns>ApiResponse containing updated job ticket data or error details.</returns>
|
||||||
|
public async Task<ApiResponse<object>> UpdateJobTicketAsync(
|
||||||
|
Guid id,
|
||||||
|
JobTicket jobTicket,
|
||||||
|
UpdateJobTicketDto model,
|
||||||
|
Employee loggedInEmployee,
|
||||||
|
Guid tenantId)
|
||||||
|
{
|
||||||
|
// Validate tenant context early
|
||||||
|
if (tenantId == Guid.Empty)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("UpdateJobTicketAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id);
|
||||||
|
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Concurrently load referenced project and status entities to validate foreign keys
|
||||||
|
var projectTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await context.ServiceProjects.FirstOrDefaultAsync(sp => sp.Id == model.ProjectId && sp.TenantId == tenantId && sp.IsActive);
|
||||||
|
});
|
||||||
|
var statusTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await context.JobStatus.FirstOrDefaultAsync(js => js.Id == model.StatusId);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(projectTask, statusTask);
|
||||||
|
|
||||||
|
// Validate existence of foreign entities
|
||||||
|
if (projectTask.Result == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Service project not found during job ticket update. ProjectId: {ProjectId}, TenantId: {TenantId}", model.ProjectId, tenantId);
|
||||||
|
return ApiResponse<object>.ErrorResponse("Service project not found", "Service project not found", 404);
|
||||||
|
}
|
||||||
|
if (statusTask.Result == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Job status not found during job ticket update. StatusId: {StatusId}", model.StatusId);
|
||||||
|
return ApiResponse<object>.ErrorResponse("Job status not found", "Job status not found", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin database transaction for atomicity
|
||||||
|
await using var transaction = await _context.Database.BeginTransactionAsync();
|
||||||
|
|
||||||
|
// Handle status change with validation and log creation
|
||||||
|
if (jobTicket.StatusId != model.StatusId)
|
||||||
|
{
|
||||||
|
var jobStatusMapping = await GetJobStatusMappingAsync(jobTicket.StatusId, model.StatusId, jobTicket.ProjectId, loggedInEmployee.Id, tenantId);
|
||||||
|
if (jobStatusMapping == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Invalid status transition requested from {CurrentStatusId} to {NewStatusId} in tenant {TenantId}",
|
||||||
|
jobTicket.StatusId, model.StatusId, tenantId);
|
||||||
|
return ApiResponse<object>.ErrorResponse("Invalid Status", "Selected status transition is not allowed.", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
var comment = $"Status changed from {jobStatusMapping.Status!.Name} to {jobStatusMapping.NextStatus!.Name}";
|
||||||
|
var updateLog = new StatusUpdateLog
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
EntityId = jobTicket.Id,
|
||||||
|
StatusId = jobStatusMapping.StatusId,
|
||||||
|
NextStatusId = jobStatusMapping.NextStatusId,
|
||||||
|
Comment = comment,
|
||||||
|
UpdatedAt = DateTime.UtcNow,
|
||||||
|
UpdatedById = loggedInEmployee.Id,
|
||||||
|
TenantId = tenantId
|
||||||
|
};
|
||||||
|
_context.StatusUpdateLogs.Add(updateLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create BSON snapshot of existing entity for audit logging (MongoDB)
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
|
||||||
|
BsonDocument existingEntityBson = updateLogHelper.EntityToBsonDocument(jobTicket);
|
||||||
|
|
||||||
|
// Map updated properties from DTO, set audit metadata
|
||||||
|
_mapper.Map(model, jobTicket);
|
||||||
|
jobTicket.UpdatedAt = DateTime.UtcNow;
|
||||||
|
jobTicket.UpdatedById = loggedInEmployee.Id;
|
||||||
|
|
||||||
|
_context.JobTickets.Update(jobTicket);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Handle assignee changes: add new and remove inactive mappings
|
||||||
|
if (model.Assignees?.Any() == true)
|
||||||
|
{
|
||||||
|
var employeeIds = model.Assignees.Select(e => e.EmployeeId).ToList();
|
||||||
|
|
||||||
|
var employeeTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await context.Employees.Where(e => employeeIds.Contains(e.Id)).ToListAsync();
|
||||||
|
});
|
||||||
|
var jobEmployeeMappingTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await context.JobEmployeeMappings.Where(jem => employeeIds.Contains(jem.AssigneeId) && jem.TenantId == tenantId).ToListAsync();
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(employeeTask, jobEmployeeMappingTask);
|
||||||
|
|
||||||
|
var employees = employeeTask.Result;
|
||||||
|
var jobEmployeeMappings = jobEmployeeMappingTask.Result;
|
||||||
|
|
||||||
|
var newMappings = new List<JobEmployeeMapping>();
|
||||||
|
var removeMappings = new List<JobEmployeeMapping>();
|
||||||
|
|
||||||
|
foreach (var assignee in model.Assignees)
|
||||||
|
{
|
||||||
|
var employee = employees.FirstOrDefault(e => e.Id == assignee.EmployeeId);
|
||||||
|
var mapping = jobEmployeeMappings.FirstOrDefault(jem => jem.AssigneeId == assignee.EmployeeId);
|
||||||
|
|
||||||
|
if (assignee.IsActive && mapping == null && employee != null)
|
||||||
|
{
|
||||||
|
newMappings.Add(new JobEmployeeMapping
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
AssigneeId = assignee.EmployeeId,
|
||||||
|
JobTicketId = jobTicket.Id,
|
||||||
|
TenantId = tenantId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (!assignee.IsActive && mapping != null)
|
||||||
|
{
|
||||||
|
removeMappings.Add(mapping);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newMappings.Any()) _context.JobEmployeeMappings.AddRange(newMappings);
|
||||||
|
if (removeMappings.Any()) _context.JobEmployeeMappings.RemoveRange(removeMappings);
|
||||||
|
}
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Handle tag changes: add new tags/mappings and remove inactive mappings
|
||||||
|
if (model.Tags?.Any() == true)
|
||||||
|
{
|
||||||
|
var tagNames = model.Tags.Select(jt => jt.Name).ToList();
|
||||||
|
|
||||||
|
var tagTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await context.JobTags.Where(jt => tagNames.Contains(jt.Name) && jt.TenantId == tenantId).ToListAsync();
|
||||||
|
});
|
||||||
|
|
||||||
|
var tagMappingTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await context.JobTagMappings.Where(jtm => jtm.JobTicketId == jobTicket.Id && jtm.TenantId == tenantId).ToListAsync();
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(tagTask, tagMappingTask);
|
||||||
|
|
||||||
|
var existingTags = tagTask.Result;
|
||||||
|
var existingTagMappings = tagMappingTask.Result;
|
||||||
|
|
||||||
|
var newJobTags = new List<JobTag>();
|
||||||
|
var newJobTagMappings = new List<JobTagMapping>();
|
||||||
|
var removeJobTagMappings = new List<JobTagMapping>();
|
||||||
|
|
||||||
|
foreach (var tagDto in model.Tags)
|
||||||
|
{
|
||||||
|
var tag = existingTags.FirstOrDefault(jt => jt.Name == tagDto.Name);
|
||||||
|
if (tag == null)
|
||||||
|
{
|
||||||
|
tag = new JobTag
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = tagDto.Name,
|
||||||
|
TenantId = tenantId
|
||||||
|
};
|
||||||
|
newJobTags.Add(tag);
|
||||||
|
}
|
||||||
|
var tagMapping = existingTagMappings.FirstOrDefault(jtm => jtm.JobTagId == tag.Id);
|
||||||
|
|
||||||
|
if (tagDto.IsActive && tagMapping == null)
|
||||||
|
{
|
||||||
|
newJobTagMappings.Add(new JobTagMapping
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
JobTagId = tag.Id,
|
||||||
|
JobTicketId = jobTicket.Id,
|
||||||
|
TenantId = tenantId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (!tagDto.IsActive && tagMapping != null)
|
||||||
|
{
|
||||||
|
removeJobTagMappings.Add(tagMapping);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newJobTags.Any()) _context.JobTags.AddRange(newJobTags);
|
||||||
|
if (newJobTagMappings.Any()) _context.JobTagMappings.AddRange(newJobTagMappings);
|
||||||
|
if (removeJobTagMappings.Any()) _context.JobTagMappings.RemoveRange(removeJobTagMappings);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
await transaction.CommitAsync();
|
||||||
|
|
||||||
|
// Push update log asynchronously to MongoDB for audit
|
||||||
|
var updateLogTask = updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
|
||||||
|
{
|
||||||
|
EntityId = id.ToString(),
|
||||||
|
UpdatedById = loggedInEmployee.Id.ToString(),
|
||||||
|
OldObject = existingEntityBson,
|
||||||
|
UpdatedAt = DateTime.UtcNow
|
||||||
|
}, "JobTicketModificationLog");
|
||||||
|
|
||||||
|
// Reload updated job ticket with navigation properties
|
||||||
|
var jobTicketTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await context.JobTickets
|
||||||
|
.Include(jt => jt.Status)
|
||||||
|
.Include(jt => jt.Project)
|
||||||
|
.Include(jt => jt.CreatedBy).ThenInclude(e => e!.JobRole)
|
||||||
|
.Include(jt => jt.UpdatedBy).ThenInclude(e => e!.JobRole)
|
||||||
|
.FirstOrDefaultAsync(jt => jt.Id == id && jt.TenantId == tenantId);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(updateLogTask, jobTicketTask);
|
||||||
|
|
||||||
|
jobTicket = jobTicketTask.Result ?? new JobTicket();
|
||||||
|
|
||||||
|
var response = _mapper.Map<JobTicketVM>(jobTicket);
|
||||||
|
|
||||||
|
_logger.LogInfo("Job ticket {JobTicketId} updated successfully by employee {EmployeeId} in tenant {TenantId}", id, loggedInEmployee.Id, tenantId);
|
||||||
|
|
||||||
|
return ApiResponse<object>.SuccessResponse(response, "Job updated successfully", 200);
|
||||||
|
}
|
||||||
|
catch (DbUpdateException dbEx)
|
||||||
|
{
|
||||||
|
_logger.LogError(dbEx, "Database error while updating job ticket for project {ProjectId} by employee {EmployeeId} in tenant {TenantId}",
|
||||||
|
model.ProjectId, loggedInEmployee.Id, tenantId);
|
||||||
|
return ApiResponse<object>.ErrorResponse("Database Error", "An error occurred while saving data to the database.", 500);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Unhandled exception while updating job ticket for project {ProjectId} by employee {EmployeeId} in tenant {TenantId}",
|
||||||
|
model.ProjectId, loggedInEmployee.Id, tenantId);
|
||||||
|
return ApiResponse<object>.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
#region =================================================================== Job Comments Functions ===================================================================
|
#region =================================================================== Job Comments Functions ===================================================================
|
||||||
|
|
||||||
@ -1195,5 +1416,83 @@ namespace Marco.Pms.Services.Service
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
#region =================================================================== Pubic Helper Functions ===================================================================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a job ticket by its unique identifier and associated tenant ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The unique identifier of the job ticket.</param>
|
||||||
|
/// <param name="tenantId">The tenant identifier for multi-tenant isolation.</param>
|
||||||
|
/// <returns>The job ticket if found; otherwise, null.</returns>
|
||||||
|
public async Task<JobTicket?> GetJobTicketByIdAsync(Guid id, Guid tenantId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInfo($"Attempting to retrieve job ticket with ID: {id} for tenant: {tenantId}");
|
||||||
|
|
||||||
|
// Use AsNoTracking for read-only queries to improve performance
|
||||||
|
var jobTicket = await _context.JobTickets
|
||||||
|
.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(jt => jt.Id == id && jt.TenantId == tenantId);
|
||||||
|
|
||||||
|
if (jobTicket == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning($"Job ticket not found. ID: {id}, TenantID: {tenantId}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogInfo($"Job ticket found. ID: {id}, TenantID: {tenantId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobTicket;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"An error occurred while retrieving job ticket. ID: {id}, TenantID: {tenantId}");
|
||||||
|
// Consider whether you want to rethrow or return null on error; returning null here
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<JobStatusMapping?> GetJobStatusMappingAsync(Guid statusId, Guid nextStatusId, Guid projectId, Guid loggedInEmployeeId, Guid tenantId)
|
||||||
|
{
|
||||||
|
// 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 == statusId &&
|
||||||
|
jsm.NextStatusId == nextStatusId &&
|
||||||
|
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 == loggedInEmployeeId &&
|
||||||
|
spa.ProjectId == 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();
|
||||||
|
return jobStatusMapping;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user