Refactored the function to add project in cache and added auto Mapper
This commit is contained in:
parent
d27cdee72d
commit
8bb8b3643f
@ -24,132 +24,14 @@ namespace Marco.Pms.CacheHelper
|
||||
_projetCollection = mongoDB.GetCollection<ProjectMongoDB>("ProjectDetails");
|
||||
_taskCollection = mongoDB.GetCollection<WorkItemMongoDB>("WorkItemDetails");
|
||||
}
|
||||
public async Task AddProjectDetailsToCache(Project project)
|
||||
|
||||
public async Task AddProjectDetailsToCache(ProjectMongoDB projectDetails)
|
||||
{
|
||||
//_logger.LogInfo("[AddProjectDetails] Initiated for ProjectId: {ProjectId}", project.Id);
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
// Get project status
|
||||
var status = await _context.StatusMasters
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(s => s.Id == project.ProjectStatusId);
|
||||
|
||||
projectDetails.ProjectStatus = new StatusMasterMongoDB
|
||||
{
|
||||
Id = status?.Id.ToString(),
|
||||
Status = status?.Status
|
||||
};
|
||||
|
||||
// Get project team size
|
||||
var teamSize = await _context.ProjectAllocations
|
||||
.AsNoTracking()
|
||||
.CountAsync(pa => pa.ProjectId == project.Id && pa.IsActive);
|
||||
|
||||
projectDetails.TeamSize = teamSize;
|
||||
|
||||
// Fetch related infrastructure in parallel
|
||||
var buildings = await _context.Buildings
|
||||
.AsNoTracking()
|
||||
.Where(b => b.ProjectId == project.Id)
|
||||
.ToListAsync();
|
||||
var buildingIds = buildings.Select(b => b.Id).ToList();
|
||||
|
||||
var floors = await _context.Floor
|
||||
.AsNoTracking()
|
||||
.Where(f => buildingIds.Contains(f.BuildingId))
|
||||
.ToListAsync();
|
||||
|
||||
var floorIds = floors.Select(f => f.Id).ToList();
|
||||
|
||||
var workAreas = await _context.WorkAreas
|
||||
.AsNoTracking()
|
||||
.Where(wa => floorIds.Contains(wa.FloorId))
|
||||
.ToListAsync();
|
||||
var workAreaIds = workAreas.Select(wa => wa.Id).ToList();
|
||||
|
||||
var workItems = await _context.WorkItems
|
||||
.Where(wi => workAreaIds.Contains(wi.WorkAreaId))
|
||||
.ToListAsync();
|
||||
|
||||
double totalPlannedWork = 0, totalCompletedWork = 0;
|
||||
|
||||
var buildingMongoList = new List<BuildingMongoDB>();
|
||||
|
||||
foreach (var building in buildings)
|
||||
{
|
||||
double buildingPlanned = 0, buildingCompleted = 0;
|
||||
var buildingFloors = floors.Where(f => f.BuildingId == building.Id).ToList();
|
||||
|
||||
var floorMongoList = new List<FloorMongoDB>();
|
||||
foreach (var floor in buildingFloors)
|
||||
{
|
||||
double floorPlanned = 0, floorCompleted = 0;
|
||||
var floorWorkAreas = workAreas.Where(wa => wa.FloorId == floor.Id).ToList();
|
||||
|
||||
var workAreaMongoList = new List<WorkAreaMongoDB>();
|
||||
foreach (var wa in floorWorkAreas)
|
||||
{
|
||||
var items = workItems.Where(wi => wi.WorkAreaId == wa.Id).ToList();
|
||||
double waPlanned = items.Sum(wi => wi.PlannedWork);
|
||||
double waCompleted = items.Sum(wi => wi.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;
|
||||
|
||||
await _projetCollection.InsertOneAsync(projectDetails);
|
||||
}
|
||||
public async Task AddProjectDetailsListToCache(List<ProjectMongoDB> projectDetailsList)
|
||||
{
|
||||
await _projetCollection.InsertManyAsync(projectDetailsList);
|
||||
//_logger.LogInfo("[AddProjectDetails] Project details inserted in MongoDB for ProjectId: {ProjectId}", project.Id);
|
||||
}
|
||||
public async Task<bool> UpdateProjectDetailsOnlyToCache(Project project)
|
||||
@ -218,7 +100,7 @@ namespace Marco.Pms.CacheHelper
|
||||
//_logger.LogInfo("Successfully fetched project details (excluding Buildings) for ProjectId: {ProjectId}", projectId);
|
||||
return project;
|
||||
}
|
||||
public async Task<List<ProjectMongoDB>?> GetProjectDetailsListFromCache(List<Guid> projectIds)
|
||||
public async Task<List<ProjectMongoDB>> GetProjectDetailsListFromCache(List<Guid> projectIds)
|
||||
{
|
||||
List<string> stringProjectIds = projectIds.Select(p => p.ToString()).ToList();
|
||||
var filter = Builders<ProjectMongoDB>.Filter.In(p => p.Id, stringProjectIds);
|
||||
@ -229,6 +111,9 @@ namespace Marco.Pms.CacheHelper
|
||||
.ToListAsync();
|
||||
return projects;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------- Project InfraStructure -------------------------------------------------------
|
||||
|
||||
public async Task AddBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId)
|
||||
{
|
||||
var stringProjectId = projectId.ToString();
|
||||
|
@ -1,8 +1,8 @@
|
||||
using System.Globalization;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Model.AttendanceModule;
|
||||
using Marco.Pms.Model.Dtos.Attendance;
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Entitlements;
|
||||
using Marco.Pms.Model.Mapper;
|
||||
using Marco.Pms.Model.Projects;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Globalization;
|
||||
using Document = Marco.Pms.Model.DocumentManager.Document;
|
||||
|
||||
namespace MarcoBMS.Services.Controllers
|
||||
@ -61,7 +62,13 @@ namespace MarcoBMS.Services.Controllers
|
||||
{
|
||||
Guid TenantId = GetTenantId();
|
||||
|
||||
List<AttendanceLog> lstAttendance = await _context.AttendanceLogs.Include(a => a.Document).Include(a => a.Employee).Include(a => a.UpdatedByEmployee).Where(c => c.AttendanceId == attendanceid && c.TenantId == TenantId).ToListAsync();
|
||||
List<AttendanceLog> lstAttendance = await _context.AttendanceLogs
|
||||
.Include(a => a.Document)
|
||||
.Include(a => a.Employee)
|
||||
.Include(a => a.UpdatedByEmployee)
|
||||
.Where(c => c.AttendanceId == attendanceid && c.TenantId == TenantId)
|
||||
.ToListAsync();
|
||||
|
||||
List<AttendanceLogVM> attendanceLogVMs = new List<AttendanceLogVM>();
|
||||
foreach (var attendanceLog in lstAttendance)
|
||||
{
|
||||
@ -139,9 +146,9 @@ namespace MarcoBMS.Services.Controllers
|
||||
{
|
||||
Guid TenantId = GetTenantId();
|
||||
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var hasTeamAttendancePermission = await _permission.HasPermission(new Guid("915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"), LoggedInEmployee.Id);
|
||||
var hasSelfAttendancePermission = await _permission.HasPermission(new Guid("ccb0589f-712b-43de-92ed-5b6088e7dc4e"), LoggedInEmployee.Id);
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId.ToString());
|
||||
var hasTeamAttendancePermission = await _permission.HasPermission(PermissionsMaster.TeamAttendance, LoggedInEmployee.Id);
|
||||
var hasSelfAttendancePermission = await _permission.HasPermission(PermissionsMaster.SelfAttendance, LoggedInEmployee.Id);
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId);
|
||||
|
||||
if (!hasProjectPermission)
|
||||
{
|
||||
@ -255,9 +262,9 @@ namespace MarcoBMS.Services.Controllers
|
||||
{
|
||||
Guid TenantId = GetTenantId();
|
||||
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var hasTeamAttendancePermission = await _permission.HasPermission(new Guid("915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"), LoggedInEmployee.Id);
|
||||
var hasSelfAttendancePermission = await _permission.HasPermission(new Guid("ccb0589f-712b-43de-92ed-5b6088e7dc4e"), LoggedInEmployee.Id);
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId.ToString());
|
||||
var hasTeamAttendancePermission = await _permission.HasPermission(PermissionsMaster.TeamAttendance, LoggedInEmployee.Id);
|
||||
var hasSelfAttendancePermission = await _permission.HasPermission(PermissionsMaster.SelfAttendance, LoggedInEmployee.Id);
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId);
|
||||
|
||||
if (!hasProjectPermission)
|
||||
{
|
||||
@ -361,7 +368,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
Guid TenantId = GetTenantId();
|
||||
Employee LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var result = new List<EmployeeAttendanceVM>();
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId.ToString());
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId);
|
||||
|
||||
if (!hasProjectPermission)
|
||||
{
|
||||
@ -371,7 +378,6 @@ namespace MarcoBMS.Services.Controllers
|
||||
|
||||
List<Attendance> lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE && c.TenantId == TenantId).ToListAsync();
|
||||
|
||||
|
||||
List<ProjectAllocation> projectteam = await _projectsHelper.GetTeamByProject(TenantId, projectId, true);
|
||||
var idList = projectteam.Select(p => p.EmployeeId).ToList();
|
||||
var jobRole = await _context.JobRoles.ToListAsync();
|
||||
|
@ -373,7 +373,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
|
||||
// Step 2: Permission check
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
bool hasAssigned = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.ToString());
|
||||
bool hasAssigned = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId);
|
||||
|
||||
if (!hasAssigned)
|
||||
{
|
||||
|
@ -1,6 +1,4 @@
|
||||
using System.Data;
|
||||
using System.Net;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Model.Dtos.Attendance;
|
||||
using Marco.Pms.Model.Dtos.Employees;
|
||||
using Marco.Pms.Model.Employees;
|
||||
@ -18,6 +16,8 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Data;
|
||||
using System.Net;
|
||||
|
||||
namespace MarcoBMS.Services.Controllers
|
||||
{
|
||||
@ -119,8 +119,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
loggedInEmployee.Id, projectid ?? Guid.Empty, ShowInactive);
|
||||
|
||||
// Step 3: Fetch project access and permissions
|
||||
List<Project> projects = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee);
|
||||
var projectIds = projects.Select(p => p.Id).ToList();
|
||||
var projectIds = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee);
|
||||
|
||||
var hasViewAllEmployeesPermission = await _permission.HasPermission(PermissionsMaster.ViewAllEmployees, loggedInEmployee.Id);
|
||||
var hasViewTeamMembersPermission = await _permission.HasPermission(PermissionsMaster.ViewTeamMembers, loggedInEmployee.Id);
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Model.Activities;
|
||||
using Marco.Pms.Model.Dtos.DocumentManager;
|
||||
using Marco.Pms.Model.Employees;
|
||||
@ -13,6 +12,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Marco.Pms.Services.Controllers
|
||||
{
|
||||
@ -54,7 +54,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
}
|
||||
|
||||
// Step 2: Check project access permission
|
||||
var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.ToString());
|
||||
var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId);
|
||||
if (!hasPermission)
|
||||
{
|
||||
_logger.LogWarning("[GetImageList] Access denied for EmployeeId: {EmployeeId} on ProjectId: {ProjectId}", loggedInEmployee.Id, projectId);
|
||||
|
@ -1,10 +1,10 @@
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using AutoMapper;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Model.Activities;
|
||||
using Marco.Pms.Model.Dtos.Project;
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Entitlements;
|
||||
using Marco.Pms.Model.Mapper;
|
||||
using Marco.Pms.Model.Master;
|
||||
using Marco.Pms.Model.MongoDBModels;
|
||||
using Marco.Pms.Model.Projects;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
@ -36,16 +36,12 @@ namespace MarcoBMS.Services.Controllers
|
||||
private readonly IHubContext<MarcoHub> _signalR;
|
||||
private readonly PermissionServices _permission;
|
||||
private readonly CacheUpdateHelper _cache;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly Guid ViewProjects;
|
||||
private readonly Guid ManageProject;
|
||||
private readonly Guid ViewInfra;
|
||||
private readonly Guid ManageInfra;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly Guid tenantId;
|
||||
|
||||
|
||||
public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, ProjectsHelper projectHelper,
|
||||
IHubContext<MarcoHub> signalR, PermissionServices permission, CacheUpdateHelper cache, IServiceScopeFactory serviceScopeFactory)
|
||||
IHubContext<MarcoHub> signalR, PermissionServices permission, CacheUpdateHelper cache, IMapper mapper)
|
||||
{
|
||||
_context = context;
|
||||
_userHelper = userHelper;
|
||||
@ -55,16 +51,12 @@ namespace MarcoBMS.Services.Controllers
|
||||
_signalR = signalR;
|
||||
_cache = cache;
|
||||
_permission = permission;
|
||||
ViewProjects = Guid.Parse("6ea44136-987e-44ba-9e5d-1cf8f5837ebc");
|
||||
ManageProject = Guid.Parse("172fc9b6-755b-4f62-ab26-55c34a330614");
|
||||
ViewInfra = Guid.Parse("8d7cc6e3-9147-41f7-aaa7-fa507e450bd4");
|
||||
ManageInfra = Guid.Parse("f2aee20a-b754-4537-8166-f9507b44585b");
|
||||
_mapper = mapper;
|
||||
tenantId = _userHelper.GetTenantId();
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
[HttpGet("list/basic")]
|
||||
public async Task<IActionResult> GetAllProjects()
|
||||
[HttpGet("list/basic1")]
|
||||
public async Task<IActionResult> GetAllProjects1()
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
@ -84,31 +76,113 @@ namespace MarcoBMS.Services.Controllers
|
||||
return Unauthorized(ApiResponse<object>.ErrorResponse("Employee not found.", null, 401));
|
||||
}
|
||||
|
||||
List<ProjectInfoVM> response = new List<ProjectInfoVM>();
|
||||
List<Guid> projectIds = await _projectsHelper.GetMyProjects(tenantId, LoggedInEmployee);
|
||||
|
||||
List<Project> projects = await _projectsHelper.GetMyProjects(tenantId, LoggedInEmployee);
|
||||
List<ProjectMongoDB>? projectsDetails = await _cache.GetProjectDetailsList(projectIds);
|
||||
if (projectsDetails == null)
|
||||
{
|
||||
List<Project> projects = await _context.Projects.Where(p => projectIds.Contains(p.Id)).ToListAsync();
|
||||
//using (var scope = _serviceScopeFactory.CreateScope())
|
||||
//{
|
||||
// var cacheHelper = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();
|
||||
|
||||
|
||||
// 4. Project projection to ProjectInfoVM
|
||||
// This part is already quite efficient.
|
||||
// Ensure ToProjectInfoVMFromProject() is also optimized and doesn't perform N+1 queries.
|
||||
// If ProjectInfoVM only needs a subset of Project properties,
|
||||
// you can use a LINQ Select directly on the IQueryable before ToListAsync()
|
||||
// to fetch only the required columns from the database.
|
||||
List<ProjectInfoVM> response = projects
|
||||
.Select(project => project.ToProjectInfoVMFromProject())
|
||||
.ToList();
|
||||
|
||||
|
||||
//List<ProjectInfoVM> response = new List<ProjectInfoVM>();
|
||||
|
||||
//foreach (var project in projects)
|
||||
//{
|
||||
// response.Add(project.ToProjectInfoVMFromProject());
|
||||
//}
|
||||
//}
|
||||
foreach (var project in projects)
|
||||
{
|
||||
await _cache.AddProjectDetails(project);
|
||||
}
|
||||
response = projects.Select(p => _mapper.Map<ProjectInfoVM>(p)).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
response = projectsDetails.Select(p => _mapper.Map<ProjectInfoVM>(p)).ToList();
|
||||
}
|
||||
|
||||
return Ok(ApiResponse<object>.SuccessResponse(response, "Success.", 200));
|
||||
}
|
||||
|
||||
[HttpGet("list/basic")]
|
||||
public async Task<IActionResult> GetAllProjects() // Renamed for clarity
|
||||
{
|
||||
// Step 1: Get the current user
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
if (loggedInEmployee == null)
|
||||
{
|
||||
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized", "User could not be identified.", 401));
|
||||
}
|
||||
|
||||
_logger.LogInfo("Basic project list requested by EmployeeId {EmployeeId}", loggedInEmployee.Id);
|
||||
|
||||
// Step 2: Get the list of project IDs the user has access to
|
||||
Guid tenantId = _userHelper.GetTenantId(); // Assuming this is still needed by the helper
|
||||
List<Guid> accessibleProjectIds = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee);
|
||||
|
||||
if (accessibleProjectIds == null || !accessibleProjectIds.Any())
|
||||
{
|
||||
_logger.LogInfo("No accessible projects found for EmployeeId {EmployeeId}", loggedInEmployee.Id);
|
||||
return Ok(ApiResponse<List<ProjectInfoVM>>.SuccessResponse(new List<ProjectInfoVM>(), "Success.", 200));
|
||||
}
|
||||
|
||||
// Step 3: Fetch project ViewModels using the optimized, cache-aware helper
|
||||
var projectVMs = await GetProjectInfosByIdsAsync(accessibleProjectIds);
|
||||
|
||||
// Step 4: Return the final list
|
||||
_logger.LogInfo("Successfully returned {ProjectCount} projects for EmployeeId {EmployeeId}", projectVMs.Count, loggedInEmployee.Id);
|
||||
return Ok(ApiResponse<List<ProjectInfoVM>>.SuccessResponse(projectVMs, $"{projectVMs.Count} records of project fetchd successfully", 200));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a list of ProjectInfoVMs by their IDs, using an efficient partial cache-hit strategy.
|
||||
/// It fetches what it can from the cache (as ProjectMongoDB), gets the rest from the
|
||||
/// database (as Project), updates the cache, and returns a unified list of ViewModels.
|
||||
/// </summary>
|
||||
/// <param name="projectIds">The list of project IDs to retrieve.</param>
|
||||
/// <returns>A list of ProjectInfoVMs.</returns>
|
||||
private async Task<List<ProjectInfoVM>> GetProjectInfosByIdsAsync(List<Guid> projectIds)
|
||||
{
|
||||
// --- Step 1: Fetch from Cache ---
|
||||
// The cache returns a list of MongoDB documents for the projects it found.
|
||||
var cachedMongoDocs = await _cache.GetProjectDetailsList(projectIds) ?? new List<ProjectMongoDB>();
|
||||
var finalViewModels = _mapper.Map<List<ProjectInfoVM>>(cachedMongoDocs);
|
||||
|
||||
_logger.LogDebug("Cache hit for {CacheCount} of {TotalCount} projects.", finalViewModels.Count, projectIds.Count);
|
||||
|
||||
// --- Step 2: Identify Missing Projects ---
|
||||
// If we found everything in the cache, we can return early.
|
||||
if (finalViewModels.Count == projectIds.Count)
|
||||
{
|
||||
return finalViewModels;
|
||||
}
|
||||
|
||||
var cachedIds = cachedMongoDocs.Select(p => p.Id).ToHashSet(); // Assuming ProjectMongoDB has an Id
|
||||
var missingIds = projectIds.Where(id => !cachedIds.Contains(id.ToString())).ToList();
|
||||
|
||||
// --- Step 3: Fetch Missing from Database ---
|
||||
if (missingIds.Any())
|
||||
{
|
||||
_logger.LogDebug("Cache miss for {MissingCount} projects. Querying database.", missingIds.Count);
|
||||
|
||||
var projectsFromDb = await _context.Projects
|
||||
.Where(p => missingIds.Contains(p.Id))
|
||||
.AsNoTracking() // Use AsNoTracking for read-only query performance
|
||||
.ToListAsync();
|
||||
|
||||
if (projectsFromDb.Any())
|
||||
{
|
||||
// Map the newly fetched projects (from SQL) to their ViewModel
|
||||
var vmsFromDb = _mapper.Map<List<ProjectInfoVM>>(projectsFromDb);
|
||||
finalViewModels.AddRange(vmsFromDb);
|
||||
|
||||
// --- Step 4: Update Cache with Missing Items in a new scope ---
|
||||
_logger.LogDebug("Updating cache with {DbCount} newly fetched projects.", projectsFromDb.Count);
|
||||
await _cache.AddProjectDetailsList(projectsFromDb);
|
||||
}
|
||||
}
|
||||
|
||||
return finalViewModels;
|
||||
}
|
||||
|
||||
[HttpGet("list")]
|
||||
public async Task<IActionResult> GetAll()
|
||||
{
|
||||
@ -139,39 +213,63 @@ namespace MarcoBMS.Services.Controllers
|
||||
// projects = await _context.Projects.Where(c => projectsId.Contains(c.Id.ToString()) && c.TenantId == tenantId).ToListAsync();
|
||||
//}
|
||||
|
||||
List<Project> projects = await _projectsHelper.GetMyProjects(tenantId, LoggedInEmployee);
|
||||
|
||||
|
||||
|
||||
|
||||
//List<Project> projects = await _projectsHelper.GetMyProjects(tenantId, LoggedInEmployee);
|
||||
////List<Project> projects = new List<Project>();
|
||||
///
|
||||
List<ProjectListVM> response = new List<ProjectListVM>();
|
||||
foreach (var project in projects)
|
||||
List<Guid> projectIds = await _projectsHelper.GetMyProjects(tenantId, LoggedInEmployee);
|
||||
|
||||
var projectsDetails = await _cache.GetProjectDetailsList(projectIds);
|
||||
if (projectsDetails == null)
|
||||
{
|
||||
var result = project.ToProjectListVMFromProject();
|
||||
var team = await _context.ProjectAllocations.Where(p => p.TenantId == tenantId && p.ProjectId == project.Id && p.IsActive == true).ToListAsync();
|
||||
List<Project> projects = await _context.Projects.Where(p => projectIds.Contains(p.Id)).ToListAsync();
|
||||
|
||||
result.TeamSize = team.Count();
|
||||
var teams = await _context.ProjectAllocations.Where(p => p.TenantId == tenantId && projectIds.Contains(p.ProjectId) && p.IsActive == true).ToListAsync();
|
||||
|
||||
List<Building> buildings = await _context.Buildings.Where(b => b.ProjectId == project.Id && b.TenantId == tenantId).ToListAsync();
|
||||
List<Guid> idList = buildings.Select(b => b.Id).ToList();
|
||||
|
||||
List<Floor> floors = await _context.Floor.Where(f => idList.Contains(f.BuildingId) && f.TenantId == tenantId).ToListAsync();
|
||||
idList = floors.Select(f => f.Id).ToList();
|
||||
List<Building> allBuildings = await _context.Buildings.Where(b => projectIds.Contains(b.ProjectId) && b.TenantId == tenantId).ToListAsync();
|
||||
List<Guid> idList = allBuildings.Select(b => b.Id).ToList();
|
||||
|
||||
List<WorkArea> workAreas = await _context.WorkAreas.Where(a => idList.Contains(a.FloorId) && a.TenantId == tenantId).ToListAsync();
|
||||
idList = workAreas.Select(a => a.Id).ToList();
|
||||
List<Floor> allFloors = await _context.Floor.Where(f => idList.Contains(f.BuildingId) && f.TenantId == tenantId).ToListAsync();
|
||||
idList = allFloors.Select(f => f.Id).ToList();
|
||||
|
||||
List<WorkItem> workItems = await _context.WorkItems.Where(i => idList.Contains(i.WorkAreaId) && i.TenantId == tenantId).Include(i => i.ActivityMaster).ToListAsync();
|
||||
double completedTask = 0;
|
||||
double plannedTask = 0;
|
||||
foreach (var workItem in workItems)
|
||||
List<WorkArea> allWorkAreas = await _context.WorkAreas.Where(a => idList.Contains(a.FloorId) && a.TenantId == tenantId).ToListAsync();
|
||||
idList = allWorkAreas.Select(a => a.Id).ToList();
|
||||
|
||||
List<WorkItem> allWorkItems = await _context.WorkItems.Where(i => idList.Contains(i.WorkAreaId) && i.TenantId == tenantId).Include(i => i.ActivityMaster).ToListAsync();
|
||||
|
||||
foreach (var project in projects)
|
||||
{
|
||||
completedTask += workItem.CompletedWork;
|
||||
plannedTask += workItem.PlannedWork;
|
||||
var result = _mapper.Map<ProjectListVM>(project);
|
||||
var team = teams.Where(p => p.TenantId == tenantId && p.ProjectId == project.Id && p.IsActive == true).ToList();
|
||||
|
||||
result.TeamSize = team.Count();
|
||||
|
||||
List<Building> buildings = allBuildings.Where(b => b.ProjectId == project.Id && b.TenantId == tenantId).ToList();
|
||||
idList = buildings.Select(b => b.Id).ToList();
|
||||
|
||||
List<Floor> floors = allFloors.Where(f => idList.Contains(f.BuildingId) && f.TenantId == tenantId).ToList();
|
||||
idList = floors.Select(f => f.Id).ToList();
|
||||
|
||||
List<WorkArea> workAreas = allWorkAreas.Where(a => idList.Contains(a.FloorId) && a.TenantId == tenantId).ToList();
|
||||
idList = workAreas.Select(a => a.Id).ToList();
|
||||
|
||||
List<WorkItem> workItems = allWorkItems.Where(i => idList.Contains(i.WorkAreaId) && i.TenantId == tenantId).ToList();
|
||||
double completedTask = 0;
|
||||
double plannedTask = 0;
|
||||
foreach (var workItem in workItems)
|
||||
{
|
||||
completedTask += workItem.CompletedWork;
|
||||
plannedTask += workItem.PlannedWork;
|
||||
}
|
||||
result.PlannedWork = plannedTask;
|
||||
result.CompletedWork = completedTask;
|
||||
response.Add(result);
|
||||
}
|
||||
result.PlannedWork = plannedTask;
|
||||
result.CompletedWork = completedTask;
|
||||
response.Add(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
response = projectsDetails.Select(p => _mapper.Map<ProjectListVM>(p)).ToList();
|
||||
}
|
||||
|
||||
return Ok(ApiResponse<object>.SuccessResponse(response, "Success.", 200));
|
||||
@ -215,7 +313,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
_logger.LogInfo("Details requested by EmployeeId: {EmployeeId} for ProjectId: {ProjectId}", loggedInEmployee.Id, id);
|
||||
|
||||
// Step 3: Check global view project permission
|
||||
var hasViewProjectPermission = await _permission.HasPermission(ViewProjects, loggedInEmployee.Id);
|
||||
var hasViewProjectPermission = await _permission.HasPermission(PermissionsMaster.ViewProject, loggedInEmployee.Id);
|
||||
if (!hasViewProjectPermission)
|
||||
{
|
||||
_logger.LogWarning("ViewProjects permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id);
|
||||
@ -223,7 +321,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
}
|
||||
|
||||
// Step 4: Check permission for this specific project
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, id.ToString());
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, id);
|
||||
if (!hasProjectPermission)
|
||||
{
|
||||
_logger.LogWarning("Project-specific access denied. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployee.Id, id);
|
||||
@ -238,7 +336,9 @@ namespace MarcoBMS.Services.Controllers
|
||||
var project = await _context.Projects
|
||||
.Include(c => c.ProjectStatus)
|
||||
.FirstOrDefaultAsync(c => c.TenantId == tenantId && c.Id == id);
|
||||
projectVM = GetProjectViewModel(project);
|
||||
|
||||
projectVM = _mapper.Map<ProjectVM>(project);
|
||||
|
||||
if (project != null)
|
||||
{
|
||||
await _cache.AddProjectDetails(project);
|
||||
@ -246,23 +346,28 @@ namespace MarcoBMS.Services.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
projectVM = new ProjectVM
|
||||
projectVM = _mapper.Map<ProjectVM>(projectDetails);
|
||||
if (projectVM.ProjectStatus != null)
|
||||
{
|
||||
Id = projectDetails.Id != null ? Guid.Parse(projectDetails.Id) : Guid.Empty,
|
||||
Name = projectDetails.Name,
|
||||
ShortName = projectDetails.ShortName,
|
||||
ProjectAddress = projectDetails.ProjectAddress,
|
||||
StartDate = projectDetails.StartDate,
|
||||
EndDate = projectDetails.EndDate,
|
||||
ContactPerson = projectDetails.ContactPerson,
|
||||
ProjectStatus = new StatusMaster
|
||||
{
|
||||
Id = projectDetails.ProjectStatus?.Id != null ? Guid.Parse(projectDetails.ProjectStatus.Id) : Guid.Empty,
|
||||
Status = projectDetails.ProjectStatus?.Status,
|
||||
TenantId = tenantId
|
||||
}
|
||||
//ProjectStatusId = projectDetails.ProjectStatus?.Id != null ? Guid.Parse(projectDetails.ProjectStatus.Id) : Guid.Empty,
|
||||
};
|
||||
projectVM.ProjectStatus.TenantId = tenantId;
|
||||
}
|
||||
//projectVM = new ProjectVM
|
||||
//{
|
||||
// Id = projectDetails.Id != null ? Guid.Parse(projectDetails.Id) : Guid.Empty,
|
||||
// Name = projectDetails.Name,
|
||||
// ShortName = projectDetails.ShortName,
|
||||
// ProjectAddress = projectDetails.ProjectAddress,
|
||||
// StartDate = projectDetails.StartDate,
|
||||
// EndDate = projectDetails.EndDate,
|
||||
// ContactPerson = projectDetails.ContactPerson,
|
||||
// ProjectStatus = new StatusMaster
|
||||
// {
|
||||
// Id = projectDetails.ProjectStatus?.Id != null ? Guid.Parse(projectDetails.ProjectStatus.Id) : Guid.Empty,
|
||||
// Status = projectDetails.ProjectStatus?.Status,
|
||||
// TenantId = tenantId
|
||||
// }
|
||||
// //ProjectStatusId = projectDetails.ProjectStatus?.Id != null ? Guid.Parse(projectDetails.ProjectStatus.Id) : Guid.Empty,
|
||||
//};
|
||||
}
|
||||
|
||||
if (projectVM == null)
|
||||
@ -277,25 +382,6 @@ namespace MarcoBMS.Services.Controllers
|
||||
return Ok(ApiResponse<object>.SuccessResponse(projectVM, "Project details fetched successfully", 200));
|
||||
}
|
||||
|
||||
private ProjectVM? GetProjectViewModel(Project? project)
|
||||
{
|
||||
if (project == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return new ProjectVM
|
||||
{
|
||||
Id = project.Id,
|
||||
Name = project.Name,
|
||||
ShortName = project.ShortName,
|
||||
StartDate = project.StartDate,
|
||||
EndDate = project.EndDate,
|
||||
ProjectStatus = project.ProjectStatus,
|
||||
ContactPerson = project.ContactPerson,
|
||||
ProjectAddress = project.ProjectAddress,
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("details-old/{id}")]
|
||||
public async Task<IActionResult> DetailsOld([FromRoute] Guid id)
|
||||
{
|
||||
@ -470,7 +556,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
{
|
||||
// These operations do not depend on each other, so they can run in parallel.
|
||||
Task cacheAddDetailsTask = _cache.AddProjectDetails(project);
|
||||
Task cacheClearListTask = _cache.ClearAllProjectIdsByPermissionId(ManageProject);
|
||||
Task cacheClearListTask = _cache.ClearAllProjectIdsByPermissionId(PermissionsMaster.ManageProject);
|
||||
|
||||
var notification = new { LoggedInUserId = loggedInUserId, Keyword = "Create_Project", Response = project.ToProjectDto() };
|
||||
// Send notification only to the relevant group (e.g., users in the same tenant)
|
||||
@ -762,7 +848,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
|
||||
// Step 2: Check project-specific permission
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.ToString());
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId);
|
||||
if (!hasProjectPermission)
|
||||
{
|
||||
_logger.LogWarning("Project access denied for EmployeeId: {EmployeeId} on ProjectId: {ProjectId}", loggedInEmployee.Id, projectId);
|
||||
@ -770,7 +856,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
}
|
||||
|
||||
// Step 3: Check 'ViewInfra' permission
|
||||
var hasViewInfraPermission = await _permission.HasPermission(ViewInfra, loggedInEmployee.Id);
|
||||
var hasViewInfraPermission = await _permission.HasPermission(PermissionsMaster.ViewProjectInfra, loggedInEmployee.Id);
|
||||
if (!hasViewInfraPermission)
|
||||
{
|
||||
_logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id);
|
||||
@ -883,7 +969,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
|
||||
// Step 2: Check if the employee has ViewInfra permission
|
||||
var hasViewInfraPermission = await _permission.HasPermission(ViewInfra, loggedInEmployee.Id);
|
||||
var hasViewInfraPermission = await _permission.HasPermission(PermissionsMaster.ViewProjectInfra, loggedInEmployee.Id);
|
||||
if (!hasViewInfraPermission)
|
||||
{
|
||||
_logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id);
|
||||
|
@ -1,7 +1,9 @@
|
||||
using Marco.Pms.CacheHelper;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Model.MongoDBModels;
|
||||
using Marco.Pms.Model.Projects;
|
||||
using MarcoBMS.Services.Service;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Project = Marco.Pms.Model.Projects.Project;
|
||||
|
||||
namespace Marco.Pms.Services.Helpers
|
||||
@ -10,25 +12,407 @@ namespace Marco.Pms.Services.Helpers
|
||||
{
|
||||
private readonly ProjectCache _projectCache;
|
||||
private readonly EmployeeCache _employeeCache;
|
||||
private readonly ReportCache _reportCache;
|
||||
private readonly ILoggingService _logger;
|
||||
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
|
||||
|
||||
public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache, ILoggingService logger)
|
||||
public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache, ReportCache reportCache, ILoggingService logger,
|
||||
IDbContextFactory<ApplicationDbContext> dbContextFactory)
|
||||
{
|
||||
_projectCache = projectCache;
|
||||
_employeeCache = employeeCache;
|
||||
_reportCache = reportCache;
|
||||
_logger = logger;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
// ------------------------------------ Project Details and Infrastructure Cache ---------------------------------------
|
||||
// ------------------------------------ Project Details Cache ---------------------------------------
|
||||
// Assuming you have access to an IDbContextFactory<YourDbContext> as _dbContextFactory
|
||||
// This is crucial for safe parallel database operations.
|
||||
|
||||
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(project);
|
||||
await _projectCache.AddProjectDetailsToCache(projectDetails);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning("Error occured while adding project {ProjectId} to Cache : {Error}", project.Id, ex.Message);
|
||||
_logger.LogWarning("Error occurred while adding project {ProjectId} to Cache: {Error}", project.Id, ex.Message);
|
||||
}
|
||||
}
|
||||
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.LogWarning("Error occurred while adding project list to Cache: {Error}", ex.Message);
|
||||
}
|
||||
}
|
||||
public async Task<bool> UpdateProjectDetailsOnly(Project project)
|
||||
@ -62,7 +446,14 @@ namespace Marco.Pms.Services.Helpers
|
||||
try
|
||||
{
|
||||
var response = await _projectCache.GetProjectDetailsListFromCache(projectIds);
|
||||
return response;
|
||||
if (response.Any())
|
||||
{
|
||||
return response;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -70,6 +461,9 @@ namespace Marco.Pms.Services.Helpers
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------ Project Infrastructure Cache ---------------------------------------
|
||||
|
||||
public async Task AddBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null)
|
||||
{
|
||||
try
|
||||
@ -342,5 +736,33 @@ namespace Marco.Pms.Services.Helpers
|
||||
_logger.LogWarning("Error occured while deleting Application role {RoleId} from Cache for employee {EmployeeId}: {Error}", roleId, employeeId, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------ Report Cache ---------------------------------------
|
||||
|
||||
public async Task<List<ProjectReportEmailMongoDB>?> GetProjectReportMail(bool IsSend)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _reportCache.GetProjectReportMailFromCache(IsSend);
|
||||
return response;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Error occured while fetching project report mail bodys: {Error}", ex.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public async Task AddProjectReportMail(ProjectReportEmailMongoDB report)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _reportCache.AddProjectReportMailToCache(report);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Error occured while adding project report mail bodys: {Error}", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Entitlements;
|
||||
using Marco.Pms.Model.MongoDBModels;
|
||||
using Marco.Pms.Model.Projects;
|
||||
using Marco.Pms.Services.Helpers;
|
||||
using Marco.Pms.Services.Service;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MarcoBMS.Services.Helpers
|
||||
@ -13,13 +13,14 @@ namespace MarcoBMS.Services.Helpers
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly RolesHelper _rolesHelper;
|
||||
private readonly CacheUpdateHelper _cache;
|
||||
private readonly PermissionServices _permission;
|
||||
|
||||
|
||||
public ProjectsHelper(ApplicationDbContext context, RolesHelper rolesHelper, CacheUpdateHelper cache)
|
||||
public ProjectsHelper(ApplicationDbContext context, RolesHelper rolesHelper, CacheUpdateHelper cache, PermissionServices permission)
|
||||
{
|
||||
_context = context;
|
||||
_rolesHelper = rolesHelper;
|
||||
_cache = cache;
|
||||
_permission = permission;
|
||||
}
|
||||
|
||||
public async Task<List<Project>> GetAllProjectByTanentID(Guid tanentID)
|
||||
@ -51,79 +52,31 @@ namespace MarcoBMS.Services.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<Project>> GetMyProjects(Guid tenantId, Employee LoggedInEmployee)
|
||||
public async Task<List<Guid>> GetMyProjects(Guid tenantId, Employee LoggedInEmployee)
|
||||
{
|
||||
string[] projectsId = [];
|
||||
List<Project> projects = new List<Project>();
|
||||
|
||||
var projectIds = await _cache.GetProjects(LoggedInEmployee.Id);
|
||||
|
||||
if (projectIds != null)
|
||||
if (projectIds == null)
|
||||
{
|
||||
|
||||
List<ProjectMongoDB> projectdetails = await _cache.GetProjectDetailsList(projectIds) ?? new List<ProjectMongoDB>();
|
||||
projects = projectdetails.Select(p => new Project
|
||||
var hasPermission = await _permission.HasPermission(LoggedInEmployee.Id, PermissionsMaster.ManageProject);
|
||||
if (hasPermission)
|
||||
{
|
||||
Id = Guid.Parse(p.Id),
|
||||
Name = p.Name,
|
||||
ShortName = p.ShortName,
|
||||
ProjectAddress = p.ProjectAddress,
|
||||
ProjectStatusId = Guid.Parse(p.ProjectStatus?.Id ?? ""),
|
||||
ContactPerson = p.ContactPerson,
|
||||
StartDate = p.StartDate,
|
||||
EndDate = p.EndDate,
|
||||
TenantId = tenantId
|
||||
}).ToList();
|
||||
|
||||
if (projects.Count != projectIds.Count)
|
||||
{
|
||||
projects = await _context.Projects.Where(p => projectIds.Contains(p.Id)).ToListAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var featurePermissionIds = await _cache.GetPermissions(LoggedInEmployee.Id);
|
||||
if (featurePermissionIds == null)
|
||||
{
|
||||
List<FeaturePermission> featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(LoggedInEmployee.Id);
|
||||
featurePermissionIds = featurePermission.Select(fp => fp.Id).ToList();
|
||||
}
|
||||
// Define a common queryable base for projects
|
||||
IQueryable<Project> projectQuery = _context.Projects.Where(c => c.TenantId == tenantId);
|
||||
|
||||
// 2. Optimized Project Retrieval Logic
|
||||
// User with permission 'manage project' can see all projects
|
||||
if (featurePermissionIds != null && featurePermissionIds.Contains(Guid.Parse("172fc9b6-755b-4f62-ab26-55c34a330614")))
|
||||
{
|
||||
// If GetAllProjectByTanentID is already optimized and directly returns IQueryable or
|
||||
// directly executes with ToListAsync(), keep it.
|
||||
// If it does more complex logic or extra trips, consider inlining here.
|
||||
projects = await projectQuery.ToListAsync(); // Directly query the context
|
||||
var projects = await _context.Projects.Where(c => c.TenantId == tenantId).ToListAsync();
|
||||
projectIds = projects.Select(p => p.Id).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 3. Efficiently get project allocations and then filter projects
|
||||
// Load allocations only once
|
||||
var allocation = await GetProjectByEmployeeID(LoggedInEmployee.Id);
|
||||
|
||||
// If there are no allocations, return an empty list early
|
||||
if (allocation == null || !allocation.Any())
|
||||
if (allocation.Any())
|
||||
{
|
||||
return new List<Project>();
|
||||
projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList();
|
||||
}
|
||||
|
||||
// Use LINQ's Contains for efficient filtering by ProjectId
|
||||
projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); // Get distinct Guids
|
||||
|
||||
// Filter projects based on the retrieved ProjectIds
|
||||
projects = await projectQuery.Where(c => projectIds.Contains(c.Id)).ToListAsync();
|
||||
|
||||
return new List<Guid>();
|
||||
}
|
||||
projectIds = projects.Select(p => p.Id).ToList();
|
||||
await _cache.AddProjects(LoggedInEmployee.Id, projectIds);
|
||||
}
|
||||
|
||||
return projects;
|
||||
return projectIds;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,20 +1,28 @@
|
||||
using System.Globalization;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Model.Dtos.Attendance;
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Mail;
|
||||
using Marco.Pms.Model.MongoDBModels;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Marco.Pms.Model.ViewModels.Report;
|
||||
using MarcoBMS.Services.Service;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Marco.Pms.Services.Helpers
|
||||
{
|
||||
public class ReportHelper
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly IEmailSender _emailSender;
|
||||
private readonly ILoggingService _logger;
|
||||
private readonly CacheUpdateHelper _cache;
|
||||
public ReportHelper(CacheUpdateHelper cache, ApplicationDbContext context)
|
||||
public ReportHelper(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, CacheUpdateHelper cache)
|
||||
{
|
||||
_cache = cache;
|
||||
_context = context;
|
||||
_emailSender = emailSender;
|
||||
_logger = logger;
|
||||
_cache = cache;
|
||||
}
|
||||
public async Task<ProjectStatisticReport?> GetDailyProjectReport(Guid projectId, Guid tenantId)
|
||||
{
|
||||
@ -270,5 +278,88 @@ namespace Marco.Pms.Services.Helpers
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/// <summary>
|
||||
/// Retrieves project statistics for a given project ID and sends an email report.
|
||||
/// </summary>
|
||||
/// <param name="projectId">The ID of the project.</param>
|
||||
/// <param name="recipientEmail">The email address of the recipient.</param>
|
||||
/// <returns>An ApiResponse indicating the success or failure of retrieving statistics and sending the email.</returns>
|
||||
public async Task<ApiResponse<object>> GetProjectStatistics(Guid projectId, List<string> recipientEmails, string body, string subject, Guid tenantId)
|
||||
{
|
||||
// --- Input Validation ---
|
||||
if (projectId == Guid.Empty)
|
||||
{
|
||||
_logger.LogError("Validation Error: Provided empty project ID while fetching project report.");
|
||||
return ApiResponse<object>.ErrorResponse("Provided empty Project ID.", "Provided empty Project ID.", 400);
|
||||
}
|
||||
|
||||
if (recipientEmails == null || !recipientEmails.Any())
|
||||
{
|
||||
_logger.LogError("Validation Error: No recipient emails provided for project ID {ProjectId}.", projectId);
|
||||
return ApiResponse<object>.ErrorResponse("No recipient emails provided.", "No recipient emails provided.", 400);
|
||||
}
|
||||
|
||||
// --- Fetch Project Statistics ---
|
||||
var statisticReport = await GetDailyProjectReport(projectId, tenantId);
|
||||
|
||||
if (statisticReport == null)
|
||||
{
|
||||
_logger.LogWarning("Project Data Not Found: User attempted to fetch project progress for project ID {ProjectId} but it was not found.", projectId);
|
||||
return ApiResponse<object>.ErrorResponse("Project not found.", "Project not found.", 404);
|
||||
}
|
||||
|
||||
// --- Send Email & Log ---
|
||||
string emailBody;
|
||||
try
|
||||
{
|
||||
emailBody = await _emailSender.SendProjectStatisticsEmail(recipientEmails, body, subject, statisticReport);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Email Sending Error: Failed to send project statistics email for project ID {ProjectId}. : {Error}", projectId, ex.Message);
|
||||
return ApiResponse<object>.ErrorResponse("Failed to send email.", "An error occurred while sending the email.", 500);
|
||||
}
|
||||
|
||||
// Find a relevant employee. Use AsNoTracking() for read-only query if the entity won't be modified.
|
||||
// Consider if you need *any* employee from the recipients or a specific one (e.g., the sender).
|
||||
var employee = await _context.Employees
|
||||
.AsNoTracking() // Optimize for read-only
|
||||
.FirstOrDefaultAsync(e => e.Email != null && recipientEmails.Contains(e.Email)) ?? new Employee();
|
||||
|
||||
// Initialize Employee to a default or null, based on whether an employee is always expected.
|
||||
// If employee.Id is a non-nullable type, ensure proper handling if employee is null.
|
||||
Guid employeeId = employee.Id; // Default to Guid.Empty if no employee found
|
||||
|
||||
var mailLogs = recipientEmails.Select(recipientEmail => new MailLog
|
||||
{
|
||||
ProjectId = projectId,
|
||||
EmailId = recipientEmail,
|
||||
Body = emailBody,
|
||||
EmployeeId = employeeId, // Use the determined employeeId
|
||||
TimeStamp = DateTime.UtcNow,
|
||||
TenantId = tenantId
|
||||
}).ToList();
|
||||
|
||||
_context.MailLogs.AddRange(mailLogs);
|
||||
|
||||
try
|
||||
{
|
||||
await _context.SaveChangesAsync();
|
||||
_logger.LogInfo("Successfully sent and logged project statistics email for Project ID {ProjectId} to {RecipientCount} recipients.", projectId, recipientEmails.Count);
|
||||
return ApiResponse<object>.SuccessResponse(statisticReport, "Email sent successfully", 200);
|
||||
}
|
||||
catch (DbUpdateException dbEx)
|
||||
{
|
||||
_logger.LogError("Database Error: Failed to save mail logs for project ID {ProjectId}. : {Error}", projectId, dbEx.Message);
|
||||
// Depending on your requirements, you might still return success here as the email was sent.
|
||||
// Or return an error indicating the logging failed.
|
||||
return ApiResponse<object>.ErrorResponse("Email sent, but failed to log activity.", "Email sent, but an error occurred while logging.", 500);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Unexpected Error: An unhandled exception occurred while processing project statistics for project ID {ProjectId}. : {Error}", projectId, ex.Message);
|
||||
return ApiResponse<object>.ErrorResponse("An unexpected error occurred.", "An unexpected error occurred.", 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
30
Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs
Normal file
30
Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using AutoMapper;
|
||||
using Marco.Pms.Model.Master;
|
||||
using Marco.Pms.Model.MongoDBModels;
|
||||
using Marco.Pms.Model.Projects;
|
||||
using Marco.Pms.Model.ViewModels.Projects;
|
||||
|
||||
namespace Marco.Pms.Services.MappingProfiles
|
||||
{
|
||||
public class ProjectMappingProfile : Profile
|
||||
{
|
||||
public ProjectMappingProfile()
|
||||
{
|
||||
// Your mappings
|
||||
CreateMap<Project, ProjectVM>();
|
||||
CreateMap<Project, ProjectInfoVM>();
|
||||
CreateMap<ProjectMongoDB, ProjectInfoVM>();
|
||||
CreateMap<Project, ProjectListVM>();
|
||||
CreateMap<ProjectMongoDB, ProjectListVM>();
|
||||
CreateMap<ProjectMongoDB, ProjectVM>()
|
||||
.ForMember(
|
||||
dest => dest.Id,
|
||||
// Explicitly and safely convert string Id to Guid Id
|
||||
opt => opt.MapFrom(src => src.Id == null ? Guid.Empty : new Guid(src.Id))
|
||||
);
|
||||
|
||||
CreateMap<StatusMasterMongoDB, StatusMaster>();
|
||||
CreateMap<ProjectVM, Project>();
|
||||
}
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
||||
<PackageReference Include="AWSSDK.S3" Version="3.7.416.13" />
|
||||
<PackageReference Include="MailKit" Version="4.9.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.20" />
|
||||
|
@ -1,4 +1,3 @@
|
||||
using System.Text;
|
||||
using Marco.Pms.CacheHelper;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Model.Authentication;
|
||||
@ -16,47 +15,23 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Serilog;
|
||||
|
||||
using System.Text;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add Serilog Configuration
|
||||
string? mongoConn = builder.Configuration["MongoDB:SerilogDatabaseUrl"];
|
||||
string timeString = "00:00:30";
|
||||
TimeSpan.TryParse(timeString, out TimeSpan timeSpan);
|
||||
#region ======================= Service Configuration (Dependency Injection) =======================
|
||||
|
||||
// Add Serilog Configuration
|
||||
#region Logging
|
||||
builder.Host.UseSerilog((context, config) =>
|
||||
{
|
||||
config.ReadFrom.Configuration(context.Configuration) // Taking all configuration from appsetting.json
|
||||
.WriteTo.MongoDB(
|
||||
databaseUrl: mongoConn ?? string.Empty,
|
||||
collectionName: "api-logs",
|
||||
batchPostingLimit: 100,
|
||||
period: timeSpan
|
||||
);
|
||||
|
||||
config.ReadFrom.Configuration(context.Configuration);
|
||||
});
|
||||
#endregion
|
||||
|
||||
// Add services
|
||||
var corsSettings = builder.Configuration.GetSection("Cors");
|
||||
var allowedOrigins = corsSettings.GetValue<string>("AllowedOrigins")?.Split(',');
|
||||
var allowedMethods = corsSettings.GetValue<string>("AllowedMethods")?.Split(',');
|
||||
var allowedHeaders = corsSettings.GetValue<string>("AllowedHeaders")?.Split(',');
|
||||
|
||||
#region CORS (Cross-Origin Resource Sharing)
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("Policy", policy =>
|
||||
{
|
||||
if (allowedOrigins != null && allowedMethods != null && allowedHeaders != null)
|
||||
{
|
||||
policy.WithOrigins(allowedOrigins)
|
||||
.WithMethods(allowedMethods)
|
||||
.WithHeaders(allowedHeaders);
|
||||
}
|
||||
});
|
||||
}).AddCors(options =>
|
||||
{
|
||||
// A more permissive policy for development
|
||||
options.AddPolicy("DevCorsPolicy", policy =>
|
||||
{
|
||||
policy.AllowAnyOrigin()
|
||||
@ -64,93 +39,51 @@ builder.Services.AddCors(options =>
|
||||
.AllowAnyHeader()
|
||||
.WithExposedHeaders("Authorization");
|
||||
});
|
||||
});
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddHostedService<StartupUserSeeder>();
|
||||
// A stricter policy for production (loaded from config)
|
||||
var corsSettings = builder.Configuration.GetSection("Cors");
|
||||
var allowedOrigins = corsSettings.GetValue<string>("AllowedOrigins")?.Split(',') ?? Array.Empty<string>();
|
||||
options.AddPolicy("ProdCorsPolicy", policy =>
|
||||
{
|
||||
policy.WithOrigins(allowedOrigins)
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader();
|
||||
});
|
||||
});
|
||||
#endregion
|
||||
|
||||
#region Core Web & Framework Services
|
||||
builder.Services.AddControllers();
|
||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||
builder.Services.AddSignalR();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
builder.Services.AddSwaggerGen(option =>
|
||||
{
|
||||
option.SwaggerDoc("v1", new OpenApiInfo { Title = "Demo API", Version = "v1" });
|
||||
option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||
{
|
||||
In = ParameterLocation.Header,
|
||||
Description = "Please enter a valid token",
|
||||
Name = "Authorization",
|
||||
Type = SecuritySchemeType.Http,
|
||||
BearerFormat = "JWT",
|
||||
Scheme = "Bearer"
|
||||
});
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddMemoryCache();
|
||||
builder.Services.AddAutoMapper(typeof(Program));
|
||||
builder.Services.AddHostedService<StartupUserSeeder>();
|
||||
#endregion
|
||||
|
||||
option.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type=ReferenceType.SecurityScheme,
|
||||
Id="Bearer"
|
||||
}
|
||||
},
|
||||
new string[]{}
|
||||
}
|
||||
});
|
||||
});
|
||||
#region Database & Identity
|
||||
string? connString = builder.Configuration.GetConnectionString("DefaultConnectionString")
|
||||
?? throw new InvalidOperationException("Database connection string 'DefaultConnectionString' not found.");
|
||||
|
||||
builder.Services.Configure<SmtpSettings>(builder.Configuration.GetSection("SmtpSettings"));
|
||||
builder.Services.AddTransient<IEmailSender, EmailSender>();
|
||||
|
||||
builder.Services.Configure<AWSSettings>(builder.Configuration.GetSection("AWS")); // For uploading images to aws s3
|
||||
builder.Services.AddTransient<S3UploadService>();
|
||||
|
||||
builder.Services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();
|
||||
|
||||
|
||||
string? connString = builder.Configuration.GetConnectionString("DefaultConnectionString");
|
||||
// This single call correctly registers BOTH the DbContext (scoped) AND the IDbContextFactory (singleton).
|
||||
builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
|
||||
options.UseMySql(connString, ServerVersion.AutoDetect(connString)));
|
||||
|
||||
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
||||
{
|
||||
options.UseMySql(connString, ServerVersion.AutoDetect(connString));
|
||||
});
|
||||
options.UseMySql(connString, ServerVersion.AutoDetect(connString)));
|
||||
|
||||
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||
.AddDefaultTokenProviders();
|
||||
#endregion
|
||||
|
||||
builder.Services.AddMemoryCache();
|
||||
|
||||
|
||||
//builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
//builder.Services.AddScoped<IProjectRepository, ProjectRepository>();
|
||||
//builder.Services.AddScoped<IEmployeeRepository, EmployeeRepository>();
|
||||
//builder.Services.AddScoped<IActivityMasterRepository, ActivityMasterRepository>();
|
||||
//builder.Services.AddScoped<IAttendenceRepository, AttendenceRepository>();
|
||||
//builder.Services.AddScoped<IProjectAllocationRepository, ProjectAllocationRepository>();
|
||||
|
||||
builder.Services.AddScoped<RefreshTokenService>();
|
||||
builder.Services.AddScoped<PermissionServices>();
|
||||
|
||||
builder.Services.AddScoped<UserHelper>();
|
||||
builder.Services.AddScoped<RolesHelper>();
|
||||
builder.Services.AddScoped<EmployeeHelper>();
|
||||
builder.Services.AddScoped<ProjectsHelper>();
|
||||
builder.Services.AddScoped<DirectoryHelper>();
|
||||
builder.Services.AddScoped<MasterHelper>();
|
||||
builder.Services.AddScoped<ReportHelper>();
|
||||
builder.Services.AddScoped<CacheUpdateHelper>();
|
||||
builder.Services.AddScoped<ProjectCache>();
|
||||
builder.Services.AddScoped<EmployeeCache>();
|
||||
builder.Services.AddSingleton<ILoggingService, LoggingService>();
|
||||
|
||||
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
|
||||
#region Authentication (JWT)
|
||||
var jwtSettings = builder.Configuration.GetSection("Jwt").Get<JwtSettings>()
|
||||
?? throw new InvalidOperationException("JwtSettings section is missing or invalid.");
|
||||
|
||||
if (jwtSettings != null && jwtSettings.Key != null)
|
||||
{
|
||||
builder.Services.AddSingleton(jwtSettings);
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
@ -168,71 +101,129 @@ if (jwtSettings != null && jwtSettings.Key != null)
|
||||
ValidAudience = jwtSettings.Audience,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key))
|
||||
};
|
||||
|
||||
// This event allows SignalR to get the token from the query string
|
||||
options.Events = new JwtBearerEvents
|
||||
{
|
||||
OnMessageReceived = context =>
|
||||
{
|
||||
var accessToken = context.Request.Query["access_token"];
|
||||
var path = context.HttpContext.Request.Path;
|
||||
|
||||
// Match your hub route here
|
||||
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs/marco"))
|
||||
if (!string.IsNullOrEmpty(accessToken) && context.HttpContext.Request.Path.StartsWithSegments("/hubs/marco"))
|
||||
{
|
||||
context.Token = accessToken;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
});
|
||||
builder.Services.AddSingleton(jwtSettings);
|
||||
}
|
||||
#endregion
|
||||
|
||||
builder.Services.AddSignalR();
|
||||
#region API Documentation (Swagger)
|
||||
builder.Services.AddSwaggerGen(option =>
|
||||
{
|
||||
option.SwaggerDoc("v1", new OpenApiInfo { Title = "Marco PMS API", Version = "v1" });
|
||||
option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||
{
|
||||
In = ParameterLocation.Header,
|
||||
Description = "Please enter a valid token",
|
||||
Name = "Authorization",
|
||||
Type = SecuritySchemeType.Http,
|
||||
BearerFormat = "JWT",
|
||||
Scheme = "Bearer"
|
||||
});
|
||||
option.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" }
|
||||
},
|
||||
Array.Empty<string>()
|
||||
}
|
||||
});
|
||||
});
|
||||
#endregion
|
||||
|
||||
#region Application-Specific Services
|
||||
// Configuration-bound services
|
||||
builder.Services.Configure<SmtpSettings>(builder.Configuration.GetSection("SmtpSettings"));
|
||||
builder.Services.Configure<AWSSettings>(builder.Configuration.GetSection("AWS"));
|
||||
|
||||
// Transient services (lightweight, created each time)
|
||||
builder.Services.AddTransient<IEmailSender, EmailSender>();
|
||||
builder.Services.AddTransient<S3UploadService>();
|
||||
|
||||
// Scoped services (one instance per HTTP request)
|
||||
builder.Services.AddScoped<RefreshTokenService>();
|
||||
builder.Services.AddScoped<PermissionServices>();
|
||||
builder.Services.AddScoped<UserHelper>();
|
||||
builder.Services.AddScoped<RolesHelper>();
|
||||
builder.Services.AddScoped<EmployeeHelper>();
|
||||
builder.Services.AddScoped<ProjectsHelper>();
|
||||
builder.Services.AddScoped<DirectoryHelper>();
|
||||
builder.Services.AddScoped<MasterHelper>();
|
||||
builder.Services.AddScoped<ReportHelper>();
|
||||
builder.Services.AddScoped<CacheUpdateHelper>();
|
||||
builder.Services.AddScoped<ProjectCache>();
|
||||
builder.Services.AddScoped<EmployeeCache>();
|
||||
builder.Services.AddScoped<ReportCache>();
|
||||
|
||||
// Singleton services (one instance for the app's lifetime)
|
||||
builder.Services.AddSingleton<ILoggingService, LoggingService>();
|
||||
#endregion
|
||||
|
||||
#region Web Server (Kestrel)
|
||||
builder.WebHost.ConfigureKestrel(options =>
|
||||
{
|
||||
options.AddServerHeader = false; // Disable the "Server" header
|
||||
options.AddServerHeader = false; // Disable the "Server" header for security
|
||||
});
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
#region ===================== HTTP Request Pipeline Configuration =====================
|
||||
|
||||
// The order of middleware registration is critical for correct application behavior.
|
||||
|
||||
#region Global Middleware (Run First)
|
||||
// These custom middleware components run at the beginning of the pipeline to handle cross-cutting concerns.
|
||||
app.UseMiddleware<ExceptionHandlingMiddleware>();
|
||||
app.UseMiddleware<TenantMiddleware>();
|
||||
app.UseMiddleware<LoggingMiddleware>();
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
#region Development Environment Configuration
|
||||
// These tools are only enabled in the Development environment for debugging and API testing.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
// Use CORS in the pipeline
|
||||
app.UseCors("DevCorsPolicy");
|
||||
}
|
||||
else
|
||||
{
|
||||
//if (app.Environment.IsProduction())
|
||||
//{
|
||||
// app.UseCors("ProdCorsPolicy");
|
||||
//}
|
||||
#endregion
|
||||
|
||||
//app.UseCors("AllowAll");
|
||||
app.UseCors("DevCorsPolicy");
|
||||
}
|
||||
#region Standard Middleware
|
||||
// Common middleware for handling static content, security, and routing.
|
||||
app.UseStaticFiles(); // Enables serving static files (e.g., from wwwroot)
|
||||
app.UseHttpsRedirection(); // Redirects HTTP requests to HTTPS
|
||||
#endregion
|
||||
|
||||
app.UseStaticFiles(); // Enables serving static files
|
||||
#region Security (CORS, Authentication & Authorization)
|
||||
// Security-related middleware must be in the correct order.
|
||||
var corsPolicy = app.Environment.IsDevelopment() ? "DevCorsPolicy" : "ProdCorsPolicy";
|
||||
app.UseCors(corsPolicy); // CORS must be applied before Authentication/Authorization.
|
||||
|
||||
//app.UseSerilogRequestLogging(); // This is Default Serilog Logging Middleware we are not using this because we're using custom logging middleware
|
||||
app.UseAuthentication(); // 1. Identifies who the user is.
|
||||
app.UseAuthorization(); // 2. Determines what the identified user is allowed to do.
|
||||
#endregion
|
||||
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.MapHub<MarcoHub>("/hubs/marco");
|
||||
#region Endpoint Routing (Run Last)
|
||||
// These map incoming requests to the correct controller actions or SignalR hubs.
|
||||
app.MapControllers();
|
||||
app.MapHub<MarcoHub>("/hubs/marco");
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
app.Run();
|
@ -1,10 +1,9 @@
|
||||
using Serilog.Context;
|
||||
|
||||
namespace MarcoBMS.Services.Service
|
||||
namespace MarcoBMS.Services.Service
|
||||
{
|
||||
public interface ILoggingService
|
||||
{
|
||||
void LogInfo(string? message, params object[]? args);
|
||||
void LogDebug(string? message, params object[]? args);
|
||||
void LogWarning(string? message, params object[]? args);
|
||||
void LogError(string? message, params object[]? args);
|
||||
|
||||
|
@ -18,10 +18,11 @@ namespace MarcoBMS.Services.Service
|
||||
{
|
||||
_logger.LogError(message, args);
|
||||
}
|
||||
else {
|
||||
else
|
||||
{
|
||||
_logger.LogError(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void LogInfo(string? message, params object[]? args)
|
||||
{
|
||||
@ -35,6 +36,18 @@ namespace MarcoBMS.Services.Service
|
||||
_logger.LogInformation(message);
|
||||
}
|
||||
}
|
||||
public void LogDebug(string? message, params object[]? args)
|
||||
{
|
||||
using (LogContext.PushProperty("LogLevel", "Information"))
|
||||
if (args != null)
|
||||
{
|
||||
_logger.LogDebug(message, args);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug(message);
|
||||
}
|
||||
}
|
||||
|
||||
public void LogWarning(string? message, params object[]? args)
|
||||
{
|
||||
@ -49,6 +62,5 @@ namespace MarcoBMS.Services.Service
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Entitlements;
|
||||
using Marco.Pms.Model.Projects;
|
||||
using Marco.Pms.Services.Helpers;
|
||||
using MarcoBMS.Services.Helpers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -12,13 +11,11 @@ namespace Marco.Pms.Services.Service
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly RolesHelper _rolesHelper;
|
||||
private readonly ProjectsHelper _projectsHelper;
|
||||
private readonly CacheUpdateHelper _cache;
|
||||
public PermissionServices(ApplicationDbContext context, RolesHelper rolesHelper, ProjectsHelper projectsHelper, CacheUpdateHelper cache)
|
||||
public PermissionServices(ApplicationDbContext context, RolesHelper rolesHelper, CacheUpdateHelper cache)
|
||||
{
|
||||
_context = context;
|
||||
_rolesHelper = rolesHelper;
|
||||
_projectsHelper = projectsHelper;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
@ -33,24 +30,31 @@ namespace Marco.Pms.Services.Service
|
||||
var hasPermission = featurePermissionIds.Contains(featurePermissionId);
|
||||
return hasPermission;
|
||||
}
|
||||
public async Task<bool> HasProjectPermission(Employee emp, string projectId)
|
||||
public async Task<bool> HasProjectPermission(Employee LoggedInEmployee, Guid projectId)
|
||||
{
|
||||
List<FeaturePermission> featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(emp.Id);
|
||||
string[] projectsId = [];
|
||||
var employeeId = LoggedInEmployee.Id;
|
||||
var projectIds = await _cache.GetProjects(employeeId);
|
||||
|
||||
/* User with permission manage project can see all projects */
|
||||
if (featurePermission != null && featurePermission.Exists(c => c.Id.ToString() == "172fc9b6-755b-4f62-ab26-55c34a330614"))
|
||||
if (projectIds == null)
|
||||
{
|
||||
List<Project> projects = await _projectsHelper.GetAllProjectByTanentID(emp.TenantId);
|
||||
projectsId = projects.Select(c => c.Id.ToString()).ToArray();
|
||||
var hasPermission = await HasPermission(employeeId, PermissionsMaster.ManageProject);
|
||||
if (hasPermission)
|
||||
{
|
||||
var projects = await _context.Projects.Where(c => c.TenantId == LoggedInEmployee.TenantId).ToListAsync();
|
||||
projectIds = projects.Select(p => p.Id).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
var allocation = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.IsActive == true).ToListAsync();
|
||||
if (allocation.Any())
|
||||
{
|
||||
projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
await _cache.AddProjects(LoggedInEmployee.Id, projectIds);
|
||||
}
|
||||
else
|
||||
{
|
||||
List<ProjectAllocation> allocation = await _projectsHelper.GetProjectByEmployeeID(emp.Id);
|
||||
projectsId = allocation.Select(c => c.ProjectId.ToString()).ToArray();
|
||||
}
|
||||
bool response = projectsId.Contains(projectId);
|
||||
return response;
|
||||
return projectIds.Contains(projectId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user