396 lines
19 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;
private static readonly Guid SuperTenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26");
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)
.ThenInclude(am => am!.ActivityGroup)
.ThenInclude(ag => ag!.Service)
.Include(wi => wi.WorkCategoryMaster)
.Where(wi => wi.WorkAreaId == workAreaId && wi.ActivityMaster != null && wi.ActivityMaster.ActivityGroup != null
&& wi.ActivityMaster.ActivityGroup.Service != null && wi.WorkCategoryMaster != null)
.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,
ActivityGroupMaster = new ActivityGroupMasterMongoDB
{
Id = wi.ActivityMaster.ActivityGroup!.Id.ToString(),
Name = wi.ActivityMaster.ActivityGroup.Name,
Description = wi.ActivityMaster.ActivityGroup.Description,
Service = new ServiceMasterMongoDB
{
Id = wi.ActivityMaster.ActivityGroup.Service!.Id.ToString(),
Name = wi.ActivityMaster.ActivityGroup.Service.Name,
Description = wi.ActivityMaster.ActivityGroup.Service.Description
}
}
} : 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>();
}
}
/// <summary>
/// Checks whether the tenant still has available seats (MaxUsers not exceeded).
/// </summary>
/// <param name="tenantId">The ID of the tenant to check.</param>
/// <returns>True if seats are available; otherwise false.</returns>
public async Task<bool> CheckSeatsRemainingAsync(Guid tenantId)
{
_logger.LogInfo("Checking seats remaining for TenantId: {TenantId}", tenantId);
try
{
if (tenantId == SuperTenantId)
{
return true;
}
// Run both queries concurrently
var totalSeatsTask = GetMaxSeatsAsync(tenantId);
var totalSeatsTakenTask = GetActiveEmployeesCountAsync(tenantId);
await Task.WhenAll(totalSeatsTask, totalSeatsTakenTask);
var totalSeats = await totalSeatsTask;
var totalSeatsTaken = await totalSeatsTakenTask;
_logger.LogInfo(
"TenantId: {TenantId} | TotalSeats: {TotalSeats} | SeatsTaken: {SeatsTaken}",
tenantId, totalSeats, totalSeatsTaken);
bool seatsAvailable = totalSeats >= totalSeatsTaken;
_logger.LogDebug("TenantId: {TenantId} | Seats Available: {SeatsAvailable}",
tenantId, seatsAvailable);
return seatsAvailable;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error checking seats for TenantId: {TenantId}", tenantId);
throw;
}
}
/// <summary>
/// Retrieves the maximum number of allowed seats (MaxUsers) for a tenant.
/// </summary>
private async Task<double> GetMaxSeatsAsync(Guid tenantId)
{
_logger.LogDebug("Fetching maximum seats for TenantId: {TenantId}", tenantId);
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
var maxSeats = await dbContext.TenantSubscriptions
.Where(ts => ts.TenantId == tenantId && !ts.IsCancelled)
.Select(ts => ts.MaxUsers)
.FirstOrDefaultAsync();
_logger.LogDebug("TenantId: {TenantId} | MaxSeats: {MaxSeats}", tenantId, maxSeats);
return maxSeats;
}
/// <summary>
/// Counts the number of active employees for a tenant.
/// </summary>
private async Task<int> GetActiveEmployeesCountAsync(Guid tenantId)
{
_logger.LogDebug("Counting active employees for TenantId: {TenantId}", tenantId);
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
var activeEmployees = await dbContext.Employees
.Where(e => e.TenantId == tenantId && e.IsActive)
.CountAsync();
_logger.LogDebug("TenantId: {TenantId} | ActiveEmployees: {ActiveEmployees}", tenantId, activeEmployees);
return activeEmployees;
}
}
}