diff --git a/Marco.Pms.Model/Dtos/ServiceProject/JobCommentDto.cs b/Marco.Pms.Model/Dtos/ServiceProject/JobCommentDto.cs new file mode 100644 index 0000000..0e738f0 --- /dev/null +++ b/Marco.Pms.Model/Dtos/ServiceProject/JobCommentDto.cs @@ -0,0 +1,8 @@ +namespace Marco.Pms.Model.Dtos.ServiceProject +{ + public class JobCommentDto + { + public required Guid JobTicketId { get; set; } + public required string Comment { get; set; } + } +} diff --git a/Marco.Pms.Services/Controllers/ServiceProjectController.cs b/Marco.Pms.Services/Controllers/ServiceProjectController.cs index 0024f0c..1935b32 100644 --- a/Marco.Pms.Services/Controllers/ServiceProjectController.cs +++ b/Marco.Pms.Services/Controllers/ServiceProjectController.cs @@ -123,6 +123,19 @@ namespace Marco.Pms.Services.Controllers } return StatusCode(response.StatusCode, response); } + + [HttpPost("job/add/comment")] + public async Task AddCommentToJobTicket(JobCommentDto model) + { + Employee loggedInEmploee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _serviceProject.AddCommentToJobTicketAsync(model, loggedInEmploee, tenantId); + if (response.Success) + { + var notification = new { LoggedInUserId = loggedInEmploee.Id, Keyword = "Job_Ticket_Comment", Response = response.Data }; + await _signalR.SendNotificationAsync(notification); + } + return StatusCode(response.StatusCode, response); + } #endregion } } diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index b667067..8b45d37 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -207,6 +207,8 @@ namespace Marco.Pms.Services.MappingProfiles dest => dest.StatusName, opt => opt.MapFrom(src => src.Status != null ? src.Status.Name : null)); + CreateMap(); + #endregion #endregion diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IServiceProject.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IServiceProject.cs index 122811f..7ebb8f6 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IServiceProject.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IServiceProject.cs @@ -19,6 +19,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> GetJobTicketsListAsync(Guid? projectId, int pageNumber, int pageSize, bool isActive, Employee loggedInEmployee, Guid tenantId); Task> GetJobTicketDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId); Task> CreateJobTicketAsync(CreateJobTicketDto model, Employee loggedInEmployee, Guid tenantId); + Task> AddCommentToJobTicketAsync(JobCommentDto model, Employee loggedInEmployee, Guid tenantId); #endregion } diff --git a/Marco.Pms.Services/Service/ServiceProjectService.cs b/Marco.Pms.Services/Service/ServiceProjectService.cs index 8f4178b..c7f50ba 100644 --- a/Marco.Pms.Services/Service/ServiceProjectService.cs +++ b/Marco.Pms.Services/Service/ServiceProjectService.cs @@ -618,8 +618,6 @@ namespace Marco.Pms.Services.Service } } - - /// /// Creates a new job ticket with optional assignees and tags within a transactional scope. /// @@ -798,6 +796,74 @@ namespace Marco.Pms.Services.Service return ApiResponse.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500); } } + /// + /// Adds a new comment to an existing job ticket within the tenant context. + /// + /// Comment data transfer object containing the job ticket ID and comment text. + /// Employee adding the comment (for auditing). + /// Tenant identifier to enforce multi-tenancy. + /// ApiResponse with the created comment view or error details. + public async Task> AddCommentToJobTicketAsync(JobCommentDto model, Employee loggedInEmployee, Guid tenantId) + { + if (tenantId == Guid.Empty) + { + _logger.LogWarning("Add comment attempt with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Access Denied", "Invalid tenant context.", 403); + } + + if (model == null || model.JobTicketId == Guid.Empty || string.IsNullOrWhiteSpace(model.Comment)) + { + _logger.LogInfo("Invalid comment model provided by employee {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Bad Request", "Comment data is incomplete or invalid.", 400); + } + + try + { + _logger.LogInfo("Attempting to add comment to job ticket {JobTicketId} by employee {EmployeeId}", model.JobTicketId, loggedInEmployee.Id); + + // Verify if the job ticket exists within tenant and load status for response mapping + var jobTicket = await _context.JobTickets + .Include(jt => jt.Status) + .AsNoTracking() + .FirstOrDefaultAsync(jt => jt.Id == model.JobTicketId && jt.TenantId == tenantId); + + if (jobTicket == null) + { + _logger.LogWarning("Job ticket {JobTicketId} not found or inaccessible in tenant {TenantId}", model.JobTicketId, tenantId); + return ApiResponse.ErrorResponse("Job Not Found", "Job ticket not found or inaccessible.", 404); + } + + // Create and save new comment entity + var comment = new JobComment + { + Id = Guid.NewGuid(), + JobTicketId = jobTicket.Id, + Comment = model.Comment.Trim(), + CreatedAt = DateTime.UtcNow, + CreatedById = loggedInEmployee.Id, + TenantId = tenantId + }; + + _context.JobComments.Add(comment); + await _context.SaveChangesAsync(); + + // Map response including basic job ticket info + var response = _mapper.Map(comment); + response.JobTicket = _mapper.Map(jobTicket); + + _logger.LogInfo("Successfully added comment {CommentId} to job ticket {JobTicketId} by employee {EmployeeId}", + comment.Id, jobTicket.Id, loggedInEmployee.Id); + + return ApiResponse.SuccessResponse(response, "Comment added to job ticket successfully.", 201); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error adding comment to job ticket {JobTicketId} by employee {EmployeeId} in tenant {TenantId}", + model.JobTicketId, loggedInEmployee.Id, tenantId); + return ApiResponse.ErrorResponse("Internal Server Error", "Failed to add comment. Please try again later.", 500); + } + } + #endregion }