diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index 9b2036d..1fd36f4 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -24,132 +24,14 @@ namespace Marco.Pms.CacheHelper _projetCollection = mongoDB.GetCollection("ProjectDetails"); _taskCollection = mongoDB.GetCollection("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(); - - foreach (var building in buildings) - { - double buildingPlanned = 0, buildingCompleted = 0; - var buildingFloors = floors.Where(f => f.BuildingId == building.Id).ToList(); - - var floorMongoList = new List(); - foreach (var floor in buildingFloors) - { - double floorPlanned = 0, floorCompleted = 0; - var floorWorkAreas = workAreas.Where(wa => wa.FloorId == floor.Id).ToList(); - - var workAreaMongoList = new List(); - 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 projectDetailsList) + { + await _projetCollection.InsertManyAsync(projectDetailsList); //_logger.LogInfo("[AddProjectDetails] Project details inserted in MongoDB for ProjectId: {ProjectId}", project.Id); } public async Task 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?> GetProjectDetailsListFromCache(List projectIds) + public async Task> GetProjectDetailsListFromCache(List projectIds) { List stringProjectIds = projectIds.Select(p => p.ToString()).ToList(); var filter = Builders.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(); diff --git a/Marco.Pms.Services/Controllers/AttendanceController.cs b/Marco.Pms.Services/Controllers/AttendanceController.cs index 2622323..4c2f2c1 100644 --- a/Marco.Pms.Services/Controllers/AttendanceController.cs +++ b/Marco.Pms.Services/Controllers/AttendanceController.cs @@ -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 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 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 attendanceLogVMs = new List(); 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(); - 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 lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE && c.TenantId == TenantId).ToListAsync(); - List projectteam = await _projectsHelper.GetTeamByProject(TenantId, projectId, true); var idList = projectteam.Select(p => p.EmployeeId).ToList(); var jobRole = await _context.JobRoles.ToListAsync(); diff --git a/Marco.Pms.Services/Controllers/DashboardController.cs b/Marco.Pms.Services/Controllers/DashboardController.cs index 3829cdc..f2332df 100644 --- a/Marco.Pms.Services/Controllers/DashboardController.cs +++ b/Marco.Pms.Services/Controllers/DashboardController.cs @@ -516,7 +516,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) { diff --git a/Marco.Pms.Services/Controllers/EmployeeController.cs b/Marco.Pms.Services/Controllers/EmployeeController.cs index 9884e53..2f0ca5e 100644 --- a/Marco.Pms.Services/Controllers/EmployeeController.cs +++ b/Marco.Pms.Services/Controllers/EmployeeController.cs @@ -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 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); diff --git a/Marco.Pms.Services/Controllers/ImageController.cs b/Marco.Pms.Services/Controllers/ImageController.cs index 48fbc3b..9014171 100644 --- a/Marco.Pms.Services/Controllers/ImageController.cs +++ b/Marco.Pms.Services/Controllers/ImageController.cs @@ -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); diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 07ddbfd..29f9d04 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -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 _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 signalR, PermissionServices permission, CacheUpdateHelper cache, IServiceScopeFactory serviceScopeFactory) + IHubContext 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 GetAllProjects() + [HttpGet("list/basic1")] + public async Task GetAllProjects1() { if (!ModelState.IsValid) { @@ -84,31 +76,113 @@ namespace MarcoBMS.Services.Controllers return Unauthorized(ApiResponse.ErrorResponse("Employee not found.", null, 401)); } + List response = new List(); + List projectIds = await _projectsHelper.GetMyProjects(tenantId, LoggedInEmployee); - List projects = await _projectsHelper.GetMyProjects(tenantId, LoggedInEmployee); + List? projectsDetails = await _cache.GetProjectDetailsList(projectIds); + if (projectsDetails == null) + { + List projects = await _context.Projects.Where(p => projectIds.Contains(p.Id)).ToListAsync(); + //using (var scope = _serviceScopeFactory.CreateScope()) + //{ + // var cacheHelper = scope.ServiceProvider.GetRequiredService(); - - // 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 response = projects - .Select(project => project.ToProjectInfoVMFromProject()) - .ToList(); - - - //List response = new List(); - - //foreach (var project in projects) - //{ - // response.Add(project.ToProjectInfoVMFromProject()); - //} + //} + foreach (var project in projects) + { + await _cache.AddProjectDetails(project); + } + response = projects.Select(p => _mapper.Map(p)).ToList(); + } + else + { + response = projectsDetails.Select(p => _mapper.Map(p)).ToList(); + } return Ok(ApiResponse.SuccessResponse(response, "Success.", 200)); } + [HttpGet("list/basic")] + public async Task GetAllProjects() // Renamed for clarity + { + // Step 1: Get the current user + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + if (loggedInEmployee == null) + { + return Unauthorized(ApiResponse.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 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>.SuccessResponse(new List(), "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>.SuccessResponse(projectVMs, $"{projectVMs.Count} records of project fetchd successfully", 200)); + } + + /// + /// 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. + /// + /// The list of project IDs to retrieve. + /// A list of ProjectInfoVMs. + private async Task> GetProjectInfosByIdsAsync(List 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(); + var finalViewModels = _mapper.Map>(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>(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 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 projects = await _projectsHelper.GetMyProjects(tenantId, LoggedInEmployee); - - - - + //List projects = await _projectsHelper.GetMyProjects(tenantId, LoggedInEmployee); + ////List projects = new List(); + /// List response = new List(); - foreach (var project in projects) + List 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 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 buildings = await _context.Buildings.Where(b => b.ProjectId == project.Id && b.TenantId == tenantId).ToListAsync(); - List idList = buildings.Select(b => b.Id).ToList(); - List floors = await _context.Floor.Where(f => idList.Contains(f.BuildingId) && f.TenantId == tenantId).ToListAsync(); - idList = floors.Select(f => f.Id).ToList(); + List allBuildings = await _context.Buildings.Where(b => projectIds.Contains(b.ProjectId) && b.TenantId == tenantId).ToListAsync(); + List idList = allBuildings.Select(b => b.Id).ToList(); - List workAreas = await _context.WorkAreas.Where(a => idList.Contains(a.FloorId) && a.TenantId == tenantId).ToListAsync(); - idList = workAreas.Select(a => a.Id).ToList(); + List allFloors = await _context.Floor.Where(f => idList.Contains(f.BuildingId) && f.TenantId == tenantId).ToListAsync(); + idList = allFloors.Select(f => f.Id).ToList(); - List 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 allWorkAreas = await _context.WorkAreas.Where(a => idList.Contains(a.FloorId) && a.TenantId == tenantId).ToListAsync(); + idList = allWorkAreas.Select(a => a.Id).ToList(); + + List 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(project); + var team = teams.Where(p => p.TenantId == tenantId && p.ProjectId == project.Id && p.IsActive == true).ToList(); + + result.TeamSize = team.Count(); + + List buildings = allBuildings.Where(b => b.ProjectId == project.Id && b.TenantId == tenantId).ToList(); + idList = buildings.Select(b => b.Id).ToList(); + + List floors = allFloors.Where(f => idList.Contains(f.BuildingId) && f.TenantId == tenantId).ToList(); + idList = floors.Select(f => f.Id).ToList(); + + List workAreas = allWorkAreas.Where(a => idList.Contains(a.FloorId) && a.TenantId == tenantId).ToList(); + idList = workAreas.Select(a => a.Id).ToList(); + + List 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(p)).ToList(); } return Ok(ApiResponse.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(project); + if (project != null) { await _cache.AddProjectDetails(project); @@ -246,23 +346,28 @@ namespace MarcoBMS.Services.Controllers } else { - projectVM = new ProjectVM + projectVM = _mapper.Map(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.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 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); diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index ae6264e..589ab52 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -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 _dbContextFactory; - public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache, ILoggingService logger) + public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache, ReportCache reportCache, ILoggingService logger, + IDbContextFactory 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 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(); + + foreach (var building in allBuildings) + { + double buildingPlanned = 0, buildingCompleted = 0; + var floorMongoList = new List(); + + foreach (var floor in floorsByBuildingId[building.Id]) // Fast lookup + { + double floorPlanned = 0, floorCompleted = 0; + var workAreaMongoList = new List(); + + 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 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(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(); + + foreach (var building in buildingsByProjectId[project.Id]) + { + double buildingPlanned = 0, buildingCompleted = 0; + var floorMongoList = new List(); + + foreach (var floor in floorsByBuildingId[building.Id]) + { + double floorPlanned = 0, floorCompleted = 0; + var workAreaMongoList = new List(); + + 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 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?> 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); + } + } } } diff --git a/Marco.Pms.Services/Helpers/ProjectsHelper.cs b/Marco.Pms.Services/Helpers/ProjectsHelper.cs index 85003ae..fb5b6f2 100644 --- a/Marco.Pms.Services/Helpers/ProjectsHelper.cs +++ b/Marco.Pms.Services/Helpers/ProjectsHelper.cs @@ -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> GetAllProjectByTanentID(Guid tanentID) @@ -51,80 +52,32 @@ namespace MarcoBMS.Services.Helpers } } - public async Task> GetMyProjects(Guid tenantId, Employee LoggedInEmployee) + public async Task> GetMyProjects(Guid tenantId, Employee LoggedInEmployee) { - string[] projectsId = []; - List projects = new List(); - var projectIds = await _cache.GetProjects(LoggedInEmployee.Id); - if (projectIds != null) + if (projectIds == null) { - - List projectdetails = await _cache.GetProjectDetailsList(projectIds) ?? new List(); - 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 = await _rolesHelper.GetFeaturePermissionByEmployeeID(LoggedInEmployee.Id); - featurePermissionIds = featurePermission.Select(fp => fp.Id).ToList(); - } - // Define a common queryable base for projects - IQueryable 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(); + 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(); } - projectIds = projects.Select(p => p.Id).ToList(); await _cache.AddProjects(LoggedInEmployee.Id, projectIds); } - return projects; + return projectIds; } } -} +} \ No newline at end of file diff --git a/Marco.Pms.Services/Helpers/ReportHelper.cs b/Marco.Pms.Services/Helpers/ReportHelper.cs index e7632fd..4ec0978 100644 --- a/Marco.Pms.Services/Helpers/ReportHelper.cs +++ b/Marco.Pms.Services/Helpers/ReportHelper.cs @@ -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 GetDailyProjectReport(Guid projectId, Guid tenantId) { @@ -270,5 +278,88 @@ namespace Marco.Pms.Services.Helpers } return null; } + /// + /// Retrieves project statistics for a given project ID and sends an email report. + /// + /// The ID of the project. + /// The email address of the recipient. + /// An ApiResponse indicating the success or failure of retrieving statistics and sending the email. + public async Task> GetProjectStatistics(Guid projectId, List 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.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.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.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.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.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.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.ErrorResponse("An unexpected error occurred.", "An unexpected error occurred.", 500); + } + } } } diff --git a/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs b/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs new file mode 100644 index 0000000..c7ec4af --- /dev/null +++ b/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs @@ -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(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap() + .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(); + CreateMap(); + } + } +} diff --git a/Marco.Pms.Services/Marco.Pms.Services.csproj b/Marco.Pms.Services/Marco.Pms.Services.csproj index a235e6a..2feafaf 100644 --- a/Marco.Pms.Services/Marco.Pms.Services.csproj +++ b/Marco.Pms.Services/Marco.Pms.Services.csproj @@ -11,6 +11,7 @@ + diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index 30831c6..7fa2647 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -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("AllowedOrigins")?.Split(','); -var allowedMethods = corsSettings.GetValue("AllowedMethods")?.Split(','); -var allowedHeaders = corsSettings.GetValue("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(); + // A stricter policy for production (loaded from config) + var corsSettings = builder.Configuration.GetSection("Cors"); + var allowedOrigins = corsSettings.GetValue("AllowedOrigins")?.Split(',') ?? Array.Empty(); + 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(); +#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(builder.Configuration.GetSection("SmtpSettings")); -builder.Services.AddTransient(); - -builder.Services.Configure(builder.Configuration.GetSection("AWS")); // For uploading images to aws s3 -builder.Services.AddTransient(); - -builder.Services.AddIdentity().AddEntityFrameworkStores().AddDefaultTokenProviders(); - - -string? connString = builder.Configuration.GetConnectionString("DefaultConnectionString"); +// This single call correctly registers BOTH the DbContext (scoped) AND the IDbContextFactory (singleton). +builder.Services.AddDbContextFactory(options => + options.UseMySql(connString, ServerVersion.AutoDetect(connString))); builder.Services.AddDbContext(options => -{ - options.UseMySql(connString, ServerVersion.AutoDetect(connString)); -}); + options.UseMySql(connString, ServerVersion.AutoDetect(connString))); +builder.Services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); +#endregion -builder.Services.AddMemoryCache(); - - -//builder.Services.AddScoped(); -//builder.Services.AddScoped(); -//builder.Services.AddScoped(); -//builder.Services.AddScoped(); -//builder.Services.AddScoped(); -//builder.Services.AddScoped(); - -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddSingleton(); - - -builder.Services.AddHttpContextAccessor(); - +#region Authentication (JWT) var jwtSettings = builder.Configuration.GetSection("Jwt").Get() ?? 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() + } + }); +}); +#endregion + +#region Application-Specific Services +// Configuration-bound services +builder.Services.Configure(builder.Configuration.GetSection("SmtpSettings")); +builder.Services.Configure(builder.Configuration.GetSection("AWS")); + +// Transient services (lightweight, created each time) +builder.Services.AddTransient(); +builder.Services.AddTransient(); + +// Scoped services (one instance per HTTP request) +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// Singleton services (one instance for the app's lifetime) +builder.Services.AddSingleton(); +#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(); app.UseMiddleware(); app.UseMiddleware(); +#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("/hubs/marco"); +#region Endpoint Routing (Run Last) +// These map incoming requests to the correct controller actions or SignalR hubs. app.MapControllers(); +app.MapHub("/hubs/marco"); +#endregion -app.Run(); +#endregion + +app.Run(); \ No newline at end of file diff --git a/Marco.Pms.Services/Service/ILoggingService.cs b/Marco.Pms.Services/Service/ILoggingService.cs index 39dbb00..b835d0c 100644 --- a/Marco.Pms.Services/Service/ILoggingService.cs +++ b/Marco.Pms.Services/Service/ILoggingService.cs @@ -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); diff --git a/Marco.Pms.Services/Service/LoggingServices.cs b/Marco.Pms.Services/Service/LoggingServices.cs index 4328a2a..5a016de 100644 --- a/Marco.Pms.Services/Service/LoggingServices.cs +++ b/Marco.Pms.Services/Service/LoggingServices.cs @@ -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 } } } - } diff --git a/Marco.Pms.Services/Service/PermissionServices.cs b/Marco.Pms.Services/Service/PermissionServices.cs index ce7476b..7162dc5 100644 --- a/Marco.Pms.Services/Service/PermissionServices.cs +++ b/Marco.Pms.Services/Service/PermissionServices.cs @@ -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 HasProjectPermission(Employee emp, string projectId) + public async Task HasProjectPermission(Employee LoggedInEmployee, Guid projectId) { - List 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 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 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); } } }