Organization_Management #142

Merged
ashutosh.nehete merged 92 commits from Organization_Management into main 2025-09-30 09:05:14 +00:00
Showing only changes of commit 8d64e9702d - Show all commits

View File

@ -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 doesnt 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 weve 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)
{ {