Added the get filter API in image gallery
This commit is contained in:
parent
98acbb66d6
commit
8d64e9702d
@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.CodeAnalysis;
|
using Microsoft.CodeAnalysis;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Serilog;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Marco.Pms.Services.Controllers
|
namespace Marco.Pms.Services.Controllers
|
||||||
@ -22,20 +23,27 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
[Authorize]
|
[Authorize]
|
||||||
public class ImageController : ControllerBase
|
public class ImageController : ControllerBase
|
||||||
{
|
{
|
||||||
|
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
|
||||||
private readonly ApplicationDbContext _context;
|
private readonly ApplicationDbContext _context;
|
||||||
private readonly S3UploadService _s3Service;
|
private readonly S3UploadService _s3Service;
|
||||||
private readonly UserHelper _userHelper;
|
private readonly UserHelper _userHelper;
|
||||||
private readonly ILoggingService _logger;
|
private readonly ILoggingService _logger;
|
||||||
private readonly PermissionServices _permission;
|
private readonly PermissionServices _permission;
|
||||||
private readonly Guid tenantId;
|
private readonly Guid tenantId;
|
||||||
public ImageController(ApplicationDbContext context, S3UploadService s3Service, UserHelper userHelper, ILoggingService logger, PermissionServices permission)
|
public ImageController(IDbContextFactory<ApplicationDbContext> dbContextFactory,
|
||||||
|
ApplicationDbContext context,
|
||||||
|
S3UploadService s3Service,
|
||||||
|
UserHelper userHelper,
|
||||||
|
ILoggingService logger,
|
||||||
|
PermissionServices permission)
|
||||||
{
|
{
|
||||||
_context = context;
|
_dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory));
|
||||||
_s3Service = s3Service;
|
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||||
_userHelper = userHelper;
|
_s3Service = s3Service ?? throw new ArgumentNullException(nameof(s3Service));
|
||||||
_logger = logger;
|
_userHelper = userHelper ?? throw new ArgumentNullException(nameof(userHelper));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_permission = permission ?? throw new ArgumentNullException(nameof(permission));
|
||||||
tenantId = userHelper.GetTenantId();
|
tenantId = userHelper.GetTenantId();
|
||||||
_permission = permission;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("images/{projectId}")]
|
[HttpGet("images/{projectId}")]
|
||||||
@ -382,6 +390,150 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
return Ok(ApiResponse<object>.SuccessResponse(response, "Images for provided batchId fetched successfully", 200));
|
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}")]
|
[HttpGet("{documentId}")]
|
||||||
public async Task<IActionResult> GetImage(Guid documentId)
|
public async Task<IActionResult> GetImage(Guid documentId)
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user