Added an API to update comments

This commit is contained in:
ashutosh.nehete 2025-11-15 12:43:36 +05:30
parent 2806dceab2
commit 2c8486f0de
4 changed files with 352 additions and 42 deletions

View File

@ -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; }

View File

@ -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);

View File

@ -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 ===================================================================

View File

@ -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>