301 lines
15 KiB
C#
301 lines
15 KiB
C#
using Marco.Pms.DataAccess.Data;
|
|
using Marco.Pms.Helpers.Utility;
|
|
using Marco.Pms.Model.MongoDBModels.Masters;
|
|
using Marco.Pms.Model.MongoDBModels.Project;
|
|
using MarcoBMS.Services.Service;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Marco.Pms.Services.Helpers
|
|
{
|
|
public class GeneralHelper
|
|
{
|
|
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
|
|
private readonly ApplicationDbContext _context; // Keeping this for direct scoped context use where appropriate
|
|
private readonly ILoggingService _logger;
|
|
private readonly FeatureDetailsHelper _featureDetailsHelper;
|
|
public GeneralHelper(IDbContextFactory<ApplicationDbContext> dbContextFactory,
|
|
ApplicationDbContext context,
|
|
ILoggingService logger,
|
|
FeatureDetailsHelper featureDetailsHelper)
|
|
{
|
|
_dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory));
|
|
_context = context ?? throw new ArgumentNullException(nameof(context));
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
_featureDetailsHelper = featureDetailsHelper ?? throw new ArgumentNullException(nameof(featureDetailsHelper));
|
|
}
|
|
public async Task<List<BuildingMongoDB>> GetProjectInfraFromDB(Guid projectId)
|
|
{
|
|
// Each task uses its own DbContext instance for thread safety. Projections are used for efficiency.
|
|
|
|
// Task to fetch Buildings, Floors, and WorkAreas using projections
|
|
var hierarchyTask = Task.Run(async () =>
|
|
{
|
|
using var context = _dbContextFactory.CreateDbContext();
|
|
var buildings = await context.Buildings.AsNoTracking().Where(b => b.ProjectId == projectId).Select(b => new { b.Id, b.Name, b.Description }).ToListAsync();
|
|
var buildingIds = buildings.Select(b => b.Id).ToList();
|
|
var floors = await context.Floor.AsNoTracking().Where(f => buildingIds.Contains(f.BuildingId)).Select(f => new { f.Id, f.BuildingId, f.FloorName }).ToListAsync();
|
|
var floorIds = floors.Select(f => f.Id).ToList();
|
|
var workAreas = await context.WorkAreas.AsNoTracking().Where(wa => floorIds.Contains(wa.FloorId)).Select(wa => new { wa.Id, wa.FloorId, wa.AreaName }).ToListAsync();
|
|
return (buildings, floors, workAreas);
|
|
});
|
|
|
|
// Task to get work summaries, AGGREGATED ON THE DATABASE SERVER
|
|
var workSummaryTask = Task.Run(async () =>
|
|
{
|
|
using var context = _dbContextFactory.CreateDbContext();
|
|
// This is the most powerful optimization. It avoids pulling all WorkItem rows.
|
|
return await context.WorkItems.AsNoTracking()
|
|
.Where(wi => wi.WorkArea != null && wi.WorkArea.Floor != null && wi.WorkArea.Floor.Building != null && wi.WorkArea.Floor.Building.ProjectId == projectId)
|
|
.GroupBy(wi => wi.WorkAreaId) // Group by the parent WorkArea
|
|
.Select(g => new
|
|
{
|
|
WorkAreaId = g.Key,
|
|
PlannedWork = g.Sum(i => i.PlannedWork),
|
|
CompletedWork = g.Sum(i => i.CompletedWork)
|
|
})
|
|
.ToDictionaryAsync(x => x.WorkAreaId); // Return a ready-to-use dictionary for fast lookups
|
|
});
|
|
|
|
await Task.WhenAll(hierarchyTask, workSummaryTask);
|
|
|
|
var (buildings, floors, workAreas) = await hierarchyTask;
|
|
var workSummariesByWorkAreaId = await workSummaryTask;
|
|
|
|
// --- Step 4: Build the hierarchy efficiently using Lookups ---
|
|
// Using lookups is much faster (O(1)) than repeated .Where() calls (O(n)).
|
|
var floorsByBuildingId = floors.ToLookup(f => f.BuildingId);
|
|
var workAreasByFloorId = workAreas.ToLookup(wa => wa.FloorId);
|
|
|
|
var buildingMongoList = new List<BuildingMongoDB>();
|
|
foreach (var building in buildings)
|
|
{
|
|
double buildingPlanned = 0, buildingCompleted = 0;
|
|
var floorMongoList = new List<FloorMongoDB>();
|
|
|
|
foreach (var floor in floorsByBuildingId[building.Id]) // Fast lookup
|
|
{
|
|
double floorPlanned = 0, floorCompleted = 0;
|
|
var workAreaMongoList = new List<WorkAreaMongoDB>();
|
|
|
|
foreach (var workArea in workAreasByFloorId[floor.Id]) // Fast lookup
|
|
{
|
|
// Get the pre-calculated summary from the dictionary. O(1) operation.
|
|
workSummariesByWorkAreaId.TryGetValue(workArea.Id, out var summary);
|
|
var waPlanned = summary?.PlannedWork ?? 0;
|
|
var waCompleted = summary?.CompletedWork ?? 0;
|
|
|
|
workAreaMongoList.Add(new WorkAreaMongoDB
|
|
{
|
|
Id = workArea.Id.ToString(),
|
|
AreaName = workArea.AreaName,
|
|
PlannedWork = waPlanned,
|
|
CompletedWork = waCompleted
|
|
});
|
|
|
|
floorPlanned += waPlanned;
|
|
floorCompleted += waCompleted;
|
|
}
|
|
|
|
floorMongoList.Add(new FloorMongoDB
|
|
{
|
|
Id = floor.Id.ToString(),
|
|
FloorName = floor.FloorName,
|
|
PlannedWork = floorPlanned,
|
|
CompletedWork = floorCompleted,
|
|
WorkAreas = workAreaMongoList
|
|
});
|
|
|
|
buildingPlanned += floorPlanned;
|
|
buildingCompleted += floorCompleted;
|
|
}
|
|
|
|
buildingMongoList.Add(new BuildingMongoDB
|
|
{
|
|
Id = building.Id.ToString(),
|
|
BuildingName = building.Name,
|
|
Description = building.Description,
|
|
PlannedWork = buildingPlanned,
|
|
CompletedWork = buildingCompleted,
|
|
Floors = floorMongoList
|
|
});
|
|
}
|
|
return buildingMongoList;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves a list of work items for a specific work area, including a summary of tasks assigned for the current day.
|
|
/// This method is highly optimized to run database operations in parallel and perform aggregations on the server.
|
|
/// </summary>
|
|
/// <param name="workAreaId">The ID of the work area.</param>
|
|
/// <returns>A list of WorkItemMongoDB objects with calculated daily assignments.</returns>
|
|
public async Task<List<WorkItemMongoDB>> GetWorkItemsListFromDB(Guid workAreaId)
|
|
{
|
|
_logger.LogInfo("Fetching DB work items for WorkAreaId: {WorkAreaId}", workAreaId);
|
|
|
|
try
|
|
{
|
|
// --- Step 1: Run independent database queries in PARALLEL ---
|
|
// We can fetch the WorkItems and the aggregated TaskAllocations at the same time.
|
|
|
|
// Task 1: Fetch the WorkItem entities and their related data.
|
|
var workItemsTask = _context.WorkItems
|
|
.Include(wi => wi.ActivityMaster)
|
|
.Include(wi => wi.WorkCategoryMaster)
|
|
.Where(wi => wi.WorkAreaId == workAreaId)
|
|
.AsNoTracking()
|
|
.ToListAsync();
|
|
|
|
// Task 2: Fetch and AGGREGATE today's task allocations ON THE DATABASE SERVER.
|
|
var todaysAssignmentsTask = Task.Run(async () =>
|
|
{
|
|
// Correctly define "today's" date range to avoid precision issues.
|
|
var today = DateTime.UtcNow.Date;
|
|
var tomorrow = today.AddDays(1);
|
|
|
|
using var context = _dbContextFactory.CreateDbContext(); // Use a factory for thread safety
|
|
|
|
// This is the most powerful optimization:
|
|
// 1. It filters by WorkAreaId directly, making it independent of the first query.
|
|
// 2. It filters by a correct date range.
|
|
// 3. It groups and sums on the DB server, returning only a small summary.
|
|
return await context.TaskAllocations
|
|
.Where(t => t.WorkItem != null && t.WorkItem.WorkAreaId == workAreaId &&
|
|
t.AssignmentDate >= today && t.AssignmentDate < tomorrow)
|
|
.GroupBy(t => t.WorkItemId)
|
|
.Select(g => new
|
|
{
|
|
WorkItemId = g.Key,
|
|
TodaysAssigned = g.Sum(x => x.PlannedTask)
|
|
})
|
|
// Return a dictionary for instant O(1) lookups later.
|
|
.ToDictionaryAsync(x => x.WorkItemId, x => x.TodaysAssigned);
|
|
});
|
|
|
|
// Await both parallel database operations to complete.
|
|
await Task.WhenAll(workItemsTask, todaysAssignmentsTask);
|
|
|
|
// Retrieve the results from the completed tasks.
|
|
var workItemsFromDb = await workItemsTask;
|
|
var todaysAssignments = await todaysAssignmentsTask;
|
|
|
|
// --- Step 2: Map to the ViewModel/MongoDB model efficiently ---
|
|
var workItemVMs = workItemsFromDb.Select(wi => new WorkItemMongoDB
|
|
{
|
|
Id = wi.Id.ToString(),
|
|
WorkAreaId = wi.WorkAreaId.ToString(),
|
|
ParentTaskId = wi.ParentTaskId.ToString(),
|
|
ActivityMaster = wi.ActivityMaster != null ? new ActivityMasterMongoDB
|
|
{
|
|
Id = wi.ActivityMaster.Id.ToString(),
|
|
ActivityName = wi.ActivityMaster.ActivityName,
|
|
UnitOfMeasurement = wi.ActivityMaster.UnitOfMeasurement
|
|
} : null,
|
|
WorkCategoryMaster = wi.WorkCategoryMaster != null ? new WorkCategoryMasterMongoDB
|
|
{
|
|
Id = wi.WorkCategoryMaster.Id.ToString(),
|
|
Name = wi.WorkCategoryMaster.Name,
|
|
Description = wi.WorkCategoryMaster.Description
|
|
} : null,
|
|
PlannedWork = wi.PlannedWork,
|
|
CompletedWork = wi.CompletedWork,
|
|
Description = wi.Description,
|
|
TaskDate = wi.TaskDate,
|
|
// Use the fast dictionary lookup instead of the slow in-memory Where/Sum.
|
|
TodaysAssigned = todaysAssignments.GetValueOrDefault(wi.Id, 0)
|
|
}).ToList();
|
|
|
|
_logger.LogInfo("Successfully processed {WorkItemCount} work items for WorkAreaId: {WorkAreaId}", workItemVMs.Count, workAreaId);
|
|
|
|
return workItemVMs;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "An error occurred while fetching DB work items for WorkAreaId: {WorkAreaId}", workAreaId);
|
|
// Return an empty list or re-throw, depending on your application's error handling strategy.
|
|
return new List<WorkItemMongoDB>();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves all enabled feature IDs for a given tenant based on their active subscription.
|
|
/// </summary>
|
|
/// <param name="tenantId">The unique identifier of the tenant.</param>
|
|
/// <returns>A list of feature IDs available for the tenant.</returns>
|
|
public async Task<List<Guid>> GetFeatureIdsByTenentIdAsync(Guid tenantId)
|
|
{
|
|
try
|
|
{
|
|
_logger.LogInfo("Fetching feature IDs for tenant: {TenantId}", tenantId);
|
|
|
|
// Step 1: Get active tenant subscription with plan
|
|
var tenantSubscription = await _context.TenantSubscriptions
|
|
.Include(ts => ts.Plan)
|
|
.AsNoTracking() // Optimization: Read-only query, no need to track
|
|
.FirstOrDefaultAsync(ts =>
|
|
ts.TenantId == tenantId &&
|
|
ts.Plan != null &&
|
|
!ts.IsCancelled &&
|
|
ts.EndDate.Date >= DateTime.UtcNow.Date); // FIX: Subscription should not be expired
|
|
|
|
if (tenantSubscription == null)
|
|
{
|
|
_logger.LogWarning("No active subscription found for tenant: {TenantId}", tenantId);
|
|
return new List<Guid>();
|
|
}
|
|
|
|
_logger.LogDebug("Active subscription found for tenant: {TenantId}, PlanId: {PlanId}",
|
|
tenantId, tenantSubscription.Plan!.Id);
|
|
|
|
var featureIds = new List<Guid> { new Guid("2f3509b7-160d-410a-b9b6-daadd96c986d"), new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be") };
|
|
|
|
// Step 2: Get feature details from Plan
|
|
var featureDetails = await _featureDetailsHelper.GetFeatureDetails(tenantSubscription.Plan!.FeaturesId);
|
|
|
|
if (featureDetails == null)
|
|
{
|
|
_logger.LogWarning("No feature details found for tenant: {TenantId}, PlanId: {PlanId}",
|
|
tenantId, tenantSubscription.Plan!.Id);
|
|
return new List<Guid>();
|
|
}
|
|
|
|
// Step 3: Collect all enabled feature IDs from modules
|
|
|
|
if (featureDetails.Modules?.Attendance?.Enabled == true)
|
|
{
|
|
featureIds.AddRange(featureDetails.Modules.Attendance.FeatureId);
|
|
_logger.LogDebug("Added Attendance module features for tenant: {TenantId}", tenantId);
|
|
}
|
|
|
|
if (featureDetails.Modules?.ProjectManagement?.Enabled == true)
|
|
{
|
|
featureIds.AddRange(featureDetails.Modules.ProjectManagement.FeatureId);
|
|
_logger.LogDebug("Added Project Management module features for tenant: {TenantId}", tenantId);
|
|
}
|
|
|
|
if (featureDetails.Modules?.Directory?.Enabled == true)
|
|
{
|
|
featureIds.AddRange(featureDetails.Modules.Directory.FeatureId);
|
|
_logger.LogDebug("Added Directory module features for tenant: {TenantId}", tenantId);
|
|
}
|
|
|
|
if (featureDetails.Modules?.Expense?.Enabled == true)
|
|
{
|
|
featureIds.AddRange(featureDetails.Modules.Expense.FeatureId);
|
|
_logger.LogDebug("Added Expense module features for tenant: {TenantId}", tenantId);
|
|
}
|
|
|
|
_logger.LogInfo("Returning {Count} feature IDs for tenant: {TenantId}", featureIds.Count, tenantId);
|
|
|
|
return featureIds.Distinct().ToList();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Step 4: Handle unexpected errors
|
|
_logger.LogError(ex, "Error retrieving feature IDs for tenant: {TenantId}", tenantId);
|
|
return new List<Guid>();
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|