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.Master;
|
||||
|
||||
namespace Marco.Pms.Model.ViewModels.ServiceProject
|
||||
namespace Marco.Pms.Model.Dtos.ServiceProject
|
||||
{
|
||||
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.ViewModels.ServiceProject;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Marco.Pms.Services.Service.ServiceInterfaces;
|
||||
using MarcoBMS.Services.Helpers;
|
||||
using MarcoBMS.Services.Service;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.JsonPatch;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Marco.Pms.Services.Controllers
|
||||
@ -15,15 +16,18 @@ namespace Marco.Pms.Services.Controllers
|
||||
public class ServiceProjectController : Controller
|
||||
{
|
||||
private readonly IServiceProject _serviceProject;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly UserHelper _userHelper;
|
||||
private readonly ILoggingService _logger;
|
||||
private readonly ISignalRService _signalR;
|
||||
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;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_userHelper = userHelper;
|
||||
_logger = logger;
|
||||
_signalR = signalR;
|
||||
tenantId = userHelper.GetTenantId();
|
||||
|
||||
@ -156,6 +160,51 @@ namespace Marco.Pms.Services.Controllers
|
||||
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")]
|
||||
public async Task<IActionResult> AddCommentToJobTicket(JobCommentDto model)
|
||||
{
|
||||
|
||||
@ -198,8 +198,11 @@ namespace Marco.Pms.Services.MappingProfiles
|
||||
CreateMap<ServiceProject, ServiceProjectVM>();
|
||||
CreateMap<ServiceProject, BasicServiceProjectVM>();
|
||||
CreateMap<ServiceProject, ServiceProjectDetailsVM>();
|
||||
|
||||
#region ======================================================= Job Ticket =======================================================
|
||||
CreateMap<CreateJobTicketDto, JobTicket>();
|
||||
CreateMap<UpdateJobTicketDto, JobTicket>();
|
||||
CreateMap<JobTicket, UpdateJobTicketDto>();
|
||||
CreateMap<JobTicket, JobTicketVM>();
|
||||
CreateMap<JobTicket, JobTicketDetailsVM>();
|
||||
CreateMap<JobTicket, BasicJobTicketVM>()
|
||||
@ -210,6 +213,7 @@ namespace Marco.Pms.Services.MappingProfiles
|
||||
CreateMap<JobComment, JobCommentVM>();
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
#region ======================================================= Employee =======================================================
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
<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.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.SignalR" Version="1.2.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.12">
|
||||
|
||||
@ -72,7 +72,7 @@ builder.Services.AddCors(options =>
|
||||
#endregion
|
||||
|
||||
#region Core Web & Framework Services
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddControllers().AddNewtonsoftJson();
|
||||
builder.Services.AddSignalR();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
using Marco.Pms.Model.Dtos.ServiceProject;
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.ServiceProject;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Marco.Pms.Model.ViewModels.ServiceProject;
|
||||
|
||||
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>> CreateJobTicketAsync(CreateJobTicketDto 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);
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Pubic Helper Functions ===================================================================
|
||||
Task<JobTicket?> GetJobTicketByIdAsync(Guid id, Guid tenantId);
|
||||
#endregion
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
using AutoMapper;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Helpers.Utility;
|
||||
using Marco.Pms.Model.DocumentManager;
|
||||
using Marco.Pms.Model.Dtos.ServiceProject;
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Master;
|
||||
using Marco.Pms.Model.MongoDBModels.Utility;
|
||||
using Marco.Pms.Model.ServiceProject;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Marco.Pms.Model.ViewModels.Activities;
|
||||
@ -14,6 +16,7 @@ using Marco.Pms.Model.ViewModels.ServiceProject;
|
||||
using Marco.Pms.Services.Service.ServiceInterfaces;
|
||||
using MarcoBMS.Services.Service;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MongoDB.Bson;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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();
|
||||
var jobStatusMapping = await GetJobStatusMappingAsync(jobTicket.StatusId, model.StatusId, jobTicket.ProjectId, loggedInEmployee.Id, tenantId);
|
||||
|
||||
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
|
||||
#region =================================================================== Job Comments Functions ===================================================================
|
||||
|
||||
@ -1195,5 +1416,83 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
|
||||
#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