1301 lines
56 KiB
C#

using AutoMapper;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Helpers;
using Marco.Pms.Helpers.CacheHelper;
using Marco.Pms.Model.Expenses;
using Marco.Pms.Model.Master;
using Marco.Pms.Model.MongoDBModels;
using Marco.Pms.Model.MongoDBModels.Employees;
using Marco.Pms.Model.MongoDBModels.Expenses;
using Marco.Pms.Model.MongoDBModels.Masters;
using Marco.Pms.Model.MongoDBModels.Project;
using Marco.Pms.Model.MongoDBModels.Utility;
using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities;
using MarcoBMS.Services.Service;
using Microsoft.EntityFrameworkCore;
using MongoDB.Driver;
using Project = Marco.Pms.Model.Projects.Project;
namespace Marco.Pms.Services.Helpers
{
public class CacheUpdateHelper
{
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
private readonly ApplicationDbContext _context;
private readonly IMapper _mapper;
private readonly ProjectCache _projectCache;
private readonly EmployeeCache _employeeCache;
private readonly ReportCache _reportCache;
private readonly ExpenseCache _expenseCache;
private readonly ILoggingService _logger;
private readonly GeneralHelper _generalHelper;
private static readonly Guid Draft = Guid.Parse("297e0d8f-f668-41b5-bfea-e03b354251c8");
private static readonly Guid Rejected = Guid.Parse("d1ee5eec-24b6-4364-8673-a8f859c60729");
public CacheUpdateHelper(
IMapper mapper,
ProjectCache projectCache,
EmployeeCache employeeCache,
ReportCache reportCache,
ExpenseCache expenseCache,
ILoggingService logger,
IDbContextFactory<ApplicationDbContext> dbContextFactory,
ApplicationDbContext context,
GeneralHelper generalHelper)
{
_mapper = mapper;
_projectCache = projectCache;
_employeeCache = employeeCache;
_reportCache = reportCache;
_expenseCache = expenseCache;
_logger = logger;
_dbContextFactory = dbContextFactory;
_context = context;
_generalHelper = generalHelper;
}
#region ======================================================= Project Details Cache =======================================================
public async Task AddProjectDetails(Project project)
{
// --- Step 1: Fetch all required data from the database in parallel ---
// Each task uses its own DbContext instance to avoid concurrency issues.
var statusTask = Task.Run(async () =>
{
using var context = _dbContextFactory.CreateDbContext();
return await context.StatusMasters
.AsNoTracking()
.Where(s => s.Id == project.ProjectStatusId)
.Select(s => new { s.Id, s.Status }) // Projection
.FirstOrDefaultAsync();
});
var teamSizeTask = Task.Run(async () =>
{
using var context = _dbContextFactory.CreateDbContext();
return await context.ProjectAllocations
.AsNoTracking()
.CountAsync(pa => pa.ProjectId == project.Id && pa.IsActive); // Server-side count is efficient
});
// This task fetches the entire infrastructure hierarchy and performs aggregations in the database.
var infrastructureTask = Task.Run(async () =>
{
using var context = _dbContextFactory.CreateDbContext();
// 1. Fetch all hierarchical data using projections.
// This is still a chain, but it's inside one task and much faster due to projections.
var buildings = await context.Buildings.AsNoTracking()
.Where(b => b.ProjectId == project.Id)
.Select(b => new { b.Id, b.ProjectId, 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();
var workAreaIds = workAreas.Select(wa => wa.Id).ToList();
// 2. THE KEY OPTIMIZATION: Aggregate work items in the database.
var workSummaries = await context.WorkItems.AsNoTracking()
.Where(wi => workAreaIds.Contains(wi.WorkAreaId))
.GroupBy(wi => wi.WorkAreaId) // Group by parent on the DB server
.Select(g => new // Let the DB do the SUM
{
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
return (buildings, floors, workAreas, workSummaries);
});
// Wait for all parallel database operations to complete.
await Task.WhenAll(statusTask, teamSizeTask, infrastructureTask);
// Get the results from the completed tasks.
var status = await statusTask;
var teamSize = await teamSizeTask;
var (allBuildings, allFloors, allWorkAreas, workSummariesByWorkAreaId) = await infrastructureTask;
// --- Step 2: Process the fetched data and build the MongoDB model ---
var projectDetails = new ProjectMongoDB
{
Id = project.Id.ToString(),
Name = project.Name,
ShortName = project.ShortName,
ProjectAddress = project.ProjectAddress,
StartDate = project.StartDate,
EndDate = project.EndDate,
ContactPerson = project.ContactPerson,
TeamSize = teamSize
};
projectDetails.ProjectStatus = new StatusMasterMongoDB
{
Id = status!.Id.ToString(),
Status = status.Status
};
// Use fast in-memory lookups instead of .Where() in loops.
var floorsByBuildingId = allFloors.ToLookup(f => f.BuildingId);
var workAreasByFloorId = allWorkAreas.ToLookup(wa => wa.FloorId);
double totalPlannedWork = 0, totalCompletedWork = 0;
var buildingMongoList = new List<BuildingMongoDB>();
foreach (var building in allBuildings)
{
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 wa in workAreasByFloorId[floor.Id]) // Fast lookup
{
// Get the pre-calculated summary from the dictionary. O(1) operation.
workSummariesByWorkAreaId.TryGetValue(wa.Id, out var summary);
var waPlanned = summary?.PlannedWork ?? 0;
var waCompleted = summary?.CompletedWork ?? 0;
workAreaMongoList.Add(new WorkAreaMongoDB
{
Id = wa.Id.ToString(),
FloorId = wa.FloorId.ToString(),
AreaName = wa.AreaName,
PlannedWork = waPlanned,
CompletedWork = waCompleted
});
floorPlanned += waPlanned;
floorCompleted += waCompleted;
}
floorMongoList.Add(new FloorMongoDB
{
Id = floor.Id.ToString(),
BuildingId = floor.BuildingId.ToString(),
FloorName = floor.FloorName,
PlannedWork = floorPlanned,
CompletedWork = floorCompleted,
WorkAreas = workAreaMongoList
});
buildingPlanned += floorPlanned;
buildingCompleted += floorCompleted;
}
buildingMongoList.Add(new BuildingMongoDB
{
Id = building.Id.ToString(),
ProjectId = building.ProjectId.ToString(),
BuildingName = building.Name,
Description = building.Description,
PlannedWork = buildingPlanned,
CompletedWork = buildingCompleted,
Floors = floorMongoList
});
totalPlannedWork += buildingPlanned;
totalCompletedWork += buildingCompleted;
}
projectDetails.Buildings = buildingMongoList;
projectDetails.PlannedWork = totalPlannedWork;
projectDetails.CompletedWork = totalCompletedWork;
try
{
await _projectCache.AddProjectDetailsToCache(projectDetails);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while adding project {ProjectId} to Cache", project.Id);
}
}
public async Task AddProjectDetailsList(List<Project> projects)
{
var projectIds = projects.Select(p => p.Id).ToList();
if (!projectIds.Any())
{
return; // Nothing to do
}
var projectStatusIds = projects.Select(p => p.ProjectStatusId).Distinct().ToList();
// --- Step 1: Fetch all required data in maximum parallel ---
// Each task uses its own DbContext and selects only the required columns (projection).
var statusTask = Task.Run(async () =>
{
using var context = _dbContextFactory.CreateDbContext();
return await context.StatusMasters
.AsNoTracking()
.Where(s => projectStatusIds.Contains(s.Id))
.Select(s => new { s.Id, s.Status }) // Projection
.ToDictionaryAsync(s => s.Id);
});
var teamSizeTask = Task.Run(async () =>
{
using var context = _dbContextFactory.CreateDbContext();
// Server-side aggregation and projection into a dictionary
return await context.ProjectAllocations
.AsNoTracking()
.Where(pa => projectIds.Contains(pa.ProjectId) && pa.IsActive)
.GroupBy(pa => pa.ProjectId)
.Select(g => new { ProjectId = g.Key, Count = g.Count() })
.ToDictionaryAsync(x => x.ProjectId, x => x.Count);
});
var buildingsTask = Task.Run(async () =>
{
using var context = _dbContextFactory.CreateDbContext();
return await context.Buildings
.AsNoTracking()
.Where(b => projectIds.Contains(b.ProjectId))
.Select(b => new { b.Id, b.ProjectId, b.Name, b.Description }) // Projection
.ToListAsync();
});
// We need the building IDs for the next level, so we must await this one first.
var allBuildings = await buildingsTask;
var buildingIds = allBuildings.Select(b => b.Id).ToList();
var floorsTask = Task.Run(async () =>
{
using var context = _dbContextFactory.CreateDbContext();
return await context.Floor
.AsNoTracking()
.Where(f => buildingIds.Contains(f.BuildingId))
.Select(f => new { f.Id, f.BuildingId, f.FloorName }) // Projection
.ToListAsync();
});
// We need floor IDs for the next level.
var allFloors = await floorsTask;
var floorIds = allFloors.Select(f => f.Id).ToList();
var workAreasTask = Task.Run(async () =>
{
using var context = _dbContextFactory.CreateDbContext();
return await context.WorkAreas
.AsNoTracking()
.Where(wa => floorIds.Contains(wa.FloorId))
.Select(wa => new { wa.Id, wa.FloorId, wa.AreaName }) // Projection
.ToListAsync();
});
// The most powerful optimization: Aggregate work items in the database.
var workSummaryTask = Task.Run(async () =>
{
using var context = _dbContextFactory.CreateDbContext();
var workAreaIds = await context.WorkAreas
.Where(wa => floorIds.Contains(wa.FloorId))
.Select(wa => wa.Id)
.ToListAsync();
// Let the DB do the SUM. This is much faster and transfers less data.
return await context.WorkItems
.AsNoTracking()
.Where(wi => workAreaIds.Contains(wi.WorkAreaId))
.GroupBy(wi => wi.WorkAreaId)
.Select(g => new
{
WorkAreaId = g.Key,
PlannedWork = g.Sum(wi => wi.PlannedWork),
CompletedWork = g.Sum(wi => wi.CompletedWork)
})
.ToDictionaryAsync(x => x.WorkAreaId);
});
// Await the remaining parallel tasks.
await Task.WhenAll(statusTask, teamSizeTask, workAreasTask, workSummaryTask);
// --- Step 2: Process the fetched data and build the MongoDB models ---
var allStatuses = await statusTask;
var teamSizesByProjectId = await teamSizeTask;
var allWorkAreas = await workAreasTask;
var workSummariesByWorkAreaId = await workSummaryTask;
// Create fast in-memory lookups for hierarchical data
var buildingsByProjectId = allBuildings.ToLookup(b => b.ProjectId);
var floorsByBuildingId = allFloors.ToLookup(f => f.BuildingId);
var workAreasByFloorId = allWorkAreas.ToLookup(wa => wa.FloorId);
var projectDetailsList = new List<ProjectMongoDB>(projects.Count);
foreach (var project in projects)
{
var projectDetails = new ProjectMongoDB
{
Id = project.Id.ToString(),
Name = project.Name,
ShortName = project.ShortName,
ProjectAddress = project.ProjectAddress,
StartDate = project.StartDate,
EndDate = project.EndDate,
ContactPerson = project.ContactPerson,
TeamSize = teamSizesByProjectId.GetValueOrDefault(project.Id, 0)
};
if (allStatuses.TryGetValue(project.ProjectStatusId, out var status))
{
projectDetails.ProjectStatus = new StatusMasterMongoDB
{
Id = status.Id.ToString(),
Status = status.Status
};
}
double totalPlannedWork = 0, totalCompletedWork = 0;
var buildingMongoList = new List<BuildingMongoDB>();
foreach (var building in buildingsByProjectId[project.Id])
{
double buildingPlanned = 0, buildingCompleted = 0;
var floorMongoList = new List<FloorMongoDB>();
foreach (var floor in floorsByBuildingId[building.Id])
{
double floorPlanned = 0, floorCompleted = 0;
var workAreaMongoList = new List<WorkAreaMongoDB>();
foreach (var wa in workAreasByFloorId[floor.Id])
{
double waPlanned = 0, waCompleted = 0;
if (workSummariesByWorkAreaId.TryGetValue(wa.Id, out var summary))
{
waPlanned = summary.PlannedWork;
waCompleted = summary.CompletedWork;
}
workAreaMongoList.Add(new WorkAreaMongoDB
{
Id = wa.Id.ToString(),
FloorId = wa.FloorId.ToString(),
AreaName = wa.AreaName,
PlannedWork = waPlanned,
CompletedWork = waCompleted
});
floorPlanned += waPlanned;
floorCompleted += waCompleted;
}
floorMongoList.Add(new FloorMongoDB
{
Id = floor.Id.ToString(),
BuildingId = floor.BuildingId.ToString(),
FloorName = floor.FloorName,
PlannedWork = floorPlanned,
CompletedWork = floorCompleted,
WorkAreas = workAreaMongoList
});
buildingPlanned += floorPlanned;
buildingCompleted += floorCompleted;
}
buildingMongoList.Add(new BuildingMongoDB
{
Id = building.Id.ToString(),
ProjectId = building.ProjectId.ToString(),
BuildingName = building.Name,
Description = building.Description,
PlannedWork = buildingPlanned,
CompletedWork = buildingCompleted,
Floors = floorMongoList
});
totalPlannedWork += buildingPlanned;
totalCompletedWork += buildingCompleted;
}
projectDetails.Buildings = buildingMongoList;
projectDetails.PlannedWork = totalPlannedWork;
projectDetails.CompletedWork = totalCompletedWork;
projectDetailsList.Add(projectDetails);
}
// --- Step 3: Update the cache ---
try
{
await _projectCache.AddProjectDetailsListToCache(projectDetailsList);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while adding project list to Cache");
}
}
public async Task<bool> UpdateProjectDetailsOnly(Project project)
{
StatusMaster projectStatus = await _context.StatusMasters
.FirstOrDefaultAsync(s => s.Id == project.ProjectStatusId) ?? new StatusMaster();
try
{
bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project, projectStatus);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while updating project {ProjectId} to Cache", project.Id);
return false;
}
}
public async Task<ProjectMongoDB?> GetProjectDetails(Guid projectId)
{
try
{
var response = await _projectCache.GetProjectDetailsFromCache(projectId);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while getting project {ProjectId} to Cache");
return null;
}
}
public async Task<ProjectMongoDB?> GetProjectDetailsWithBuildings(Guid projectId)
{
try
{
var response = await _projectCache.GetProjectDetailsWithBuildingsFromCache(projectId);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while getting project {ProjectId} to Cache");
return null;
}
}
public async Task<List<ProjectMongoDB>?> GetProjectDetailsList(List<Guid> projectIds)
{
try
{
var response = await _projectCache.GetProjectDetailsListFromCache(projectIds);
if (response.Any())
{
return response;
}
else
{
return null;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while getting list of project details from to Cache");
return null;
}
}
public async Task DeleteProjectByIdAsync(Guid projectId)
{
try
{
var response = await _projectCache.DeleteProjectByIdFromCacheAsync(projectId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while deleting project from to Cache");
}
}
public async Task RemoveProjectsAsync(List<Guid> projectIds)
{
try
{
var response = await _projectCache.RemoveProjectsFromCacheAsync(projectIds);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while deleting project list from to Cache");
}
}
#endregion
#region ======================================================= Project Infrastructure Cache =======================================================
public async Task AddBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null)
{
try
{
await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while adding project infra for project {ProjectId} to Cache: {Error}", projectId, ex.Message);
}
}
public async Task UpdateBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null)
{
try
{
var response = await _projectCache.UpdateBuildngInfraToCache(projectId, building, floor, workArea, buildingId);
if (!response)
{
await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId);
}
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while updating project infra for project {ProjectId} to Cache: {Error}", projectId, ex.Message);
}
}
public async Task<List<BuildingMongoDB>?> GetBuildingInfra(Guid projectId)
{
try
{
var response = await _projectCache.GetBuildingInfraFromCache(projectId);
return response;
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while getting project infra for project {ProjectId} form Cache: {Error}", projectId, ex.Message);
return null;
}
}
public async Task UpdatePlannedAndCompleteWorksInBuilding(Guid workAreaId, double plannedWork = 0, double completedWork = 0)
{
try
{
await _projectCache.UpdatePlannedAndCompleteWorksInBuildingFromCache(workAreaId, plannedWork, completedWork);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while updating planned work and completed work in building infra form Cache: {Error}", ex.Message);
}
}
public async Task<WorkAreaInfoMongoDB?> GetBuildingAndFloorByWorkAreaId(Guid workAreaId)
{
try
{
var response = await _projectCache.GetBuildingAndFloorByWorkAreaIdFromCache(workAreaId);
return response;
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while fetching workArea Details using its ID form Cache: {Error}", ex.Message);
return null;
}
}
#endregion
#region ======================================================= WorkItem Cache =======================================================
public async Task<List<WorkItemMongoDB>?> GetWorkItemsByWorkAreaIds(List<Guid> workAreaIds)
{
try
{
var response = await _projectCache.GetWorkItemsByWorkAreaIdsFromCache(workAreaIds);
if (response.Count > 0)
{
return response;
}
return null;
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while fetching workItems list using workArea IDs list form Cache: {Error}", ex.Message);
return null;
}
}
public async Task ManageWorkItemDetails(List<WorkItem> workItems)
{
try
{
var workAreaId = workItems.First().WorkAreaId;
var workItemDB = await _generalHelper.GetWorkItemsListFromDB(workAreaId);
await _projectCache.ManageWorkItemDetailsToCache(workItemDB);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while saving workItems form Cache: {Error}", ex.Message);
}
}
public async Task ManageWorkItemDetailsByVM(List<WorkItemMongoDB> workItems)
{
try
{
await _projectCache.ManageWorkItemDetailsToCache(workItems);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while saving workItems form Cache: {Error}", ex.Message);
}
}
public async Task<List<WorkItemMongoDB>?> GetWorkItemDetailsByWorkArea(Guid workAreaId)
{
try
{
var workItems = await _projectCache.GetWorkItemDetailsByWorkAreaFromCache(workAreaId);
if (workItems.Count > 0)
{
return workItems;
}
else
{
return null;
}
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while fetching list of workItems form Cache: {Error}", ex.Message);
return null;
}
}
public async Task<WorkItemMongoDB?> GetWorkItemDetailsById(Guid id)
{
try
{
var workItem = await _projectCache.GetWorkItemDetailsByIdFromCache(id);
if (workItem.Id != "")
{
return workItem;
}
else
{
return null;
}
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while fetching list of workItems form Cache: {Error}", ex.Message);
return null;
}
}
public async Task UpdatePlannedAndCompleteWorksInWorkItem(Guid id, double plannedWork = 0, double completedWork = 0, double todaysAssigned = 0)
{
try
{
var response = await _projectCache.UpdatePlannedAndCompleteWorksInWorkItemToCache(id, plannedWork, completedWork, todaysAssigned);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while updating planned work, completed work, and today's assigned work in workItems in Cache: {Error}", ex.Message);
}
}
public async Task DeleteWorkItemByIdAsync(Guid workItemId)
{
try
{
var response = await _projectCache.DeleteWorkItemByIdFromCacheAsync(workItemId);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while deleting work item from to Cache: {Error}", ex.Message);
}
}
#endregion
#region ======================================================= Employee Profile Cache =======================================================
public async Task AddApplicationRole(Guid employeeId, List<Guid> roleIds)
{
// 1. Guard Clause: Avoid unnecessary database work if there are no roles to add.
if (roleIds == null || !roleIds.Any())
{
return; // Nothing to add, so the operation did not result in a change.
}
Task<List<string>> getPermissionIdsTask = Task.Run(async () =>
{
using var context = _dbContextFactory.CreateDbContext();
return await context.RolePermissionMappings
.Where(rp => roleIds.Contains(rp.ApplicationRoleId))
.Select(p => p.FeaturePermissionId.ToString())
.Distinct()
.ToListAsync();
});
// 3. Prepare role IDs in parallel with the database query.
var newRoleIds = roleIds.Select(r => r.ToString()).ToList();
// 4. Await the database query result.
var newPermissionIds = await getPermissionIdsTask;
try
{
var response = await _employeeCache.AddApplicationRoleToCache(employeeId, newRoleIds, newPermissionIds);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while adding Application roleIds to Cache to employee {Employee}: {Error}", employeeId, ex.Message);
}
}
public async Task<bool> AddProjects(Guid employeeId, List<Guid> projectIds)
{
try
{
var response = await _employeeCache.AddProjectsToCache(employeeId, projectIds);
return response;
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while adding projectIds for employee {EmployeeId} to Cache: {Error}", employeeId, ex.Message);
return false;
}
}
public async Task<List<Guid>?> GetProjects(Guid employeeId)
{
try
{
var response = await _employeeCache.GetProjectsFromCache(employeeId);
if (response.Count > 0)
{
return response;
}
return null;
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while getting projectIds for employee {EmployeeId} from Cache: {Error}", employeeId, ex.Message);
return null;
}
}
public async Task<List<Guid>?> GetPermissions(Guid employeeId)
{
try
{
var response = await _employeeCache.GetPermissionsFromCache(employeeId);
if (response.Count > 0)
{
return response;
}
return null;
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while getting permissionIds for employee {EmployeeId} from Cache: {Error}", employeeId, ex.Message);
return null;
}
}
public async Task ClearAllProjectIds(Guid employeeId)
{
try
{
var response = await _employeeCache.ClearAllProjectIdsFromCache(employeeId);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while deleting projectIds from Cache for employee {EmployeeId}: {Error}", employeeId, ex.Message);
}
}
public async Task ClearAllProjectIdsByRoleId(Guid roleId)
{
try
{
await _employeeCache.ClearAllProjectIdsByRoleIdFromCache(roleId);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while deleting projectIds from Cache for Application Role {RoleId}: {Error}", roleId, ex.Message);
}
}
public async Task ClearAllProjectIdsByPermissionId(Guid permissionId)
{
try
{
await _employeeCache.ClearAllProjectIdsByPermissionIdFromCache(permissionId);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while deleting projectIds from Cache for Permission {PermissionId}: {Error}", permissionId, ex.Message);
}
}
public async Task ClearAllPermissionIdsByEmployeeID(Guid employeeId)
{
try
{
var response = await _employeeCache.ClearAllPermissionIdsByEmployeeIDFromCache(employeeId);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while deleting permissionIds from Cache for employee {EmployeeId}: {Error}", employeeId, ex.Message);
}
}
public async Task ClearAllPermissionIdsByRoleId(Guid roleId)
{
try
{
var response = await _employeeCache.ClearAllPermissionIdsByRoleIdFromCache(roleId);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while deleting permissionIds from Cache for Application role {RoleId}: {Error}", roleId, ex.Message);
}
}
public async Task RemoveRoleId(Guid employeeId, Guid roleId)
{
try
{
var response = await _employeeCache.RemoveRoleIdFromCache(employeeId, roleId);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while deleting Application role {RoleId} from Cache for employee {EmployeeId}: {Error}", roleId, employeeId, ex.Message);
}
}
public async Task ClearAllEmployees()
{
try
{
var response = await _employeeCache.ClearAllEmployeesFromCache();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while deleting all employees from Cache");
}
}
#endregion
#region ======================================================= Expenses Cache =======================================================
public async Task AddExpenseByObjectAsync(Expenses expense)
{
var expenseCache = await GetAllExpnesRelatedTablesForSingle(expense, expense.TenantId);
try
{
await _expenseCache.AddExpenseToCacheAsync(expenseCache);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurd while storing expense related table in cahce");
return;
}
return;
}
public async Task<ExpenseDetailsMongoDB?> AddExpenseByIdAsync(Guid Id, Guid tenantId)
{
var expense = await _context.Expenses.AsNoTracking().FirstOrDefaultAsync(e => e.Id == Id && e.TenantId == tenantId);
if (expense == null)
{
return null;
}
var expenseCache = await GetAllExpnesRelatedTablesForSingle(expense, expense.TenantId);
try
{
await _expenseCache.AddExpenseToCacheAsync(expenseCache);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurd while storing expense related table in cahce");
return null;
}
return expenseCache;
}
public async Task AddExpensesListToCache(List<Expenses> expenses, Guid tenantId)
{
var expensesCache = await GetAllExpnesRelatedTablesForList(expenses, tenantId);
try
{
await _expenseCache.AddExpensesListToCacheAsync(expensesCache);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while saving the list of expenses to Cache");
}
}
public async Task<(int totalPages, long totalCount, List<ExpenseDetailsMongoDB>? expenseList)> GetExpenseListAsync(Guid tenantId, Guid loggedInEmployeeId, bool viewAll,
bool viewSelf, int pageNumber, int pageSize, ExpensesFilter? filter, string? searchString)
{
try
{
var (totalPages, totalCount, expenseList) = await _expenseCache.GetExpenseListFromCacheAsync(tenantId, loggedInEmployeeId, viewAll, viewSelf, pageNumber, pageSize, filter, searchString);
if (expenseList.Any())
{
return (totalPages, totalCount, expenseList);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while fetching the list of expenses to Cache");
}
return (0, 0, null);
}
public async Task<ExpenseDetailsMongoDB?> GetExpenseDetailsById(Guid id, Guid tenantId)
{
try
{
var response = await _expenseCache.GetExpenseDetailsByIdAsync(id, tenantId);
if (response != null && response.Id != string.Empty)
{
return response;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while fetching expense details from cache");
}
return null;
}
public async Task ReplaceExpenseAsync(Expenses expense)
{
bool response = false;
try
{
response = await _expenseCache.DeleteExpenseFromCacheAsync(expense.Id, expense.TenantId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while deleting expense from cache");
}
if (response)
{
await AddExpenseByObjectAsync(expense);
}
}
public async Task DeleteExpenseAsync(Guid id, Guid tenantId)
{
try
{
var response = await _expenseCache.DeleteExpenseFromCacheAsync(id, tenantId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while deleting expense from cache");
}
}
#endregion
#region ======================================================= Report Cache =======================================================
public async Task<List<ProjectReportEmailMongoDB>?> GetProjectReportMail(bool IsSend)
{
try
{
var response = await _reportCache.GetProjectReportMailFromCache(IsSend);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while fetching project report mail bodys");
return null;
}
}
public async Task AddProjectReportMail(ProjectReportEmailMongoDB report)
{
try
{
await _reportCache.AddProjectReportMailToCache(report);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while adding project report mail bodys");
}
}
#endregion
#region ======================================================= Helper Functions =======================================================
private async Task<List<ExpenseDetailsMongoDB>> GetAllExpnesRelatedTablesForList(List<Expenses> model, Guid tenantId)
{
List<ExpenseDetailsMongoDB> expenseList = new List<ExpenseDetailsMongoDB>();
var expenseIds = model.Select(m => m.Id).ToList();
var projectIds = model.Select(m => m.ProjectId).ToList();
var statusIds = model.Select(m => m.StatusId).ToList();
var expensesTypeIds = model.Select(m => m.ExpensesTypeId).ToList();
var paymentModeIds = model.Select(m => m.PaymentModeId).ToList();
var createdByIds = model.Select(m => m.CreatedById).ToList();
var reviewedByIds = model.Select(m => m.ReviewedById).ToList();
var approvedByIds = model.Select(m => m.ApprovedById).ToList();
var processedByIds = model.Select(m => m.ProcessedById).ToList();
var paidByIds = model.Select(m => m.PaidById).ToList();
var projectTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Projects.AsNoTracking().Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
});
var paidByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => paidByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
});
var createdByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => createdByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
});
var reviewedByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => reviewedByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
});
var approvedByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => approvedByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
});
var processedByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => processedByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync();
});
var expenseTypeTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.ExpensesTypeMaster.AsNoTracking().Where(et => expensesTypeIds.Contains(et.Id) && et.TenantId == tenantId).ToListAsync();
});
var paymentModeTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.PaymentModeMatser.AsNoTracking().Where(pm => paymentModeIds.Contains(pm.Id) && pm.TenantId == tenantId).ToListAsync();
});
var statusMappingTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.ExpensesStatusMapping
.Include(s => s.Status)
.Include(s => s.NextStatus)
.AsNoTracking()
.Where(es => statusIds.Contains(es.StatusId) && es.Status != null)
.GroupBy(s => s.StatusId)
.Select(g => new
{
StatusId = g.Key,
Status = g.Select(s => s.Status).FirstOrDefault(),
NextStatus = g.Select(s => s.NextStatus).OrderBy(s => s!.Name).ToList()
}).ToListAsync();
});
var statusTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.ExpensesStatusMaster
.AsNoTracking()
.Where(es => statusIds.Contains(es.Id))
.ToListAsync();
});
var billAttachmentsTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.BillAttachments
.Include(ba => ba.Document)
.AsNoTracking()
.Where(ba => expenseIds.Contains(ba.ExpensesId) && ba.Document != null)
.GroupBy(ba => ba.ExpensesId)
.Select(g => new
{
ExpensesId = g.Key,
Documents = g.Select(ba => new DocumentMongoDB
{
DocumentId = ba.Document!.Id.ToString(),
FileName = ba.Document.FileName,
ContentType = ba.Document.ContentType,
S3Key = ba.Document.S3Key,
ThumbS3Key = ba.Document.ThumbS3Key ?? ba.Document.S3Key
}).ToList()
})
.ToListAsync();
});
// Await all prerequisite checks at once.
await Task.WhenAll(projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, createdByTask, reviewedByTask, approvedByTask,
processedByTask, statusTask, billAttachmentsTask);
var projects = projectTask.Result;
var expenseTypes = expenseTypeTask.Result;
var paymentModes = paymentModeTask.Result;
var statusMappings = statusMappingTask.Result;
var paidBys = paidByTask.Result;
var createdBys = createdByTask.Result;
var reviewedBys = reviewedByTask.Result;
var approvedBys = approvedByTask.Result;
var processedBy = processedByTask.Result;
var billAttachments = billAttachmentsTask.Result;
expenseList = model.Select(m =>
{
var response = _mapper.Map<ExpenseDetailsMongoDB>(m);
response.Project = projects.Where(p => p.Id == m.ProjectId).Select(p => _mapper.Map<ProjectBasicMongoDB>(p)).FirstOrDefault() ?? new ProjectBasicMongoDB();
response.PaidBy = paidBys.Where(p => p.Id == m.PaidById).Select(p => _mapper.Map<BasicEmployeeMongoDB>(p)).FirstOrDefault() ?? new BasicEmployeeMongoDB();
response.CreatedBy = createdBys.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map<BasicEmployeeMongoDB>(e)).FirstOrDefault() ?? new BasicEmployeeMongoDB();
response.ReviewedBy = reviewedBys.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map<BasicEmployeeMongoDB>(e)).FirstOrDefault();
response.ApprovedBy = approvedBys.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map<BasicEmployeeMongoDB>(e)).FirstOrDefault();
response.ProcessedBy = processedBy.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map<BasicEmployeeMongoDB>(e)).FirstOrDefault();
response.Status = statusMappings.Where(s => s.StatusId == m.StatusId).Select(s => _mapper.Map<ExpensesStatusMasterMongoDB>(s.Status)).FirstOrDefault() ?? new ExpensesStatusMasterMongoDB();
if (response.Status.Id == string.Empty)
{
var status = statusTask.Result;
response.Status = status.Where(s => s.Id == m.StatusId).Select(s => _mapper.Map<ExpensesStatusMasterMongoDB>(s)).FirstOrDefault() ?? new ExpensesStatusMasterMongoDB();
}
response.NextStatus = statusMappings.Where(s => s.StatusId == m.StatusId).Select(s => _mapper.Map<List<ExpensesStatusMasterMongoDB>>(s.NextStatus)).FirstOrDefault() ?? new List<ExpensesStatusMasterMongoDB>();
response.PaymentMode = paymentModes.Where(pm => pm.Id == m.PaymentModeId).Select(pm => _mapper.Map<PaymentModeMatserMongoDB>(pm)).FirstOrDefault() ?? new PaymentModeMatserMongoDB();
response.ExpensesType = expenseTypes.Where(et => et.Id == m.ExpensesTypeId).Select(et => _mapper.Map<ExpensesTypeMasterMongoDB>(et)).FirstOrDefault() ?? new ExpensesTypeMasterMongoDB();
response.Documents = billAttachments.Where(ba => ba.ExpensesId == m.Id).Select(ba => ba.Documents).FirstOrDefault() ?? new List<DocumentMongoDB>();
return response;
}).ToList();
return expenseList;
}
private async Task<ExpenseDetailsMongoDB> GetAllExpnesRelatedTablesForSingle(Expenses model, Guid tenantId)
{
var projectTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == model.ProjectId && p.TenantId == tenantId);
});
var paidByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.PaidById && e.TenantId == tenantId);
});
var createdByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.CreatedById && e.TenantId == tenantId);
});
var reviewedByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ReviewedById && e.TenantId == tenantId);
});
var approvedByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ApprovedById && e.TenantId == tenantId);
});
var processedByTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ProcessedById && e.TenantId == tenantId);
});
var expenseTypeTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.ExpensesTypeMaster.AsNoTracking().FirstOrDefaultAsync(et => et.Id == model.ExpensesTypeId && et.TenantId == tenantId);
});
var paymentModeTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.PaymentModeMatser.AsNoTracking().FirstOrDefaultAsync(pm => pm.Id == model.PaymentModeId && pm.TenantId == tenantId);
});
var statusMappingTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.ExpensesStatusMapping
.Include(s => s.Status)
.Include(s => s.NextStatus)
.AsNoTracking()
.Where(es => es.StatusId == model.StatusId && es.Status != null)
.GroupBy(s => s.StatusId)
.Select(g => new
{
StatusId = g.Key,
Status = g.Select(s => s.Status).FirstOrDefault(),
NextStatus = g.Select(s => s.NextStatus).OrderBy(s => s!.Name).ToList()
}).FirstOrDefaultAsync();
});
var statusTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.ExpensesStatusMaster
.AsNoTracking()
.FirstOrDefaultAsync(es => es.Id == model.StatusId);
});
var billAttachmentsTask = Task.Run(async () =>
{
await using var dbContext = await _dbContextFactory.CreateDbContextAsync();
return await dbContext.BillAttachments
.Include(ba => ba.Document)
.AsNoTracking()
.Where(ba => ba.ExpensesId == model.Id && ba.Document != null)
.GroupBy(ba => ba.ExpensesId)
.Select(g => new
{
ExpensesId = g.Key,
Documents = g.Select(ba => new DocumentMongoDB
{
DocumentId = ba.Document!.Id.ToString(),
FileName = ba.Document.FileName,
ContentType = ba.Document.ContentType,
S3Key = ba.Document.S3Key,
ThumbS3Key = ba.Document.ThumbS3Key ?? ba.Document.S3Key
}).ToList()
})
.FirstOrDefaultAsync();
});
// Await all prerequisite checks at once.
await Task.WhenAll(projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, createdByTask, reviewedByTask, approvedByTask,
processedByTask, statusTask, billAttachmentsTask);
var project = projectTask.Result;
var expenseType = expenseTypeTask.Result;
var paymentMode = paymentModeTask.Result;
var statusMapping = statusMappingTask.Result;
var paidBy = paidByTask.Result;
var createdBy = createdByTask.Result;
var reviewedBy = reviewedByTask.Result;
var approvedBy = approvedByTask.Result;
var processedBy = processedByTask.Result;
var billAttachment = billAttachmentsTask.Result;
var response = _mapper.Map<ExpenseDetailsMongoDB>(model);
response.Project = _mapper.Map<ProjectBasicMongoDB>(project);
response.PaidBy = _mapper.Map<BasicEmployeeMongoDB>(paidBy);
response.CreatedBy = _mapper.Map<BasicEmployeeMongoDB>(createdBy);
response.ReviewedBy = _mapper.Map<BasicEmployeeMongoDB>(reviewedBy);
response.ApprovedBy = _mapper.Map<BasicEmployeeMongoDB>(approvedBy);
response.ProcessedBy = _mapper.Map<BasicEmployeeMongoDB>(processedBy);
if (statusMapping != null)
{
response.Status = _mapper.Map<ExpensesStatusMasterMongoDB>(statusMapping.Status);
response.NextStatus = _mapper.Map<List<ExpensesStatusMasterMongoDB>>(statusMapping.NextStatus);
}
if (response.Status == null)
{
var status = statusTask.Result;
response.Status = _mapper.Map<ExpensesStatusMasterMongoDB>(status);
}
response.PaymentMode = _mapper.Map<PaymentModeMatserMongoDB>(paymentMode);
response.ExpensesType = _mapper.Map<ExpensesTypeMasterMongoDB>(expenseType);
if (billAttachment != null) response.Documents = billAttachment.Documents;
return response;
}
#endregion
}
}