Added an API to update comments
This commit is contained in:
parent
2806dceab2
commit
2c8486f0de
@ -4,6 +4,7 @@ namespace Marco.Pms.Model.Dtos.ServiceProject
|
||||
{
|
||||
public class JobCommentDto
|
||||
{
|
||||
public Guid? Id { get; set; }
|
||||
public required Guid JobTicketId { get; set; }
|
||||
public required string Comment { get; set; }
|
||||
public List<FileUploadModel>? Attachments { get; set; }
|
||||
|
||||
@ -38,8 +38,8 @@ namespace Marco.Pms.Services.Controllers
|
||||
[HttpGet("list")]
|
||||
public async Task<IActionResult> GetServiceProjectList([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20)
|
||||
{
|
||||
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.GetServiceProjectListAsync(pageNumber, pageSize, loggedInEmploee, tenantId);
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.GetServiceProjectListAsync(pageNumber, pageSize, loggedInEmployee, tenantId);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
|
||||
}
|
||||
@ -47,8 +47,8 @@ namespace Marco.Pms.Services.Controllers
|
||||
[HttpGet("details/{id}")]
|
||||
public async Task<IActionResult> GetServiceProjectDetails(Guid id)
|
||||
{
|
||||
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.GetServiceProjectDetailsAsync(id, loggedInEmploee, tenantId);
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.GetServiceProjectDetailsAsync(id, loggedInEmployee, tenantId);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
|
||||
}
|
||||
@ -56,11 +56,11 @@ namespace Marco.Pms.Services.Controllers
|
||||
[HttpPost("create")]
|
||||
public async Task<IActionResult> CreateProject([FromBody] ServiceProjectDto serviceProject)
|
||||
{
|
||||
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.CreateServiceProjectAsync(serviceProject, loggedInEmploee, tenantId);
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.CreateServiceProjectAsync(serviceProject, loggedInEmployee, tenantId);
|
||||
if (response.Success)
|
||||
{
|
||||
var notification = new { LoggedInUserId = loggedInEmploee.Id, Keyword = "Service_Project", Response = response.Data };
|
||||
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Service_Project", Response = response.Data };
|
||||
await _signalR.SendNotificationAsync(notification);
|
||||
}
|
||||
return StatusCode(response.StatusCode, response);
|
||||
@ -71,11 +71,11 @@ namespace Marco.Pms.Services.Controllers
|
||||
[HttpPut("edit/{id}")]
|
||||
public async Task<IActionResult> UpdateServicecProject(Guid id, [FromBody] ServiceProjectDto serviceProject)
|
||||
{
|
||||
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.UpdateServiceProjectAsync(id, serviceProject, loggedInEmploee, tenantId);
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.UpdateServiceProjectAsync(id, serviceProject, loggedInEmployee, tenantId);
|
||||
if (response.Success)
|
||||
{
|
||||
var notification = new { LoggedInUserId = loggedInEmploee.Id, Keyword = "Service_Project", Response = response.Data };
|
||||
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Service_Project", Response = response.Data };
|
||||
await _signalR.SendNotificationAsync(notification);
|
||||
}
|
||||
return StatusCode(response.StatusCode, response);
|
||||
@ -85,11 +85,11 @@ namespace Marco.Pms.Services.Controllers
|
||||
[HttpDelete("delete/{id}")]
|
||||
public async Task<IActionResult> DeActivateServiceProject(Guid id, bool isActive = false)
|
||||
{
|
||||
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.DeActivateServiceProjectAsync(id, isActive, loggedInEmploee, tenantId);
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.DeActivateServiceProjectAsync(id, isActive, loggedInEmployee, tenantId);
|
||||
if (response.Success)
|
||||
{
|
||||
var notification = new { LoggedInUserId = loggedInEmploee.Id, Keyword = "Service_Project", Response = response.Data };
|
||||
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Service_Project", Response = response.Data };
|
||||
await _signalR.SendNotificationAsync(notification);
|
||||
}
|
||||
return StatusCode(response.StatusCode, response);
|
||||
@ -101,19 +101,19 @@ namespace Marco.Pms.Services.Controllers
|
||||
[HttpGet("get/allocation/list")]
|
||||
public async Task<IActionResult> GetServiceProjectAllocationList([FromQuery] Guid? projectId, [FromQuery] Guid? employeeId, [FromQuery] bool isActive = true)
|
||||
{
|
||||
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.GetServiceProjectAllocationListAsync(projectId, employeeId, true, loggedInEmploee, tenantId);
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.GetServiceProjectAllocationListAsync(projectId, employeeId, true, loggedInEmployee, 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);
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.ManageServiceProjectAllocationAsync(model, loggedInEmployee, tenantId);
|
||||
if (response.Success)
|
||||
{
|
||||
var notification = new { LoggedInUserId = loggedInEmploee.Id, Keyword = "Service_Project_Allocation", Response = response.Data };
|
||||
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Service_Project_Allocation", Response = response.Data };
|
||||
await _signalR.SendNotificationAsync(notification);
|
||||
}
|
||||
return StatusCode(response.StatusCode, response);
|
||||
@ -125,8 +125,8 @@ namespace Marco.Pms.Services.Controllers
|
||||
[HttpGet("job/list")]
|
||||
public async Task<IActionResult> GetJobTicketsList([FromQuery] Guid? projectId, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20, [FromQuery] bool isActive = true)
|
||||
{
|
||||
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.GetJobTicketsListAsync(projectId, pageNumber, pageSize, isActive, loggedInEmploee, tenantId);
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.GetJobTicketsListAsync(projectId, pageNumber, pageSize, isActive, loggedInEmployee, tenantId);
|
||||
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
@ -134,8 +134,8 @@ namespace Marco.Pms.Services.Controllers
|
||||
[HttpGet("job/details/{id}")]
|
||||
public async Task<IActionResult> GetJobTicketDetails(Guid id)
|
||||
{
|
||||
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.GetJobTicketDetailsAsync(id, loggedInEmploee, tenantId);
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.GetJobTicketDetailsAsync(id, loggedInEmployee, tenantId);
|
||||
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
@ -143,8 +143,8 @@ namespace Marco.Pms.Services.Controllers
|
||||
[HttpGet("job/tag/list")]
|
||||
public async Task<IActionResult> GetJobTagList()
|
||||
{
|
||||
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.GetJobTagListAsync(loggedInEmploee, tenantId);
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.GetJobTagListAsync(loggedInEmployee, tenantId);
|
||||
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
@ -152,11 +152,11 @@ namespace Marco.Pms.Services.Controllers
|
||||
[HttpPost("job/create")]
|
||||
public async Task<IActionResult> CreateJobTicket([FromBody] CreateJobTicketDto model)
|
||||
{
|
||||
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.CreateJobTicketAsync(model, loggedInEmploee, tenantId);
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.CreateJobTicketAsync(model, loggedInEmployee, tenantId);
|
||||
if (response.Success)
|
||||
{
|
||||
var notification = new { LoggedInUserId = loggedInEmploee.Id, Keyword = "Job_Ticket", Response = response.Data };
|
||||
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Job_Ticket", Response = response.Data };
|
||||
await _signalR.SendNotificationAsync(notification);
|
||||
}
|
||||
return StatusCode(response.StatusCode, response);
|
||||
@ -165,11 +165,11 @@ namespace Marco.Pms.Services.Controllers
|
||||
[HttpPost("job/status-change")]
|
||||
public async Task<IActionResult> ChangeJobsStatus([FromBody] ChangeJobStatusDto model)
|
||||
{
|
||||
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.ChangeJobsStatusAsync(model, loggedInEmploee, tenantId);
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.ChangeJobsStatusAsync(model, loggedInEmployee, tenantId);
|
||||
if (response.Success)
|
||||
{
|
||||
var notification = new { LoggedInUserId = loggedInEmploee.Id, Keyword = "Job_Ticket", Response = response.Data };
|
||||
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Job_Ticket", Response = response.Data };
|
||||
await _signalR.SendNotificationAsync(notification);
|
||||
}
|
||||
return StatusCode(response.StatusCode, response);
|
||||
@ -226,8 +226,8 @@ namespace Marco.Pms.Services.Controllers
|
||||
[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);
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.GetCommentListByJobTicketAsync(jobTicketId, pageNumber, pageSize, loggedInEmployee, tenantId);
|
||||
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
@ -235,11 +235,24 @@ namespace Marco.Pms.Services.Controllers
|
||||
[HttpPost("job/add/comment")]
|
||||
public async Task<IActionResult> AddCommentToJobTicket([FromBody] JobCommentDto model)
|
||||
{
|
||||
Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.AddCommentToJobTicketAsync(model, loggedInEmploee, tenantId);
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.AddCommentToJobTicketAsync(model, loggedInEmployee, tenantId);
|
||||
if (response.Success)
|
||||
{
|
||||
var notification = new { LoggedInUserId = loggedInEmploee.Id, Keyword = "Job_Ticket_Comment", Response = response.Data };
|
||||
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Job_Ticket_Comment", Response = response.Data };
|
||||
await _signalR.SendNotificationAsync(notification);
|
||||
}
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPut("job/edit/comment")]
|
||||
public async Task<IActionResult> UpdateComment(Guid id, [FromBody] JobCommentDto model)
|
||||
{
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _serviceProject.UpdateCommentAsync(id, model, loggedInEmployee, tenantId);
|
||||
if (response.Success)
|
||||
{
|
||||
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Job_Ticket_Comment", Response = response.Data };
|
||||
await _signalR.SendNotificationAsync(notification);
|
||||
}
|
||||
return StatusCode(response.StatusCode, response);
|
||||
|
||||
@ -33,6 +33,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
||||
#region =================================================================== Job Comments Functions ===================================================================
|
||||
Task<ApiResponse<object>> GetCommentListByJobTicketAsync(Guid? jobTicketId, int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> AddCommentToJobTicketAsync(JobCommentDto model, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> UpdateCommentAsync(Guid id, JobCommentDto model, Employee loggedInEmployee, Guid tenantId);
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Helper Functions ===================================================================
|
||||
|
||||
@ -377,6 +377,11 @@ namespace Marco.Pms.Services.Service
|
||||
return ApiResponse<object>.ErrorResponse("Project Not Found", $"No active project found with ID {id}.", 404);
|
||||
}
|
||||
|
||||
// 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(serviceProject);
|
||||
|
||||
// Map incoming DTO to the tracked entity
|
||||
_mapper.Map(model, serviceProject);
|
||||
serviceProject.UpdatedAt = DateTime.UtcNow;
|
||||
@ -442,7 +447,16 @@ namespace Marco.Pms.Services.Service
|
||||
.ToListAsync();
|
||||
});
|
||||
|
||||
await Task.WhenAll(serviceProjectTask, servicesTask);
|
||||
// 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");
|
||||
|
||||
await Task.WhenAll(serviceProjectTask, servicesTask, updateLogTask);
|
||||
|
||||
serviceProject = serviceProjectTask.Result;
|
||||
services = servicesTask.Result;
|
||||
@ -490,11 +504,25 @@ namespace Marco.Pms.Services.Service
|
||||
return ApiResponse<object>.ErrorResponse("Project Not Found", $"No project found with ID {id}.", 404);
|
||||
}
|
||||
|
||||
// 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(serviceProject);
|
||||
|
||||
// Update active status as requested by the client
|
||||
serviceProject.IsActive = isActive;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Push update log asynchronously to MongoDB for audit
|
||||
await updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
|
||||
{
|
||||
EntityId = id.ToString(),
|
||||
UpdatedById = loggedInEmployee.Id.ToString(),
|
||||
OldObject = existingEntityBson,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
}, "JobTicketModificationLog");
|
||||
|
||||
_logger.LogInfo("Service project {ProjectId} {(Action)}d successfully by employee {EmployeeId} in tenant {TenantId}",
|
||||
id, isActive ? "activate" : "deactivate", loggedInEmployee.Id, tenantId);
|
||||
|
||||
@ -1176,7 +1204,6 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
_logger.LogError(ex, "Unhandled exception while creating 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);
|
||||
@ -1285,6 +1312,9 @@ namespace Marco.Pms.Services.Service
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
||||
}
|
||||
|
||||
// Begin database transaction for atomicity
|
||||
await using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// Concurrently load referenced project and status entities to validate foreign keys
|
||||
@ -1313,9 +1343,6 @@ namespace Marco.Pms.Services.Service
|
||||
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)
|
||||
{
|
||||
@ -1501,6 +1528,7 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
catch (DbUpdateException dbEx)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
_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);
|
||||
@ -1706,8 +1734,8 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
// Determine content type and generate storage keys
|
||||
var fileType = _s3Service.GetContentTypeFromBase64(base64);
|
||||
var fileName = _s3Service.GenerateFileName(fileType, tenantId, "job");
|
||||
var objectKey = $"tenant-{tenantId}/ServiceProject/{jobTicket.ProjectId}/Job/{jobTicket.Id}/{fileName}";
|
||||
var fileName = _s3Service.GenerateFileName(fileType, tenantId, "job_comment");
|
||||
var objectKey = $"tenant-{tenantId}/ServiceProject/{jobTicket.ProjectId}/Job/{jobTicket.Id}/Comment/{comment.Id}/{fileName}";
|
||||
|
||||
// Upload file asynchronously to S3
|
||||
await _s3Service.UploadFileAsync(base64, fileType, objectKey);
|
||||
@ -1762,10 +1790,277 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a job comment, including its content and attachments, with audit and error logging.
|
||||
/// </summary>
|
||||
/// <param name="id">ID of the job comment to be updated.</param>
|
||||
/// <param name="model">DTO containing updated comment and attachment details.</param>
|
||||
/// <param name="loggedInEmployee">Employee performing the update (for audit/versioning).</param>
|
||||
/// <param name="tenantId">Tenant identifier for multi-tenant isolation.</param>
|
||||
/// <returns>ApiResponse containing updated comment details or error information.</returns>
|
||||
public async Task<ApiResponse<object>> UpdateCommentAsync(Guid id, JobCommentDto model, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
// Transaction ensures atomic update of comment and attachments.
|
||||
await using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
// Validate ID consistency and input presence
|
||||
if (!model.Id.HasValue || model.Id != id)
|
||||
{
|
||||
_logger.LogWarning("ID mismatch: route ({RouteId}) vs model ({ModelId}) by employee {EmployeeId}", id, model.Id ?? Guid.Empty, loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("ID mismatch between route and payload", "ID mismatch between route and payload", 400);
|
||||
}
|
||||
|
||||
// Concurrently fetch existing job comment and related active job ticket for validation
|
||||
var jobCommentTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.JobComments
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(jc => jc.Id == id && jc.JobTicketId == model.JobTicketId && jc.TenantId == tenantId);
|
||||
});
|
||||
var jobTicketTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.JobTickets
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(jc => jc.Id == model.JobTicketId && jc.TenantId == tenantId && jc.IsActive);
|
||||
});
|
||||
|
||||
await Task.WhenAll(jobCommentTask, jobTicketTask);
|
||||
var jobComment = jobCommentTask.Result;
|
||||
var jobTicket = jobTicketTask.Result;
|
||||
|
||||
if (jobTicket == null)
|
||||
{
|
||||
_logger.LogWarning("Job ticket {JobTicketId} not found for updating comment {CommentId}", model.JobTicketId, id);
|
||||
return ApiResponse<object>.ErrorResponse("Job not found", "Job not found", 404);
|
||||
}
|
||||
if (jobComment == null)
|
||||
{
|
||||
_logger.LogWarning("Job comment {CommentId} not found for update.", id);
|
||||
return ApiResponse<object>.ErrorResponse("Job Comment not found", "Job Comment not found", 404);
|
||||
}
|
||||
|
||||
// Audit: BSON snapshot before update (MongoDB)
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
|
||||
BsonDocument existingEntityBson = updateLogHelper.EntityToBsonDocument(jobComment);
|
||||
|
||||
// Update comment core fields and audit
|
||||
_mapper.Map(model, jobComment);
|
||||
jobComment.UpdatedAt = DateTime.UtcNow;
|
||||
jobComment.UpdatedById = loggedInEmployee.Id;
|
||||
_context.JobComments.Update(jobComment);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Attachment: Add new or remove deleted as specified in DTO
|
||||
if (model.Attachments?.Any() == true)
|
||||
{
|
||||
// New attachments
|
||||
var newBillAttachments = model.Attachments.Where(ba => ba.DocumentId == null && ba.IsActive).ToList();
|
||||
if (newBillAttachments.Any())
|
||||
{
|
||||
var batchId = Guid.NewGuid();
|
||||
var documents = new List<Document>();
|
||||
var attachments = new List<JobAttachment>();
|
||||
|
||||
foreach (var attachment in newBillAttachments)
|
||||
{
|
||||
string base64 = attachment.Base64Data?.Split(',').LastOrDefault() ?? "";
|
||||
if (string.IsNullOrWhiteSpace(base64))
|
||||
{
|
||||
_logger.LogWarning("Missing base64 data for new attachment in comment {CommentId}", id);
|
||||
return ApiResponse<object>.ErrorResponse("Bad Request", "Attachment image data is missing or invalid.", 400);
|
||||
}
|
||||
|
||||
// File upload and document creation
|
||||
var fileType = _s3Service.GetContentTypeFromBase64(base64);
|
||||
var fileName = _s3Service.GenerateFileName(fileType, tenantId, "job_comment");
|
||||
var objectKey = $"tenant-{tenantId}/ServiceProject/{jobTicket.ProjectId}/Job/{jobTicket.Id}/Comment/{jobComment.Id}/{fileName}";
|
||||
await _s3Service.UploadFileAsync(base64, fileType, objectKey);
|
||||
|
||||
var document = new Document
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
BatchId = batchId,
|
||||
FileName = attachment.FileName ?? fileName,
|
||||
ContentType = fileType,
|
||||
S3Key = objectKey,
|
||||
FileSize = attachment.FileSize,
|
||||
UploadedAt = DateTime.UtcNow,
|
||||
UploadedById = loggedInEmployee.Id,
|
||||
TenantId = tenantId
|
||||
};
|
||||
documents.Add(document);
|
||||
|
||||
attachments.Add(new JobAttachment
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
DocumentId = document.Id,
|
||||
StatusId = jobTicket.StatusId,
|
||||
JobCommentId = id,
|
||||
TenantId = tenantId
|
||||
});
|
||||
}
|
||||
_context.Documents.AddRange(documents);
|
||||
_context.JobAttachments.AddRange(attachments);
|
||||
|
||||
try
|
||||
{
|
||||
await _context.SaveChangesAsync();
|
||||
_logger.LogInfo("{Count} new attachments added to comment {CommentId} by employee {EmployeeId}", newBillAttachments.Count, id, loggedInEmployee.Id);
|
||||
}
|
||||
catch (DbUpdateException dbEx)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
_logger.LogError(dbEx, "Database error adding new attachments for comment {CommentId}", id);
|
||||
return ApiResponse<object>.ErrorResponse("Database Error", dbEx.Message, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Attachments for deletion
|
||||
var deleteBillAttachments = model.Attachments.Where(ba => ba.DocumentId != null && !ba.IsActive).ToList();
|
||||
if (deleteBillAttachments.Any())
|
||||
{
|
||||
var documentIds = deleteBillAttachments.Select(d => d.DocumentId!.Value).ToList();
|
||||
try
|
||||
{
|
||||
await DeleteAttachemnts(documentIds);
|
||||
_logger.LogInfo("{Count} attachments deleted for comment {CommentId} by employee {EmployeeId}", deleteBillAttachments.Count, id, loggedInEmployee.Id);
|
||||
}
|
||||
catch (DbUpdateException dbEx)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
_logger.LogError(dbEx, "Database error deleting attachments during comment update {CommentId}", id);
|
||||
return ApiResponse<object>.ErrorResponse("Database Error", dbEx.Message, 500);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
_logger.LogError(ex, "General error deleting attachments during comment update {CommentId}", id);
|
||||
return ApiResponse<object>.ErrorResponse("Attachment Deletion Error", ex.Message, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Push audit log to MongoDB
|
||||
var updateLogTask = updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
|
||||
{
|
||||
EntityId = id.ToString(),
|
||||
UpdatedById = loggedInEmployee.Id.ToString(),
|
||||
OldObject = existingEntityBson,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
}, "JobTicketModificationLog");
|
||||
|
||||
// Reload updated comment with related entities for comprehensive response
|
||||
var jobCommentTaskReload = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.JobComments
|
||||
.Include(jc => jc.JobTicket).ThenInclude(jt => jt!.Status)
|
||||
.Include(jc => jc.CreatedBy).ThenInclude(e => e!.JobRole)
|
||||
.Include(jc => jc.UpdatedBy).ThenInclude(e => e!.JobRole)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(jc => jc.Id == id && jc.JobTicketId == model.JobTicketId && jc.TenantId == tenantId);
|
||||
});
|
||||
|
||||
var documentReloadTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.JobAttachments
|
||||
.Include(ja => ja.Document)
|
||||
.AsNoTracking()
|
||||
.Where(ja => ja.JobCommentId == id && ja.Document != null && ja.TenantId == tenantId)
|
||||
.Select(ja => ja.Document!)
|
||||
.ToListAsync();
|
||||
});
|
||||
|
||||
await Task.WhenAll(updateLogTask, jobCommentTaskReload, documentReloadTask);
|
||||
|
||||
var updatedJobComment = jobCommentTaskReload.Result;
|
||||
var updatedDocuments = documentReloadTask.Result;
|
||||
|
||||
var response = _mapper.Map<JobCommentVM>(updatedJobComment);
|
||||
response.Attachments = updatedDocuments.Select(doc =>
|
||||
{
|
||||
var docVM = _mapper.Map<DocumentVM>(doc);
|
||||
docVM.PreSignedUrl = _s3Service.GeneratePreSignedUrl(doc.S3Key);
|
||||
docVM.ThumbPreSignedUrl = string.IsNullOrWhiteSpace(doc.ThumbS3Key)
|
||||
? _s3Service.GeneratePreSignedUrl(doc.S3Key)
|
||||
: _s3Service.GeneratePreSignedUrl(doc.ThumbS3Key);
|
||||
return docVM;
|
||||
}).ToList();
|
||||
|
||||
await transaction.CommitAsync();
|
||||
_logger.LogInfo("Comment {CommentId} updated successfully by employee {EmployeeId}", id, loggedInEmployee.Id);
|
||||
|
||||
return ApiResponse<object>.SuccessResponse(response, "Comment updated successfully", 200);
|
||||
}
|
||||
catch (DbUpdateException dbEx)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
_logger.LogError(dbEx, "Database error while updating comment {CommentId} by employee {EmployeeId} in tenant {TenantId}", id, loggedInEmployee.Id, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Database Error", "An error occurred while saving data to the database.", 500);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
_logger.LogError(ex, "Unhandled exception while updating comment {CommentId} by employee {EmployeeId} in tenant {TenantId}", id, loggedInEmployee.Id, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Helper Functions ===================================================================
|
||||
|
||||
private async Task DeleteAttachemnts(List<Guid> documentIds)
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var _updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
|
||||
|
||||
var attachmentTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
var attachments = await dbContext.JobAttachments.AsNoTracking().Where(ba => documentIds.Contains(ba.DocumentId)).ToListAsync();
|
||||
|
||||
dbContext.JobAttachments.RemoveRange(attachments);
|
||||
await dbContext.SaveChangesAsync();
|
||||
});
|
||||
var documentsTask = Task.Run(async () =>
|
||||
{
|
||||
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
var documents = await dbContext.Documents.AsNoTracking().Where(ba => documentIds.Contains(ba.Id)).ToListAsync();
|
||||
|
||||
if (documents.Any())
|
||||
{
|
||||
dbContext.Documents.RemoveRange(documents);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
List<S3DeletionObject> deletionObject = new List<S3DeletionObject>();
|
||||
foreach (var document in documents)
|
||||
{
|
||||
deletionObject.Add(new S3DeletionObject
|
||||
{
|
||||
Key = document.S3Key
|
||||
});
|
||||
if (!string.IsNullOrWhiteSpace(document.ThumbS3Key) && document.ThumbS3Key != document.S3Key)
|
||||
{
|
||||
deletionObject.Add(new S3DeletionObject
|
||||
{
|
||||
Key = document.ThumbS3Key
|
||||
});
|
||||
}
|
||||
}
|
||||
await _updateLogHelper.PushToS3DeletionAsync(deletionObject);
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(attachmentTask, documentsTask);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a job ticket by its unique identifier and associated tenant ID.
|
||||
/// </summary>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user