578 lines
28 KiB
C#
578 lines
28 KiB
C#
using Marco.Pms.DataAccess.Data;
|
||
using Marco.Pms.Model.Activities;
|
||
using Marco.Pms.Model.Dtos.DocumentManager;
|
||
using Marco.Pms.Model.Employees;
|
||
using Marco.Pms.Model.Filters;
|
||
using Marco.Pms.Model.Mapper;
|
||
using Marco.Pms.Model.Projects;
|
||
using Marco.Pms.Model.Utilities;
|
||
using Marco.Pms.Services.Service;
|
||
using MarcoBMS.Services.Helpers;
|
||
using MarcoBMS.Services.Service;
|
||
using Microsoft.AspNetCore.Authorization;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
using Microsoft.CodeAnalysis;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Serilog;
|
||
using System.Text.Json;
|
||
|
||
namespace Marco.Pms.Services.Controllers
|
||
{
|
||
[Route("api/[controller]")]
|
||
[ApiController]
|
||
[Authorize]
|
||
public class ImageController : ControllerBase
|
||
{
|
||
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
|
||
private readonly ApplicationDbContext _context;
|
||
private readonly S3UploadService _s3Service;
|
||
private readonly UserHelper _userHelper;
|
||
private readonly ILoggingService _logger;
|
||
private readonly PermissionServices _permission;
|
||
private readonly Guid tenantId;
|
||
public ImageController(IDbContextFactory<ApplicationDbContext> dbContextFactory,
|
||
ApplicationDbContext context,
|
||
S3UploadService s3Service,
|
||
UserHelper userHelper,
|
||
ILoggingService logger,
|
||
PermissionServices permission)
|
||
{
|
||
_dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory));
|
||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||
_s3Service = s3Service ?? throw new ArgumentNullException(nameof(s3Service));
|
||
_userHelper = userHelper ?? throw new ArgumentNullException(nameof(userHelper));
|
||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||
_permission = permission ?? throw new ArgumentNullException(nameof(permission));
|
||
tenantId = userHelper.GetTenantId();
|
||
}
|
||
|
||
[HttpGet("images/{projectId}")]
|
||
|
||
public async Task<IActionResult> GetImageList(Guid projectId, [FromQuery] string? filter, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10)
|
||
{
|
||
_logger.LogInfo("[GetImageList] Called by Employee for ProjectId: {ProjectId}", projectId);
|
||
|
||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||
|
||
// Step 1: Validate project existence
|
||
var isProjectExist = await _context.Projects.AnyAsync(p => p.Id == projectId && p.TenantId == tenantId);
|
||
if (!isProjectExist)
|
||
{
|
||
_logger.LogWarning("[GetImageList] ProjectId: {ProjectId} not found", projectId);
|
||
return BadRequest(ApiResponse<object>.ErrorResponse("Project not found", "Project not found in database", 400));
|
||
}
|
||
|
||
// Step 2: Check project access permission
|
||
var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId);
|
||
if (!hasPermission)
|
||
{
|
||
_logger.LogWarning("[GetImageList] Access denied for EmployeeId: {EmployeeId} on ProjectId: {ProjectId}", loggedInEmployee.Id, projectId);
|
||
return StatusCode(403, ApiResponse<object>.ErrorResponse("You don't have access", "You don't have access", 403));
|
||
}
|
||
|
||
// Step 3: Deserialize filter
|
||
ImageFilter? imageFilter = null;
|
||
if (!string.IsNullOrWhiteSpace(filter))
|
||
{
|
||
try
|
||
{
|
||
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||
//string unescapedJsonString = JsonSerializer.Deserialize<string>(filter, options) ?? "";
|
||
//imageFilter = JsonSerializer.Deserialize<ImageFilter>(unescapedJsonString, options);
|
||
imageFilter = JsonSerializer.Deserialize<ImageFilter>(filter, options);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogWarning("[GetImageList] Failed to parse filter: {Message}", ex.Message);
|
||
}
|
||
}
|
||
|
||
var taskQuery = _context.TaskAllocations
|
||
.Include(t => t.Employee)
|
||
.Include(t => t.ReportedBy)
|
||
.Include(t => t.ApprovedBy)
|
||
.Include(t => t.WorkStatus)
|
||
.Include(t => t.WorkItem)
|
||
.ThenInclude(wi => wi!.ActivityMaster)
|
||
.ThenInclude(a => a!.ActivityGroup)
|
||
.ThenInclude(ag => ag!.Service)
|
||
.Include(t => t.WorkItem)
|
||
.ThenInclude(wi => wi!.WorkArea)
|
||
.ThenInclude(wa => wa!.Floor)
|
||
.ThenInclude(f => f!.Building)
|
||
.Include(t => t.WorkItem)
|
||
.ThenInclude(wi => wi!.WorkCategoryMaster)
|
||
.Where(t => t.WorkItem != null &&
|
||
t.WorkItem.WorkArea != null &&
|
||
t.WorkItem.WorkArea.Floor != null &&
|
||
t.WorkItem.WorkArea.Floor.Building != null &&
|
||
t.WorkItem.WorkArea.Floor.Building.ProjectId != projectId &&
|
||
t.TenantId == tenantId);
|
||
|
||
// Step 4: Extract filter values
|
||
var buildingIds = imageFilter?.BuildingIds;
|
||
var floorIds = imageFilter?.FloorIds;
|
||
var workAreaIds = imageFilter?.WorkAreaIds;
|
||
var activityIds = imageFilter?.ActivityIds;
|
||
var workCategoryIds = imageFilter?.WorkCategoryIds;
|
||
var startDate = imageFilter?.StartDate;
|
||
var endDate = imageFilter?.EndDate;
|
||
var uploadedByIds = imageFilter?.UploadedByIds;
|
||
var serviceIds = imageFilter?.ServiceIds;
|
||
|
||
if (buildingIds?.Any() ?? false)
|
||
{
|
||
|
||
taskQuery = taskQuery
|
||
.Where(t => t.WorkItem != null &&
|
||
t.WorkItem.WorkArea != null &&
|
||
t.WorkItem.WorkArea.Floor != null &&
|
||
buildingIds.Contains(t.WorkItem.WorkArea.Floor.BuildingId));
|
||
}
|
||
|
||
if (floorIds?.Any() ?? false)
|
||
{
|
||
taskQuery = taskQuery
|
||
.Where(t => t.WorkItem != null &&
|
||
t.WorkItem.WorkArea != null &&
|
||
floorIds.Contains(t.WorkItem.WorkArea.FloorId));
|
||
}
|
||
|
||
if (workAreaIds?.Any() ?? false)
|
||
{
|
||
taskQuery = taskQuery
|
||
.Where(t => t.WorkItem != null &&
|
||
workAreaIds.Contains(t.WorkItem.WorkAreaId));
|
||
}
|
||
|
||
|
||
if (activityIds?.Any() == true)
|
||
{
|
||
taskQuery = taskQuery
|
||
.Where(t => t.WorkItem != null &&
|
||
activityIds.Contains(t.WorkItem.ActivityId));
|
||
}
|
||
|
||
if (workCategoryIds?.Any() == true)
|
||
{
|
||
taskQuery = taskQuery
|
||
.Where(t => t.WorkItem != null &&
|
||
t.WorkItem.WorkCategoryId.HasValue &&
|
||
workCategoryIds.Contains(t.WorkItem.WorkCategoryId.Value));
|
||
}
|
||
|
||
if (serviceIds?.Any() ?? false)
|
||
{
|
||
taskQuery = taskQuery.Where(t => t.WorkItem != null &&
|
||
t.WorkItem.ActivityMaster != null &&
|
||
t.WorkItem.ActivityMaster.ActivityGroup != null &&
|
||
serviceIds.Contains(t.WorkItem.ActivityMaster.ActivityGroup.ServiceId));
|
||
}
|
||
|
||
// Step 6: Fetch task allocations and comments
|
||
var tasks = await taskQuery.ToListAsync();
|
||
|
||
var taskIds = tasks.Select(t => t.Id).ToList();
|
||
|
||
var comments = await _context.TaskComments.Include(c => c.Employee)
|
||
.Where(c => taskIds.Contains(c.TaskAllocationId)).ToListAsync();
|
||
var commentIds = comments.Select(c => c.Id).ToList();
|
||
|
||
var attachments = await _context.TaskAttachments
|
||
.Where(ta => taskIds.Contains(ta.ReferenceId) || commentIds.Contains(ta.ReferenceId)).ToListAsync();
|
||
|
||
var documentIds = attachments.Select(ta => ta.DocumentId).ToList();
|
||
|
||
// Step 7: Fetch and filter documents
|
||
List<DocumentBatchDto> documents = new List<DocumentBatchDto>();
|
||
var docQuery = _context.Documents.Include(d => d.UploadedBy)
|
||
.Where(d => documentIds.Contains(d.Id) && d.TenantId == tenantId);
|
||
if (startDate != null && endDate != null)
|
||
{
|
||
docQuery = docQuery.Where(d => d.UploadedAt.Date >= startDate.Value.Date && d.UploadedAt.Date <= endDate.Value.Date);
|
||
}
|
||
|
||
int totalRecords = await docQuery.GroupBy(d => d.BatchId).CountAsync();
|
||
int totalPages = (int)Math.Ceiling((double)totalRecords / pageSize);
|
||
|
||
documents = await docQuery
|
||
.GroupBy(d => d.BatchId)
|
||
.OrderByDescending(g => g.Max(d => d.UploadedAt))
|
||
.Skip((pageNumber - 1) * pageSize)
|
||
.Take(pageSize)
|
||
.Select(g => new DocumentBatchDto
|
||
{
|
||
BatchId = g.Key,
|
||
Documents = g.ToList()
|
||
})
|
||
.ToListAsync();
|
||
|
||
|
||
// Step 8: Build response
|
||
var documentVM = documents.Select(d =>
|
||
{
|
||
var docIds = d.Documents?.Select(x => x.Id).ToList() ?? new List<Guid>();
|
||
var refId = attachments.FirstOrDefault(ta => docIds.Contains(ta.DocumentId))?.ReferenceId;
|
||
var task = tasks.FirstOrDefault(t => t.Id == refId);
|
||
var comment = comments.FirstOrDefault(c => c.Id == refId);
|
||
|
||
var source = task != null ? "Report" : comment != null ? "Comment" : "";
|
||
var uploadedBy = task?.ReportedBy ?? comment?.Employee;
|
||
|
||
if (comment != null)
|
||
{
|
||
task = tasks.FirstOrDefault(t => t.Id == comment.TaskAllocationId);
|
||
}
|
||
if (task != null)
|
||
{
|
||
comment = comments.OrderBy(c => c.CommentDate).FirstOrDefault(c => c.TaskAllocationId == task.Id);
|
||
}
|
||
|
||
var workItem = task!.WorkItem;
|
||
var workArea = task!.WorkItem!.WorkArea;
|
||
var floor = task!.WorkItem!.WorkArea!.Floor;
|
||
var building = task!.WorkItem!.WorkArea!.Floor!.Building;
|
||
|
||
return new
|
||
{
|
||
|
||
BatchId = d.BatchId,
|
||
Documents = d.Documents?.Select(x => new
|
||
{
|
||
Id = x.Id,
|
||
thumbnailUrl = x.ThumbS3Key != null ? _s3Service.GeneratePreSignedUrl(x.ThumbS3Key) : (x.S3Key != null ? _s3Service.GeneratePreSignedUrl(x.S3Key) : null),
|
||
Url = x.S3Key != null ? _s3Service.GeneratePreSignedUrl(x.S3Key) : null,
|
||
UploadedBy = x.UploadedBy?.ToBasicEmployeeVMFromEmployee() ?? uploadedBy?.ToBasicEmployeeVMFromEmployee(),
|
||
UploadedAt = x.UploadedAt,
|
||
}).ToList(),
|
||
Source = source,
|
||
ProjectId = projectId,
|
||
BuildingId = building?.Id,
|
||
BuildingName = building?.Name,
|
||
FloorIds = floor?.Id,
|
||
FloorName = floor?.FloorName,
|
||
WorkAreaId = workArea?.Id,
|
||
WorkAreaName = workArea?.AreaName,
|
||
TaskId = task?.Id,
|
||
ActivityId = workItem?.ActivityMaster?.Id,
|
||
ActivityName = workItem?.ActivityMaster?.ActivityName,
|
||
WorkCategoryId = workItem?.WorkCategoryMaster?.Id,
|
||
WorkCategoryName = workItem?.WorkCategoryMaster?.Name,
|
||
CommentId = comment?.Id,
|
||
Comment = comment?.Comment
|
||
};
|
||
}).ToList();
|
||
|
||
if (uploadedByIds?.Any() == true)
|
||
{
|
||
documentVM = documentVM.Where(d => d.Documents != null && d.Documents.Any(x => uploadedByIds.Contains(x.UploadedBy?.Id ?? Guid.Empty))).ToList();
|
||
}
|
||
|
||
var VM = new
|
||
{
|
||
TotalCount = totalRecords,
|
||
TotalPages = totalPages,
|
||
CurrentPage = pageNumber,
|
||
PageSize = pageSize,
|
||
Data = documentVM
|
||
};
|
||
|
||
_logger.LogInfo("[GetImageList] Fetched {Count} documents for ProjectId: {ProjectId}", documentVM.Count, projectId);
|
||
return Ok(ApiResponse<object>.SuccessResponse(VM, $"{documentVM.Count} image records fetched successfully", 200));
|
||
}
|
||
|
||
[HttpGet("batch/{batchId}")]
|
||
public async Task<IActionResult> GetImagesByBatch(Guid batchId)
|
||
{
|
||
_logger.LogInfo("GetImagesByBatch called for BatchId: {BatchId}", batchId);
|
||
|
||
// Step 1: Get the logged-in employee
|
||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||
|
||
// Step 2: Retrieve all documents in the batch
|
||
var documents = await _context.Documents
|
||
.Include(d => d.UploadedBy)
|
||
.Where(d => d.BatchId == batchId)
|
||
.ToListAsync();
|
||
|
||
if (!documents.Any())
|
||
{
|
||
_logger.LogWarning("No documents found for BatchId: {BatchId}", batchId);
|
||
return NotFound(ApiResponse<object>.ErrorResponse("No images found", "No images associated with this batch", 404));
|
||
}
|
||
|
||
var documentIds = documents.Select(d => d.Id).ToList();
|
||
|
||
// Step 3: Get task/comment reference IDs linked to these documents
|
||
var referenceIds = await _context.TaskAttachments
|
||
.Where(ta => documentIds.Contains(ta.DocumentId))
|
||
.Select(ta => ta.ReferenceId)
|
||
.Distinct()
|
||
.ToListAsync();
|
||
|
||
// Step 4: Try to identify the source of the attachment (task or comment)
|
||
var task = await _context.TaskAllocations
|
||
.Include(t => t.ReportedBy)
|
||
.FirstOrDefaultAsync(t => referenceIds.Contains(t.Id));
|
||
|
||
TaskComment? comment = null;
|
||
WorkItem? workItem = null;
|
||
Employee? uploadedBy = null;
|
||
string source = "";
|
||
|
||
if (task != null)
|
||
{
|
||
uploadedBy = task.ReportedBy;
|
||
workItem = await _context.WorkItems
|
||
.Include(wi => wi.ActivityMaster)
|
||
.Include(wi => wi.WorkCategoryMaster)
|
||
.FirstOrDefaultAsync(wi => wi.Id == task.WorkItemId);
|
||
source = "Report";
|
||
}
|
||
else
|
||
{
|
||
comment = await _context.TaskComments
|
||
.Include(tc => tc.TaskAllocation)
|
||
.Include(tc => tc.Employee)
|
||
.FirstOrDefaultAsync(tc => referenceIds.Contains(tc.Id));
|
||
var workItemId = comment?.TaskAllocation?.WorkItemId;
|
||
|
||
uploadedBy = comment?.Employee;
|
||
workItem = await _context.WorkItems
|
||
.Include(wi => wi.ActivityMaster)
|
||
.Include(wi => wi.WorkCategoryMaster)
|
||
.FirstOrDefaultAsync(wi => wi.Id == workItemId);
|
||
source = "Comment";
|
||
}
|
||
|
||
// Step 5: Traverse up to building level
|
||
var workAreaId = workItem?.WorkAreaId;
|
||
var workArea = await _context.WorkAreas
|
||
.Include(wa => wa.Floor)
|
||
.FirstOrDefaultAsync(wa => wa.Id == workAreaId);
|
||
|
||
var buildingId = workArea?.Floor?.BuildingId;
|
||
var building = await _context.Buildings
|
||
.FirstOrDefaultAsync(b => b.Id == buildingId);
|
||
|
||
// Step 6: Construct the response
|
||
var response = new
|
||
{
|
||
|
||
BatchId = batchId,
|
||
Documents = documents?.Select(x => new
|
||
{
|
||
Id = x.Id,
|
||
thumbnailUrl = x.ThumbS3Key != null ? _s3Service.GeneratePreSignedUrl(x.ThumbS3Key) : (x.S3Key != null ? _s3Service.GeneratePreSignedUrl(x.S3Key) : null),
|
||
Url = x.S3Key != null ? _s3Service.GeneratePreSignedUrl(x.S3Key) : null,
|
||
UploadedBy = x.UploadedBy?.ToBasicEmployeeVMFromEmployee() ?? uploadedBy?.ToBasicEmployeeVMFromEmployee(),
|
||
UploadedAt = x.UploadedAt,
|
||
}).ToList(),
|
||
Source = source,
|
||
ProjectId = building?.ProjectId,
|
||
BuildingId = building?.Id,
|
||
BuildingName = building?.Name,
|
||
FloorIds = workArea?.Floor?.Id,
|
||
FloorName = workArea?.Floor?.FloorName,
|
||
WorkAreaId = workArea?.Id,
|
||
WorkAreaName = workArea?.AreaName,
|
||
TaskId = task?.Id,
|
||
ActivityId = workItem?.ActivityMaster?.Id,
|
||
ActivityName = workItem?.ActivityMaster?.ActivityName,
|
||
WorkCategoryId = workItem?.WorkCategoryMaster?.Id,
|
||
WorkCategoryName = workItem?.WorkCategoryMaster?.Name,
|
||
CommentId = comment?.Id,
|
||
Comment = comment?.Comment
|
||
};
|
||
|
||
_logger.LogInfo("Fetched {Count} image(s) for BatchId: {BatchId}", response.Documents?.Count ?? 0, batchId);
|
||
|
||
return Ok(ApiResponse<object>.SuccessResponse(response, "Images for provided batchId fetched successfully", 200));
|
||
}
|
||
|
||
[HttpGet("filter/{projectId}")]
|
||
public async Task<IActionResult> GetFilterObjectAsync(Guid projectId, CancellationToken ct)
|
||
{
|
||
_logger.LogInfo("GetFilterObject started for ProjectId {ProjectId}", projectId); // start log [memory:1]
|
||
|
||
// Validate input early
|
||
if (projectId == Guid.Empty)
|
||
{
|
||
Log.Warning("GetFilterObject received empty ProjectId"); // input validation [memory:1]
|
||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid project id", 400));
|
||
}
|
||
|
||
// Load only what is needed for attachments; project scoping will be enforced downstream
|
||
// NOTE: Select only the columns required to reduce payload
|
||
var taskAttachments = await _context.TaskAttachments
|
||
.AsNoTracking()
|
||
.Select(ta => new { ta.ReferenceId, ta.DocumentId })
|
||
.ToListAsync(ct); // I/O bound work [memory:10]
|
||
|
||
if (taskAttachments.Count == 0)
|
||
{
|
||
Log.Information("No task attachments found for ProjectId {ProjectId}", projectId); // early exit [memory:1]
|
||
var emptyResponse = new
|
||
{
|
||
Buildings = Array.Empty<object>(),
|
||
Floors = Array.Empty<object>(),
|
||
WorkAreas = Array.Empty<object>(),
|
||
WorkCategories = Array.Empty<object>(),
|
||
Activities = Array.Empty<object>(),
|
||
UploadedBys = Array.Empty<object>(),
|
||
Services = Array.Empty<object>()
|
||
};
|
||
return Ok(ApiResponse<object>.SuccessResponse(emptyResponse, "No data found", 200));
|
||
}
|
||
|
||
// HashSets for O(1) membership tests and to dedupe upfront
|
||
var referenceIds = new HashSet<Guid>(taskAttachments.Select(x => x.ReferenceId));
|
||
var documentIds = new HashSet<Guid>(taskAttachments.Select(x => x.DocumentId));
|
||
|
||
_logger.LogDebug("Collected {ReferenceCount} referenceIds and {DocumentCount} documentIds", referenceIds.Count, documentIds.Count); // metrics [memory:1]
|
||
|
||
// Load comments for the references under this tenant
|
||
var comments = await _context.TaskComments
|
||
.AsNoTracking()
|
||
.Where(tc => referenceIds.Contains(tc.Id) && tc.TenantId == tenantId)
|
||
.Select(tc => new { tc.Id, tc.TaskAllocationId })
|
||
.ToListAsync(ct); // filtered selection [memory:10]
|
||
|
||
var taskIds = new HashSet<Guid>(comments.Select(c => c.TaskAllocationId));
|
||
_logger.LogDebug("Resolved {CommentCount} comments mapping to {TaskIdCount} taskIds", comments.Count, taskIds.Count); // observation [memory:1]
|
||
|
||
// IMPORTANT: Correct project filter should be == not !=
|
||
// Include graph tailored to fields needed in final projection (avoid over-inclusion)
|
||
// Avoid Task.Run for async EF (it doesn’t add value and can harm thread pool)
|
||
var tasks = await _context.TaskAllocations
|
||
.AsNoTracking()
|
||
.Where(t =>
|
||
t.WorkItem != null &&
|
||
t.WorkItem.WorkArea != null &&
|
||
t.WorkItem.WorkArea.Floor != null &&
|
||
t.WorkItem.WorkArea.Floor.Building != null &&
|
||
t.WorkItem.WorkArea.Floor.Building.ProjectId == projectId && // fixed filter
|
||
(taskIds.Contains(t.Id) || referenceIds.Contains(t.Id)))
|
||
.Include(t => t.WorkItem!)
|
||
.ThenInclude(wi => wi.ActivityMaster!)
|
||
.ThenInclude(a => a.ActivityGroup!)
|
||
.ThenInclude(ag => ag.Service)
|
||
.Include(t => t.WorkItem!)
|
||
.ThenInclude(wi => wi.WorkArea!)
|
||
.ThenInclude(wa => wa.Floor!)
|
||
.ThenInclude(f => f.Building)
|
||
.Include(t => t.WorkItem!)
|
||
.ThenInclude(wi => wi.WorkCategoryMaster)
|
||
// Only select fields used later to reduce tracked object size in memory
|
||
.Select(t => new
|
||
{
|
||
TaskId = t.Id,
|
||
Building = new { Id = t.WorkItem!.WorkArea!.Floor!.Building!.Id, Name = t.WorkItem.WorkArea.Floor.Building.Name },
|
||
Floor = new { Id = t.WorkItem.WorkArea.Floor.Id, Name = t.WorkItem.WorkArea.Floor.FloorName },
|
||
WorkArea = new { Id = t.WorkItem.WorkArea.Id, Name = t.WorkItem.WorkArea.AreaName },
|
||
Activity = t.WorkItem.ActivityMaster == null ? null : new { Id = t.WorkItem.ActivityMaster.Id, Name = t.WorkItem.ActivityMaster.ActivityName },
|
||
WorkCategory = t.WorkItem.WorkCategoryMaster == null ? null : new { Id = t.WorkItem.WorkCategoryMaster.Id, Name = t.WorkItem.WorkCategoryMaster.Name },
|
||
Service = t.WorkItem.ActivityMaster!.ActivityGroup!.Service == null ? null : new { Id = t.WorkItem.ActivityMaster.ActivityGroup.Service.Id, Name = t.WorkItem.ActivityMaster.ActivityGroup.Service.Name }
|
||
})
|
||
.ToListAsync(ct); // optimized projection [memory:10]
|
||
|
||
_logger.LogDebug("Fetched {TaskCount} tasks after filtering and projection", tasks.Count); // metrics [memory:1]
|
||
|
||
// Documents query in parallel with tasks is okay; here we’ve already awaited tasks, but both can run together if needed.
|
||
// Only fetch uploader fields needed
|
||
var documents = await _context.Documents
|
||
.AsNoTracking()
|
||
.Where(d => documentIds.Contains(d.Id))
|
||
.Select(d => new
|
||
{
|
||
d.Id,
|
||
UploadedBy = d.UploadedBy == null ? null : new
|
||
{
|
||
d.UploadedBy.Id,
|
||
d.UploadedBy.FirstName,
|
||
d.UploadedBy.LastName
|
||
}
|
||
})
|
||
.ToListAsync(ct); // minimal shape [memory:10]
|
||
|
||
_logger.LogDebug("Fetched {DocumentCount} documents for UploadedBy resolution", documents.Count); // metrics [memory:1]
|
||
|
||
// Distinct projections via HashSet to avoid custom equality or anonymous Distinct pitfalls
|
||
static List<T> DistinctBy<T, TKey>(IEnumerable<T> source, Func<T, TKey> keySelector)
|
||
=> source.GroupBy(keySelector).Select(g => g.First()).ToList();
|
||
|
||
var buildings = DistinctBy(tasks.Select(t => t.Building), b => b.Id);
|
||
var floors = DistinctBy(tasks.Select(t => t.Floor), f => f.Id);
|
||
var workAreas = DistinctBy(tasks.Select(t => t.WorkArea), wa => wa.Id);
|
||
var activities = DistinctBy(tasks.Where(t => t.Activity != null).Select(t => t.Activity!), a => a.Id);
|
||
var workCategories = DistinctBy(tasks.Where(t => t.WorkCategory != null).Select(t => t.WorkCategory!), wc => wc.Id);
|
||
var services = DistinctBy(tasks.Where(t => t.Service != null).Select(t => t.Service!), s => s.Id);
|
||
|
||
var uploadedBys = DistinctBy(
|
||
documents.Where(d => d.UploadedBy != null)
|
||
.Select(d => new
|
||
{
|
||
Id = d.UploadedBy!.Id,
|
||
Name = string.Join(' ', new[] { d.UploadedBy!.FirstName, d.UploadedBy!.LastName }.Where(x => !string.IsNullOrWhiteSpace(x)))
|
||
}),
|
||
u => u.Id);
|
||
|
||
var response = new
|
||
{
|
||
Buildings = buildings,
|
||
Floors = floors,
|
||
WorkAreas = workAreas,
|
||
WorkCategories = workCategories,
|
||
Activities = activities,
|
||
UploadedBys = uploadedBys,
|
||
Services = services
|
||
};
|
||
|
||
_logger.LogInfo("GetFilterObject succeeded for ProjectId {ProjectId}. Buildings={Buildings}, Floors={Floors}, WorkAreas={WorkAreas}, Activities={Activities}, WorkCategories={WorkCategories}, Services={Services}, UploadedBys={UploadedBys}",
|
||
projectId, buildings.Count, floors.Count, workAreas.Count, activities.Count, workCategories.Count, services.Count, uploadedBys.Count); // success log [memory:1]
|
||
|
||
return Ok(ApiResponse<object>.SuccessResponse(response, "Filter object for image gallery fetched successfully", 200));
|
||
}
|
||
|
||
[HttpGet("{documentId}")]
|
||
public async Task<IActionResult> GetImage(Guid documentId)
|
||
{
|
||
// Log the start of the image fetch process
|
||
_logger.LogInfo("GetImage called for DocumentId: {DocumentId}", documentId);
|
||
|
||
// Step 1: Get the currently logged-in employee (for future use like permission checks or auditing)
|
||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||
|
||
// Step 2: Fetch the document from the database based on the provided ID
|
||
var document = await _context.Documents.FirstOrDefaultAsync(d => d.Id == documentId);
|
||
|
||
// Step 3: If document doesn't exist, return a 400 Bad Request response
|
||
if (document == null)
|
||
{
|
||
_logger.LogWarning("Document not found for DocumentId: {DocumentId}", documentId);
|
||
return BadRequest(ApiResponse<object>.ErrorResponse("Document not found", "Document not found", 400));
|
||
}
|
||
|
||
// Step 4: Generate pre-signed URLs for thumbnail and full image (if keys exist)
|
||
string? thumbnailUrl = document.ThumbS3Key != null
|
||
? _s3Service.GeneratePreSignedUrl(document.ThumbS3Key)
|
||
: null;
|
||
|
||
string? imageUrl = document.S3Key != null
|
||
? _s3Service.GeneratePreSignedUrl(document.S3Key)
|
||
: null;
|
||
|
||
// Step 5: Prepare the response object
|
||
var response = new
|
||
{
|
||
ThumbnailUrl = thumbnailUrl,
|
||
ImageUrl = imageUrl
|
||
};
|
||
|
||
// Step 6: Log successful fetch and return the result
|
||
_logger.LogInfo("Image fetched successfully for DocumentId: {DocumentId}", documentId);
|
||
return Ok(ApiResponse<object>.SuccessResponse(response, "Image fetched successfully", 200));
|
||
}
|
||
}
|
||
}
|