409 lines
20 KiB
C#
409 lines
20 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
|
|
|
|
var featureIds = new List<Guid>
|
|
{
|
|
new Guid("a4e25142-449b-4334-a6e5-22f70e4732d7"), // Expense Management feature
|
|
new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), // Employee Management feature
|
|
new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), // Attendance Management feature
|
|
new Guid("a8cf4331-8f04-4961-8360-a3f7c3cc7462"), // Document Management feature
|
|
new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), // Masters Management feature
|
|
new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), // Directory Management feature
|
|
new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914") // Organization Management feature
|
|
//new Guid("2f3509b7-160d-410a-b9b6-daadd96c986d") // Tenant Management feature
|
|
};
|
|
|
|
if (tenantSubscription == null)
|
|
{
|
|
_logger.LogWarning("No active subscription found for tenant: {TenantId}", tenantId);
|
|
return featureIds;
|
|
}
|
|
|
|
_logger.LogDebug("Active subscription found for tenant: {TenantId}, PlanId: {PlanId}",
|
|
tenantId, tenantSubscription.Plan!.Id);
|
|
|
|
//var featureIds = new List<Guid> { 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;
|
|
}
|
|
|
|
}
|
|
}
|