Added the Attachments in job comments APIs and removed list of comments from job tickets details API
This commit is contained in:
parent
611d7753bb
commit
2f04339b4d
@ -1,8 +1,11 @@
|
|||||||
namespace Marco.Pms.Model.Dtos.ServiceProject
|
using Marco.Pms.Model.Utilities;
|
||||||
|
|
||||||
|
namespace Marco.Pms.Model.Dtos.ServiceProject
|
||||||
{
|
{
|
||||||
public class JobCommentDto
|
public class JobCommentDto
|
||||||
{
|
{
|
||||||
public required Guid JobTicketId { get; set; }
|
public required Guid JobTicketId { get; set; }
|
||||||
public required string Comment { get; set; }
|
public required string Comment { get; set; }
|
||||||
|
public List<FileUploadModel>? Attachments { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,6 @@ namespace Marco.Pms.Model.ViewModels.ServiceProject
|
|||||||
public BasicEmployeeVM? CreatedBy { get; set; }
|
public BasicEmployeeVM? CreatedBy { get; set; }
|
||||||
public DateTime? UpdatedAt { get; set; }
|
public DateTime? UpdatedAt { get; set; }
|
||||||
public BasicEmployeeVM? UpdatedBy { get; set; }
|
public BasicEmployeeVM? UpdatedBy { get; set; }
|
||||||
public List<DocumentVM>? Documents { get; set; }
|
public List<DocumentVM>? Attachments { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,5 @@ namespace Marco.Pms.Model.ViewModels.ServiceProject
|
|||||||
public BasicEmployeeVM? CreatedBy { get; set; }
|
public BasicEmployeeVM? CreatedBy { get; set; }
|
||||||
public List<TagVM>? Tags { get; set; }
|
public List<TagVM>? Tags { get; set; }
|
||||||
public List<JobUpdateLogVM>? UpdateLogs { get; set; }
|
public List<JobUpdateLogVM>? UpdateLogs { get; set; }
|
||||||
public List<JobCommentVM>? Comments { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -499,6 +499,7 @@ namespace Marco.Pms.Services.MappingProfiles
|
|||||||
|
|
||||||
#region ======================================================= Document =======================================================
|
#region ======================================================= Document =======================================================
|
||||||
|
|
||||||
|
CreateMap<Document, DocumentVM>();
|
||||||
CreateMap<DocumentMongoDB, BasicDocumentVM>()
|
CreateMap<DocumentMongoDB, BasicDocumentVM>()
|
||||||
.ForMember(
|
.ForMember(
|
||||||
dest => dest.DocumentId,
|
dest => dest.DocumentId,
|
||||||
|
|||||||
@ -67,11 +67,7 @@ namespace Marco.Pms.Services.Service
|
|||||||
/// <param name="loggedInEmployee">Employee requesting the statuses.</param>
|
/// <param name="loggedInEmployee">Employee requesting the statuses.</param>
|
||||||
/// <param name="tenantId">Tenant identifier for multi-tenant scope.</param>
|
/// <param name="tenantId">Tenant identifier for multi-tenant scope.</param>
|
||||||
/// <returns>ApiResponse containing list of job statuses or error details.</returns>
|
/// <returns>ApiResponse containing list of job statuses or error details.</returns>
|
||||||
public async Task<ApiResponse<object>> GetJobStatusAsync(
|
public async Task<ApiResponse<object>> GetJobStatusAsync(Guid? statusId, Guid? projectId, Employee loggedInEmployee, Guid tenantId)
|
||||||
Guid? statusId,
|
|
||||||
Guid? projectId,
|
|
||||||
Employee loggedInEmployee,
|
|
||||||
Guid tenantId)
|
|
||||||
{
|
{
|
||||||
_logger.LogDebug("GetJobStatusAsync called by employee {EmployeeId} in tenant {TenantId}", loggedInEmployee.Id, tenantId);
|
_logger.LogDebug("GetJobStatusAsync called by employee {EmployeeId} in tenant {TenantId}", loggedInEmployee.Id, tenantId);
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Marco.Pms.DataAccess.Data;
|
using Marco.Pms.DataAccess.Data;
|
||||||
|
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.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;
|
||||||
|
using Marco.Pms.Model.ViewModels.DocumentManager;
|
||||||
using Marco.Pms.Model.ViewModels.Master;
|
using Marco.Pms.Model.ViewModels.Master;
|
||||||
using Marco.Pms.Model.ViewModels.Organization;
|
using Marco.Pms.Model.ViewModels.Organization;
|
||||||
using Marco.Pms.Model.ViewModels.ServiceProject;
|
using Marco.Pms.Model.ViewModels.ServiceProject;
|
||||||
using Marco.Pms.Services.Helpers;
|
|
||||||
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;
|
||||||
@ -23,7 +24,7 @@ namespace Marco.Pms.Services.Service
|
|||||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
private readonly ApplicationDbContext _context; // Keeping this for direct scoped context use where appropriate
|
private readonly ApplicationDbContext _context; // Keeping this for direct scoped context use where appropriate
|
||||||
private readonly ILoggingService _logger;
|
private readonly ILoggingService _logger;
|
||||||
private readonly CacheUpdateHelper _cache;
|
private readonly S3UploadService _s3Service;
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
private readonly Guid NewStatus = Guid.Parse("32d76a02-8f44-4aa0-9b66-c3716c45a918");
|
private readonly Guid NewStatus = Guid.Parse("32d76a02-8f44-4aa0-9b66-c3716c45a918");
|
||||||
@ -33,13 +34,13 @@ namespace Marco.Pms.Services.Service
|
|||||||
IServiceScopeFactory serviceScopeFactory,
|
IServiceScopeFactory serviceScopeFactory,
|
||||||
ApplicationDbContext context,
|
ApplicationDbContext context,
|
||||||
ILoggingService logger,
|
ILoggingService logger,
|
||||||
CacheUpdateHelper cache,
|
S3UploadService s3Service,
|
||||||
IMapper mapper)
|
IMapper mapper)
|
||||||
{
|
{
|
||||||
_serviceScopeFactory = serviceScopeFactory;
|
_serviceScopeFactory = serviceScopeFactory;
|
||||||
_context = context;
|
_context = context;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_cache = cache;
|
_s3Service = s3Service;
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
_dbContextFactory = dbContextFactory;
|
_dbContextFactory = dbContextFactory;
|
||||||
}
|
}
|
||||||
@ -560,15 +561,6 @@ namespace Marco.Pms.Services.Service
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
});
|
});
|
||||||
|
|
||||||
var commentTask = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
|
||||||
return await context.JobComments
|
|
||||||
.Include(jc => jc.CreatedBy).ThenInclude(e => e!.JobRole)
|
|
||||||
.Where(jc => jc.JobTicketId == id && jc.TenantId == tenantId)
|
|
||||||
.ToListAsync();
|
|
||||||
});
|
|
||||||
|
|
||||||
var updateLogTask = Task.Run(async () =>
|
var updateLogTask = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
@ -578,7 +570,7 @@ namespace Marco.Pms.Services.Service
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
});
|
});
|
||||||
|
|
||||||
await Task.WhenAll(assigneeTask, tagTask, commentTask, updateLogTask);
|
await Task.WhenAll(assigneeTask, tagTask, updateLogTask);
|
||||||
|
|
||||||
// Map update logs with status descriptions
|
// Map update logs with status descriptions
|
||||||
var jobUpdateLogVMs = updateLogTask.Result.Select(ul =>
|
var jobUpdateLogVMs = updateLogTask.Result.Select(ul =>
|
||||||
@ -595,13 +587,7 @@ namespace Marco.Pms.Services.Service
|
|||||||
};
|
};
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
// Map comments, assignees, and tags to their respective viewmodels
|
// Map assignees, and tags to their respective viewmodels
|
||||||
var commentVMs = _mapper.Map<List<JobCommentVM>>(commentTask.Result);
|
|
||||||
commentVMs = commentVMs.Select(vm =>
|
|
||||||
{
|
|
||||||
vm.JobTicket = _mapper.Map<BasicJobTicketVM>(jobTicket);
|
|
||||||
return vm;
|
|
||||||
}).ToList();
|
|
||||||
var assigneeVMs = _mapper.Map<List<BasicEmployeeVM>>(assigneeTask.Result);
|
var assigneeVMs = _mapper.Map<List<BasicEmployeeVM>>(assigneeTask.Result);
|
||||||
var tagVMs = _mapper.Map<List<TagVM>>(tagTask.Result);
|
var tagVMs = _mapper.Map<List<TagVM>>(tagTask.Result);
|
||||||
|
|
||||||
@ -610,7 +596,6 @@ namespace Marco.Pms.Services.Service
|
|||||||
response.Assignees = assigneeVMs;
|
response.Assignees = assigneeVMs;
|
||||||
response.Tags = tagVMs;
|
response.Tags = tagVMs;
|
||||||
response.UpdateLogs = jobUpdateLogVMs;
|
response.UpdateLogs = jobUpdateLogVMs;
|
||||||
response.Comments = commentVMs;
|
|
||||||
|
|
||||||
_logger.LogInfo("Job ticket details assembled successfully for JobTicketId: {JobTicketId}", id);
|
_logger.LogInfo("Job ticket details assembled successfully for JobTicketId: {JobTicketId}", id);
|
||||||
|
|
||||||
@ -623,91 +608,6 @@ namespace Marco.Pms.Services.Service
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves a paginated list of comments for a specified job ticket within a tenant context.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="jobTicketId">Optional job ticket identifier to filter comments.</param>
|
|
||||||
/// <param name="pageNumber">Page number (1-based index) for pagination.</param>
|
|
||||||
/// <param name="pageSize">Page size for pagination.</param>
|
|
||||||
/// <param name="loggedInEmployee">Employee making the request (for logging).</param>
|
|
||||||
/// <param name="tenantId">Tenant context to scope data.</param>
|
|
||||||
/// <returns>ApiResponse with paged comment view models or error details.</returns>
|
|
||||||
public async Task<ApiResponse<object>> GetCommentListByJobTicketAsync(Guid? jobTicketId, int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId)
|
|
||||||
{
|
|
||||||
if (tenantId == Guid.Empty)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("TenantId missing in comment list request by employee {EmployeeId}", loggedInEmployee.Id);
|
|
||||||
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pageNumber < 1 || pageSize < 1)
|
|
||||||
{
|
|
||||||
_logger.LogInfo("Invalid pagination parameters in comment list request. PageNumber: {PageNumber}, PageSize: {PageSize}", pageNumber, pageSize);
|
|
||||||
return ApiResponse<object>.ErrorResponse("Bad Request", "Page number and size must be greater than zero.", 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_logger.LogInfo("Fetching comment list for jobTicketId {JobTicketId} by employee {EmployeeId} in tenant {TenantId}",
|
|
||||||
jobTicketId ?? Guid.Empty, loggedInEmployee.Id, tenantId);
|
|
||||||
|
|
||||||
var commentQuery = _context.JobComments
|
|
||||||
.Include(jc => jc.JobTicket).ThenInclude(jt => jt!.Status)
|
|
||||||
.Include(jc => jc.CreatedBy).ThenInclude(e => e!.JobRole)
|
|
||||||
.Where(jc =>
|
|
||||||
jc.TenantId == tenantId &&
|
|
||||||
jc.JobTicket != null &&
|
|
||||||
jc.CreatedBy != null &&
|
|
||||||
jc.CreatedBy.JobRole != null);
|
|
||||||
|
|
||||||
// Validate and filter by job ticket if specified
|
|
||||||
if (jobTicketId.HasValue)
|
|
||||||
{
|
|
||||||
var jobTicketExists = await _context.JobTickets.AnyAsync(jt =>
|
|
||||||
jt.Id == jobTicketId && jt.TenantId == tenantId);
|
|
||||||
|
|
||||||
if (!jobTicketExists)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Job ticket {JobTicketId} not found in tenant {TenantId} for comment listing", jobTicketId, tenantId);
|
|
||||||
return ApiResponse<object>.ErrorResponse("Job not found", "Job ticket not found.", 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
commentQuery = commentQuery.Where(jc => jc.JobTicketId == jobTicketId.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
var totalEntities = await commentQuery.CountAsync();
|
|
||||||
var totalPages = (int)Math.Ceiling((double)totalEntities / pageSize);
|
|
||||||
|
|
||||||
var comments = await commentQuery
|
|
||||||
.OrderByDescending(jc => jc.CreatedAt)
|
|
||||||
.Skip((pageNumber - 1) * pageSize)
|
|
||||||
.Take(pageSize)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var commentVMs = _mapper.Map<List<JobCommentVM>>(comments);
|
|
||||||
|
|
||||||
var response = new
|
|
||||||
{
|
|
||||||
CurrentPage = pageNumber,
|
|
||||||
TotalPages = totalPages,
|
|
||||||
TotalEntities = totalEntities,
|
|
||||||
Data = commentVMs,
|
|
||||||
};
|
|
||||||
|
|
||||||
_logger.LogInfo("{Count} comments fetched successfully for jobTicketId {JobTicketId} by employee {EmployeeId}",
|
|
||||||
commentVMs.Count, jobTicketId ?? Guid.Empty, loggedInEmployee.Id);
|
|
||||||
|
|
||||||
return ApiResponse<object>.SuccessResponse(response, $"{commentVMs.Count} record(s) fetched successfully.", 200);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error fetching comments for jobTicketId {JobTicketId} by employee {EmployeeId} in tenant {TenantId}",
|
|
||||||
jobTicketId ?? Guid.Empty, loggedInEmployee.Id, tenantId);
|
|
||||||
|
|
||||||
return ApiResponse<object>.ErrorResponse("Internal Server Error", "Failed to fetch comments. Please try again later.", 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves all job tags associated with the tenant, ordered alphabetically by name.
|
/// Retrieves all job tags associated with the tenant, ordered alphabetically by name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -928,21 +828,145 @@ namespace Marco.Pms.Services.Service
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
#region =================================================================== Job Comments Functions ===================================================================
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a new comment to an existing job ticket within the tenant context.
|
/// Retrieves a paginated list of comments with attachments for a specified job ticket within a tenant context.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="model">Comment data transfer object containing the job ticket ID and comment text.</param>
|
/// <param name="jobTicketId">Optional job ticket ID to filter comments.</param>
|
||||||
/// <param name="loggedInEmployee">Employee adding the comment (for auditing).</param>
|
/// <param name="pageNumber">Page number (1-based) for pagination.</param>
|
||||||
/// <param name="tenantId">Tenant identifier to enforce multi-tenancy.</param>
|
/// <param name="pageSize">Page size for pagination.</param>
|
||||||
/// <returns>ApiResponse with the created comment view or error details.</returns>
|
/// <param name="loggedInEmployee">Employee making the request (for authorization and logging).</param>
|
||||||
|
/// <param name="tenantId">Tenant context ID for multi-tenancy.</param>
|
||||||
|
/// <returns>ApiResponse with paged comments, attachments, and metadata, or error details.</returns>
|
||||||
|
public async Task<ApiResponse<object>> GetCommentListByJobTicketAsync(Guid? jobTicketId, int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId)
|
||||||
|
{
|
||||||
|
if (tenantId == Guid.Empty)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("TenantId missing in comment list request by employee {EmployeeId}", loggedInEmployee.Id);
|
||||||
|
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageNumber < 1 || pageSize < 1)
|
||||||
|
{
|
||||||
|
_logger.LogInfo("Invalid pagination parameters in comment list request. PageNumber: {PageNumber}, PageSize: {PageSize}", pageNumber, pageSize);
|
||||||
|
return ApiResponse<object>.ErrorResponse("Bad Request", "Page number and size must be greater than zero.", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInfo("Fetching comment list for jobTicketId {JobTicketId} by employee {EmployeeId} in tenant {TenantId}",
|
||||||
|
jobTicketId ?? Guid.Empty, loggedInEmployee.Id, tenantId);
|
||||||
|
|
||||||
|
var commentQuery = _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)
|
||||||
|
.Where(jc => jc.TenantId == tenantId && jc.JobTicket != null && jc.CreatedBy != null && jc.CreatedBy.JobRole != null);
|
||||||
|
|
||||||
|
// Filter by jobTicketId if provided after verifying existence
|
||||||
|
if (jobTicketId.HasValue)
|
||||||
|
{
|
||||||
|
var jobTicketExists = await _context.JobTickets.AnyAsync(jt =>
|
||||||
|
jt.Id == jobTicketId && jt.TenantId == tenantId);
|
||||||
|
|
||||||
|
if (!jobTicketExists)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Job ticket {JobTicketId} not found in tenant {TenantId} for comment listing", jobTicketId, tenantId);
|
||||||
|
return ApiResponse<object>.ErrorResponse("Job not found", "Job ticket not found.", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
commentQuery = commentQuery.Where(jc => jc.JobTicketId == jobTicketId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate total count for pagination
|
||||||
|
var totalEntities = await commentQuery.CountAsync();
|
||||||
|
var totalPages = (int)Math.Ceiling((double)totalEntities / pageSize);
|
||||||
|
|
||||||
|
// Fetch paged comments ordered by creation date descending
|
||||||
|
var comments = await commentQuery
|
||||||
|
.OrderByDescending(jc => jc.CreatedAt)
|
||||||
|
.Skip((pageNumber - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var commentIds = comments.Select(jc => jc.Id).ToList();
|
||||||
|
|
||||||
|
// Fetch attachments for current page comments
|
||||||
|
var attachments = await _context.JobAttachments
|
||||||
|
.Include(ja => ja.Document)
|
||||||
|
.Where(ja => ja.JobCommentId.HasValue &&
|
||||||
|
ja.Document != null &&
|
||||||
|
commentIds.Contains(ja.JobCommentId.Value) &&
|
||||||
|
ja.TenantId == tenantId)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// Map comments and attach corresponding documents with pre-signed URLs for access
|
||||||
|
var commentVMs = comments.Select(jc =>
|
||||||
|
{
|
||||||
|
var relatedDocuments = attachments
|
||||||
|
.Where(ja => ja.JobCommentId == jc.Id)
|
||||||
|
.Select(ja => ja.Document!)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var mappedComment = _mapper.Map<JobCommentVM>(jc);
|
||||||
|
|
||||||
|
if (relatedDocuments.Any())
|
||||||
|
{
|
||||||
|
mappedComment.Attachments = relatedDocuments.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();
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappedComment;
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
CurrentPage = pageNumber,
|
||||||
|
TotalPages = totalPages,
|
||||||
|
TotalEntities = totalEntities,
|
||||||
|
Data = commentVMs
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogInfo("{Count} comments fetched successfully for jobTicketId {JobTicketId} by employee {EmployeeId}",
|
||||||
|
commentVMs.Count, jobTicketId ?? Guid.Empty, loggedInEmployee.Id);
|
||||||
|
|
||||||
|
return ApiResponse<object>.SuccessResponse(response, $"{commentVMs.Count} record(s) fetched successfully.", 200);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error fetching comments for jobTicketId {JobTicketId} by employee {EmployeeId} in tenant {TenantId}",
|
||||||
|
jobTicketId ?? Guid.Empty, loggedInEmployee.Id, tenantId);
|
||||||
|
|
||||||
|
return ApiResponse<object>.ErrorResponse("Internal Server Error", "Failed to fetch comments. Please try again later.", 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a comment with optional attachments to a job ticket within the specified tenant context.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="model">DTO containing comment content, job ticket ID, and attachments.</param>
|
||||||
|
/// <param name="loggedInEmployee">Employee making the comment (for auditing).</param>
|
||||||
|
/// <param name="tenantId">Tenant context for data isolation.</param>
|
||||||
|
/// <returns>ApiResponse containing created comment details or relevant error information.</returns>
|
||||||
public async Task<ApiResponse<object>> AddCommentToJobTicketAsync(JobCommentDto model, Employee loggedInEmployee, Guid tenantId)
|
public async Task<ApiResponse<object>> AddCommentToJobTicketAsync(JobCommentDto model, Employee loggedInEmployee, Guid tenantId)
|
||||||
{
|
{
|
||||||
|
// Validate tenant context
|
||||||
if (tenantId == Guid.Empty)
|
if (tenantId == Guid.Empty)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Add comment attempt with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id);
|
_logger.LogWarning("Add comment attempt with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id);
|
||||||
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate input DTO
|
||||||
if (model == null || model.JobTicketId == Guid.Empty || string.IsNullOrWhiteSpace(model.Comment))
|
if (model == null || model.JobTicketId == Guid.Empty || string.IsNullOrWhiteSpace(model.Comment))
|
||||||
{
|
{
|
||||||
_logger.LogInfo("Invalid comment model provided by employee {EmployeeId}", loggedInEmployee.Id);
|
_logger.LogInfo("Invalid comment model provided by employee {EmployeeId}", loggedInEmployee.Id);
|
||||||
@ -953,7 +977,7 @@ namespace Marco.Pms.Services.Service
|
|||||||
{
|
{
|
||||||
_logger.LogInfo("Attempting to add comment to job ticket {JobTicketId} by employee {EmployeeId}", model.JobTicketId, loggedInEmployee.Id);
|
_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
|
// Verify the job ticket's existence and load minimal required info
|
||||||
var jobTicket = await _context.JobTickets
|
var jobTicket = await _context.JobTickets
|
||||||
.Include(jt => jt.Status)
|
.Include(jt => jt.Status)
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
@ -965,22 +989,78 @@ namespace Marco.Pms.Services.Service
|
|||||||
return ApiResponse<object>.ErrorResponse("Job Not Found", "Job ticket not found or inaccessible.", 404);
|
return ApiResponse<object>.ErrorResponse("Job Not Found", "Job ticket not found or inaccessible.", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and save new comment entity
|
// Create new comment entity
|
||||||
var comment = new JobComment
|
var comment = new JobComment
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
JobTicketId = jobTicket.Id,
|
JobTicketId = jobTicket.Id,
|
||||||
Comment = model.Comment.Trim(),
|
Comment = model.Comment.Trim(),
|
||||||
|
IsActive = true,
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
CreatedById = loggedInEmployee.Id,
|
CreatedById = loggedInEmployee.Id,
|
||||||
TenantId = tenantId
|
TenantId = tenantId
|
||||||
};
|
};
|
||||||
|
|
||||||
_context.JobComments.Add(comment);
|
_context.JobComments.Add(comment);
|
||||||
|
|
||||||
|
// Handle attachments if provided
|
||||||
|
if (model.Attachments?.Any() ?? false)
|
||||||
|
{
|
||||||
|
var batchId = Guid.NewGuid();
|
||||||
|
var documents = new List<Document>();
|
||||||
|
var attachments = new List<JobAttachment>();
|
||||||
|
|
||||||
|
foreach (var attachment in model.Attachments)
|
||||||
|
{
|
||||||
|
string base64 = attachment.Base64Data?.Split(',').LastOrDefault() ?? "";
|
||||||
|
if (string.IsNullOrWhiteSpace(base64))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Attachment missing base64 data in comment for job ticket {JobTicketId} by employee {EmployeeId}", jobTicket.Id, loggedInEmployee.Id);
|
||||||
|
return ApiResponse<object>.ErrorResponse("Bad Request", "Attachment image data is missing or invalid.", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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}";
|
||||||
|
|
||||||
|
// Upload file asynchronously to S3
|
||||||
|
await _s3Service.UploadFileAsync(base64, fileType, objectKey);
|
||||||
|
|
||||||
|
// Create document record for uploaded file
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Link document as attachment to the comment
|
||||||
|
attachments.Add(new JobAttachment
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
DocumentId = document.Id,
|
||||||
|
StatusId = jobTicket.StatusId,
|
||||||
|
JobCommentId = comment.Id,
|
||||||
|
TenantId = tenantId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_context.Documents.AddRange(documents);
|
||||||
|
_context.JobAttachments.AddRange(attachments);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist all inserts in database
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
// Map response including basic job ticket info
|
// Prepare response with mapped comment, creator info, and basic job ticket info
|
||||||
var response = _mapper.Map<JobCommentVM>(comment);
|
var response = _mapper.Map<JobCommentVM>(comment);
|
||||||
|
response.CreatedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
|
||||||
response.JobTicket = _mapper.Map<BasicJobTicketVM>(jobTicket);
|
response.JobTicket = _mapper.Map<BasicJobTicketVM>(jobTicket);
|
||||||
|
|
||||||
_logger.LogInfo("Successfully added comment {CommentId} to job ticket {JobTicketId} by employee {EmployeeId}",
|
_logger.LogInfo("Successfully added comment {CommentId} to job ticket {JobTicketId} by employee {EmployeeId}",
|
||||||
@ -996,7 +1076,6 @@ namespace Marco.Pms.Services.Service
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user