From 2c445878d0d805c5cfc935e527373954cfcc9cc2 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 1 Jul 2025 12:39:07 +0530 Subject: [PATCH 001/124] project details API is split into three APIs. --- .../ViewModels/Projects/ProjectVM.cs | 13 +- .../Controllers/ProjectController.cs | 281 +++++++++++------- 2 files changed, 179 insertions(+), 115 deletions(-) diff --git a/Marco.Pms.Model/ViewModels/Projects/ProjectVM.cs b/Marco.Pms.Model/ViewModels/Projects/ProjectVM.cs index cd349bb..240b35f 100644 --- a/Marco.Pms.Model/ViewModels/Projects/ProjectVM.cs +++ b/Marco.Pms.Model/ViewModels/Projects/ProjectVM.cs @@ -1,10 +1,17 @@ -using Marco.Pms.Model.Dtos.Project; +using Marco.Pms.Model.Master; namespace Marco.Pms.Model.ViewModels.Projects { - public class ProjectVM : ProjectDto + public class ProjectVM { - public List? Buildings { get; set; } + public Guid Id { get; set; } + public string? Name { get; set; } + public string? ShortName { get; set; } + public string? ProjectAddress { get; set; } + public string? ContactPerson { get; set; } + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + public StatusMaster? ProjectStatus { get; set; } } } diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 6b83a6c..6490c54 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -1,14 +1,13 @@ 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.Projects; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Model.ViewModels.Projects; using Marco.Pms.Services.Hubs; +using Marco.Pms.Services.Service; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; @@ -29,9 +28,16 @@ namespace MarcoBMS.Services.Controllers private readonly RolesHelper _rolesHelper; private readonly ProjectsHelper _projectsHelper; private readonly IHubContext _signalR; + private readonly PermissionServices _permission; + private readonly Guid ViewProjects; + private readonly Guid ManageProject; + private readonly Guid ViewInfra; + private readonly Guid ManageInfra; + private readonly Guid tenantId; - public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, ProjectsHelper projectHelper, IHubContext signalR) + public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, ProjectsHelper projectHelper, + IHubContext signalR, PermissionServices permission) { _context = context; _userHelper = userHelper; @@ -39,6 +45,12 @@ namespace MarcoBMS.Services.Controllers _rolesHelper = rolesHelper; _projectsHelper = projectHelper; _signalR = signalR; + _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"); + tenantId = _userHelper.GetTenantId(); } @@ -177,133 +189,68 @@ namespace MarcoBMS.Services.Controllers [HttpGet("details/{id}")] public async Task Details([FromRoute] Guid id) { - // ProjectDetailsVM vm = new ProjectDetailsVM(); - + // Step 1: Validate model state if (!ModelState.IsValid) { var errors = ModelState.Values .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); + _logger.LogWarning("Invalid model state in Details endpoint. Errors: {@Errors}", errors); + return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } - var project = await _context.Projects.Where(c => c.TenantId == _userHelper.GetTenantId() && c.Id == id).Include(c => c.ProjectStatus).SingleOrDefaultAsync(); // includeProperties: "ProjectStatus,Tenant"); //_context.Stock.FindAsync(id); + // Step 2: Get logged-in employee + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + _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); + if (!hasViewProjectPermission) + { + _logger.LogWarning("ViewProjects permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); + return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have permission to view projects", 403)); + } + + // Step 4: Check permission for this specific project + var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, id.ToString()); + if (!hasProjectPermission) + { + _logger.LogWarning("Project-specific access denied. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployee.Id, id); + return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have access to this project", 403)); + } + + // Step 5: Fetch project with status + var project = await _context.Projects + .Include(c => c.ProjectStatus) + .FirstOrDefaultAsync(c => c.TenantId == tenantId && c.Id == id); if (project == null) { + _logger.LogWarning("Project not found. ProjectId: {ProjectId}", id); return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); - - } - else - { - //var project = projects.Where(c => c.Id == id).SingleOrDefault(); - ProjectDetailsVM vm = await GetProjectViewModel(id, project); - - ProjectVM projectVM = new ProjectVM(); - if (vm.project != null) - { - projectVM.Id = vm.project.Id; - projectVM.Name = vm.project.Name; - projectVM.ShortName = vm.project.ShortName; - projectVM.ProjectAddress = vm.project.ProjectAddress; - projectVM.ContactPerson = vm.project.ContactPerson; - projectVM.StartDate = vm.project.StartDate; - projectVM.EndDate = vm.project.EndDate; - projectVM.ProjectStatusId = vm.project.ProjectStatusId; - } - projectVM.Buildings = new List(); - if (vm.buildings != null) - { - foreach (Building build in vm.buildings) - { - BuildingVM buildVM = new BuildingVM() { Id = build.Id, Description = build.Description, Name = build.Name }; - buildVM.Floors = new List(); - if (vm.floors != null) - { - foreach (Floor floorDto in vm.floors.Where(c => c.BuildingId == build.Id).ToList()) - { - FloorsVM floorVM = new FloorsVM() { FloorName = floorDto.FloorName, Id = floorDto.Id }; - floorVM.WorkAreas = new List(); - - if (vm.workAreas != null) - { - foreach (WorkArea workAreaDto in vm.workAreas.Where(c => c.FloorId == floorVM.Id).ToList()) - { - WorkAreaVM workAreaVM = new WorkAreaVM() { Id = workAreaDto.Id, AreaName = workAreaDto.AreaName, WorkItems = new List() }; - - if (vm.workItems != null) - { - foreach (WorkItem workItemDto in vm.workItems.Where(c => c.WorkAreaId == workAreaDto.Id).ToList()) - { - WorkItemVM workItemVM = new WorkItemVM() { WorkItemId = workItemDto.Id, WorkItem = workItemDto }; - - workItemVM.WorkItem.WorkArea = new WorkArea(); - - if (workItemVM.WorkItem.ActivityMaster != null) - { - workItemVM.WorkItem.ActivityMaster.Tenant = new Tenant(); - } - workItemVM.WorkItem.Tenant = new Tenant(); - - double todaysAssigned = 0; - if (vm.Tasks != null) - { - var tasks = vm.Tasks.Where(t => t.WorkItemId == workItemDto.Id).ToList(); - foreach (TaskAllocation task in tasks) - { - todaysAssigned += task.PlannedTask; - } - } - workItemVM.TodaysAssigned = todaysAssigned; - - workAreaVM.WorkItems.Add(workItemVM); - } - } - - floorVM.WorkAreas.Add(workAreaVM); - } - } - - buildVM.Floors.Add(floorVM); - } - } - projectVM.Buildings.Add(buildVM); - } - } - return Ok(ApiResponse.SuccessResponse(projectVM, "Success.", 200)); } - + // Step 6: Map and return result + var projectVM = GetProjectViewModel(project); + _logger.LogInfo("Project details fetched successfully. ProjectId: {ProjectId}", id); + return Ok(ApiResponse.SuccessResponse(projectVM, "Project details fetched successfully", 200)); } - private async Task GetProjectViewModel(Guid? id, Project project) + private ProjectVM GetProjectViewModel(Project project) { - ProjectDetailsVM vm = new ProjectDetailsVM(); - - // List buildings = _unitOfWork.Building.GetAll(c => c.ProjectId == id).ToList(); - List buildings = await _context.Buildings.Where(c => c.ProjectId == id).ToListAsync(); - List idList = buildings.Select(o => o.Id).ToList(); - // List floors = _unitOfWork.Floor.GetAll(c => idList.Contains(c.Id)).ToList(); - List floors = await _context.Floor.Where(c => idList.Contains(c.BuildingId)).ToListAsync(); - idList = floors.Select(o => o.Id).ToList(); - //List workAreas = _unitOfWork.WorkArea.GetAll(c => idList.Contains(c.Id), includeProperties: "WorkItems,WorkItems.ActivityMaster").ToList(); - - List workAreas = await _context.WorkAreas.Where(c => idList.Contains(c.FloorId)).ToListAsync(); - - idList = workAreas.Select(o => o.Id).ToList(); - List workItems = await _context.WorkItems.Include(c => c.WorkCategoryMaster).Where(c => idList.Contains(c.WorkAreaId)).Include(c => c.ActivityMaster).ToListAsync(); - // List workItems = _unitOfWork.WorkItem.GetAll(c => idList.Contains(c.WorkAreaId), includeProperties: "ActivityMaster").ToList(); - idList = workItems.Select(t => t.Id).ToList(); - List tasks = await _context.TaskAllocations.Where(t => idList.Contains(t.WorkItemId) && t.AssignmentDate.Date == DateTime.UtcNow.Date).ToListAsync(); - vm.project = project; - vm.buildings = buildings; - vm.floors = floors; - vm.workAreas = workAreas; - vm.workItems = workItems; - vm.Tasks = tasks; - return vm; + 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, + }; } private Guid GetTenantId() @@ -594,6 +541,116 @@ namespace MarcoBMS.Services.Controllers } + + [HttpGet("infra-details/{projectId}")] + public async Task GetInfraDetails(Guid projectId) + { + _logger.LogInfo("GetInfraDetails called for ProjectId: {ProjectId}", projectId); + + // Step 1: Get logged-in employee + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // Step 2: Check project-specific permission + var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.ToString()); + if (!hasProjectPermission) + { + _logger.LogWarning("Project access denied for EmployeeId: {EmployeeId} on ProjectId: {ProjectId}", loggedInEmployee.Id, projectId); + return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have access to this project", 403)); + } + + // Step 3: Check 'ViewInfra' permission + var hasViewInfraPermission = await _permission.HasPermission(ViewInfra, loggedInEmployee.Id); + if (!hasViewInfraPermission) + { + _logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); + return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have access to view infra", 403)); + } + + // Step 4: Fetch buildings for the project + var buildings = await _context.Buildings + .Where(b => b.ProjectId == projectId) + .ToListAsync(); + + var buildingIds = buildings.Select(b => b.Id).ToList(); + + // Step 5: Fetch floors associated with the buildings + var floors = await _context.Floor + .Where(f => buildingIds.Contains(f.BuildingId)) + .ToListAsync(); + + var floorIds = floors.Select(f => f.Id).ToList(); + + // Step 6: Fetch work areas associated with the floors + var workAreas = await _context.WorkAreas + .Where(wa => floorIds.Contains(wa.FloorId)) + .ToListAsync(); + + // Step 7: Build the infra hierarchy (Building > Floors > Work Areas) + var infraVM = buildings.Select(b => + { + var selectedFloors = floors + .Where(f => f.BuildingId == b.Id) + .Select(f => new + { + Id = f.Id, + FloorName = f.FloorName, + WorkAreas = workAreas + .Where(wa => wa.FloorId == f.Id) + .Select(wa => new { wa.Id, wa.AreaName }) + .ToList() + }).ToList(); + + return new + { + Id = b.Id, + BuildingName = b.Name, + Floors = selectedFloors + }; + }).ToList(); + + _logger.LogInfo("Infra details fetched successfully for ProjectId: {ProjectId}, EmployeeId: {EmployeeId}, Buildings: {Count}", + projectId, loggedInEmployee.Id, infraVM.Count); + + return Ok(ApiResponse.SuccessResponse(infraVM, "Infra details fetched successfully", 200)); + } + + [HttpGet("tasks/{workAreaId}")] + public async Task GetWorkItems(Guid workAreaId) + { + _logger.LogInfo("GetWorkItems called for WorkAreaId: {WorkAreaId}", workAreaId); + + // Step 1: Get the currently logged-in employee + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // Step 2: Check if the employee has ViewInfra permission + var hasViewInfraPermission = await _permission.HasPermission(ViewInfra, loggedInEmployee.Id); + if (!hasViewInfraPermission) + { + _logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); + return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have permission to view infrastructure", 403)); + } + + // Step 3: Check if the specified Work Area exists + var isWorkAreaExist = await _context.WorkAreas.AnyAsync(wa => wa.Id == workAreaId); + if (!isWorkAreaExist) + { + _logger.LogWarning("Work Area not found for WorkAreaId: {WorkAreaId}", workAreaId); + return NotFound(ApiResponse.ErrorResponse("Work Area not found", "Work Area not found in database", 404)); + } + + // Step 4: Fetch WorkItems with related Activity and Work Category data + var workItems = await _context.WorkItems + .Include(wi => wi.ActivityMaster) + .Include(wi => wi.WorkCategoryMaster) + .Where(wi => wi.WorkAreaId == workAreaId) + .ToListAsync(); + + _logger.LogInfo("{Count} work items fetched successfully for WorkAreaId: {WorkAreaId}", workItems.Count, workAreaId); + + // Step 5: Return result + return Ok(ApiResponse.SuccessResponse(workItems, $"{workItems.Count} records of tasks fetched successfully", 200)); + } + [HttpPost("task")] public async Task CreateProjectTask(List workItemDtos) { From 8055c04e4b85e54415cf10f5b5bfa5e62d5bae7a Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 1 Jul 2025 12:39:07 +0530 Subject: [PATCH 002/124] project details API is split into three APIs. --- .../ViewModels/Projects/ProjectVM.cs | 13 +- .../Controllers/ProjectController.cs | 281 +++++++++++------- 2 files changed, 179 insertions(+), 115 deletions(-) diff --git a/Marco.Pms.Model/ViewModels/Projects/ProjectVM.cs b/Marco.Pms.Model/ViewModels/Projects/ProjectVM.cs index cd349bb..240b35f 100644 --- a/Marco.Pms.Model/ViewModels/Projects/ProjectVM.cs +++ b/Marco.Pms.Model/ViewModels/Projects/ProjectVM.cs @@ -1,10 +1,17 @@ -using Marco.Pms.Model.Dtos.Project; +using Marco.Pms.Model.Master; namespace Marco.Pms.Model.ViewModels.Projects { - public class ProjectVM : ProjectDto + public class ProjectVM { - public List? Buildings { get; set; } + public Guid Id { get; set; } + public string? Name { get; set; } + public string? ShortName { get; set; } + public string? ProjectAddress { get; set; } + public string? ContactPerson { get; set; } + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + public StatusMaster? ProjectStatus { get; set; } } } diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 6b83a6c..6490c54 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -1,14 +1,13 @@ 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.Projects; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Model.ViewModels.Projects; using Marco.Pms.Services.Hubs; +using Marco.Pms.Services.Service; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; @@ -29,9 +28,16 @@ namespace MarcoBMS.Services.Controllers private readonly RolesHelper _rolesHelper; private readonly ProjectsHelper _projectsHelper; private readonly IHubContext _signalR; + private readonly PermissionServices _permission; + private readonly Guid ViewProjects; + private readonly Guid ManageProject; + private readonly Guid ViewInfra; + private readonly Guid ManageInfra; + private readonly Guid tenantId; - public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, ProjectsHelper projectHelper, IHubContext signalR) + public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, ProjectsHelper projectHelper, + IHubContext signalR, PermissionServices permission) { _context = context; _userHelper = userHelper; @@ -39,6 +45,12 @@ namespace MarcoBMS.Services.Controllers _rolesHelper = rolesHelper; _projectsHelper = projectHelper; _signalR = signalR; + _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"); + tenantId = _userHelper.GetTenantId(); } @@ -177,133 +189,68 @@ namespace MarcoBMS.Services.Controllers [HttpGet("details/{id}")] public async Task Details([FromRoute] Guid id) { - // ProjectDetailsVM vm = new ProjectDetailsVM(); - + // Step 1: Validate model state if (!ModelState.IsValid) { var errors = ModelState.Values .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); + _logger.LogWarning("Invalid model state in Details endpoint. Errors: {@Errors}", errors); + return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } - var project = await _context.Projects.Where(c => c.TenantId == _userHelper.GetTenantId() && c.Id == id).Include(c => c.ProjectStatus).SingleOrDefaultAsync(); // includeProperties: "ProjectStatus,Tenant"); //_context.Stock.FindAsync(id); + // Step 2: Get logged-in employee + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + _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); + if (!hasViewProjectPermission) + { + _logger.LogWarning("ViewProjects permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); + return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have permission to view projects", 403)); + } + + // Step 4: Check permission for this specific project + var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, id.ToString()); + if (!hasProjectPermission) + { + _logger.LogWarning("Project-specific access denied. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployee.Id, id); + return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have access to this project", 403)); + } + + // Step 5: Fetch project with status + var project = await _context.Projects + .Include(c => c.ProjectStatus) + .FirstOrDefaultAsync(c => c.TenantId == tenantId && c.Id == id); if (project == null) { + _logger.LogWarning("Project not found. ProjectId: {ProjectId}", id); return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); - - } - else - { - //var project = projects.Where(c => c.Id == id).SingleOrDefault(); - ProjectDetailsVM vm = await GetProjectViewModel(id, project); - - ProjectVM projectVM = new ProjectVM(); - if (vm.project != null) - { - projectVM.Id = vm.project.Id; - projectVM.Name = vm.project.Name; - projectVM.ShortName = vm.project.ShortName; - projectVM.ProjectAddress = vm.project.ProjectAddress; - projectVM.ContactPerson = vm.project.ContactPerson; - projectVM.StartDate = vm.project.StartDate; - projectVM.EndDate = vm.project.EndDate; - projectVM.ProjectStatusId = vm.project.ProjectStatusId; - } - projectVM.Buildings = new List(); - if (vm.buildings != null) - { - foreach (Building build in vm.buildings) - { - BuildingVM buildVM = new BuildingVM() { Id = build.Id, Description = build.Description, Name = build.Name }; - buildVM.Floors = new List(); - if (vm.floors != null) - { - foreach (Floor floorDto in vm.floors.Where(c => c.BuildingId == build.Id).ToList()) - { - FloorsVM floorVM = new FloorsVM() { FloorName = floorDto.FloorName, Id = floorDto.Id }; - floorVM.WorkAreas = new List(); - - if (vm.workAreas != null) - { - foreach (WorkArea workAreaDto in vm.workAreas.Where(c => c.FloorId == floorVM.Id).ToList()) - { - WorkAreaVM workAreaVM = new WorkAreaVM() { Id = workAreaDto.Id, AreaName = workAreaDto.AreaName, WorkItems = new List() }; - - if (vm.workItems != null) - { - foreach (WorkItem workItemDto in vm.workItems.Where(c => c.WorkAreaId == workAreaDto.Id).ToList()) - { - WorkItemVM workItemVM = new WorkItemVM() { WorkItemId = workItemDto.Id, WorkItem = workItemDto }; - - workItemVM.WorkItem.WorkArea = new WorkArea(); - - if (workItemVM.WorkItem.ActivityMaster != null) - { - workItemVM.WorkItem.ActivityMaster.Tenant = new Tenant(); - } - workItemVM.WorkItem.Tenant = new Tenant(); - - double todaysAssigned = 0; - if (vm.Tasks != null) - { - var tasks = vm.Tasks.Where(t => t.WorkItemId == workItemDto.Id).ToList(); - foreach (TaskAllocation task in tasks) - { - todaysAssigned += task.PlannedTask; - } - } - workItemVM.TodaysAssigned = todaysAssigned; - - workAreaVM.WorkItems.Add(workItemVM); - } - } - - floorVM.WorkAreas.Add(workAreaVM); - } - } - - buildVM.Floors.Add(floorVM); - } - } - projectVM.Buildings.Add(buildVM); - } - } - return Ok(ApiResponse.SuccessResponse(projectVM, "Success.", 200)); } - + // Step 6: Map and return result + var projectVM = GetProjectViewModel(project); + _logger.LogInfo("Project details fetched successfully. ProjectId: {ProjectId}", id); + return Ok(ApiResponse.SuccessResponse(projectVM, "Project details fetched successfully", 200)); } - private async Task GetProjectViewModel(Guid? id, Project project) + private ProjectVM GetProjectViewModel(Project project) { - ProjectDetailsVM vm = new ProjectDetailsVM(); - - // List buildings = _unitOfWork.Building.GetAll(c => c.ProjectId == id).ToList(); - List buildings = await _context.Buildings.Where(c => c.ProjectId == id).ToListAsync(); - List idList = buildings.Select(o => o.Id).ToList(); - // List floors = _unitOfWork.Floor.GetAll(c => idList.Contains(c.Id)).ToList(); - List floors = await _context.Floor.Where(c => idList.Contains(c.BuildingId)).ToListAsync(); - idList = floors.Select(o => o.Id).ToList(); - //List workAreas = _unitOfWork.WorkArea.GetAll(c => idList.Contains(c.Id), includeProperties: "WorkItems,WorkItems.ActivityMaster").ToList(); - - List workAreas = await _context.WorkAreas.Where(c => idList.Contains(c.FloorId)).ToListAsync(); - - idList = workAreas.Select(o => o.Id).ToList(); - List workItems = await _context.WorkItems.Include(c => c.WorkCategoryMaster).Where(c => idList.Contains(c.WorkAreaId)).Include(c => c.ActivityMaster).ToListAsync(); - // List workItems = _unitOfWork.WorkItem.GetAll(c => idList.Contains(c.WorkAreaId), includeProperties: "ActivityMaster").ToList(); - idList = workItems.Select(t => t.Id).ToList(); - List tasks = await _context.TaskAllocations.Where(t => idList.Contains(t.WorkItemId) && t.AssignmentDate.Date == DateTime.UtcNow.Date).ToListAsync(); - vm.project = project; - vm.buildings = buildings; - vm.floors = floors; - vm.workAreas = workAreas; - vm.workItems = workItems; - vm.Tasks = tasks; - return vm; + 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, + }; } private Guid GetTenantId() @@ -594,6 +541,116 @@ namespace MarcoBMS.Services.Controllers } + + [HttpGet("infra-details/{projectId}")] + public async Task GetInfraDetails(Guid projectId) + { + _logger.LogInfo("GetInfraDetails called for ProjectId: {ProjectId}", projectId); + + // Step 1: Get logged-in employee + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // Step 2: Check project-specific permission + var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.ToString()); + if (!hasProjectPermission) + { + _logger.LogWarning("Project access denied for EmployeeId: {EmployeeId} on ProjectId: {ProjectId}", loggedInEmployee.Id, projectId); + return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have access to this project", 403)); + } + + // Step 3: Check 'ViewInfra' permission + var hasViewInfraPermission = await _permission.HasPermission(ViewInfra, loggedInEmployee.Id); + if (!hasViewInfraPermission) + { + _logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); + return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have access to view infra", 403)); + } + + // Step 4: Fetch buildings for the project + var buildings = await _context.Buildings + .Where(b => b.ProjectId == projectId) + .ToListAsync(); + + var buildingIds = buildings.Select(b => b.Id).ToList(); + + // Step 5: Fetch floors associated with the buildings + var floors = await _context.Floor + .Where(f => buildingIds.Contains(f.BuildingId)) + .ToListAsync(); + + var floorIds = floors.Select(f => f.Id).ToList(); + + // Step 6: Fetch work areas associated with the floors + var workAreas = await _context.WorkAreas + .Where(wa => floorIds.Contains(wa.FloorId)) + .ToListAsync(); + + // Step 7: Build the infra hierarchy (Building > Floors > Work Areas) + var infraVM = buildings.Select(b => + { + var selectedFloors = floors + .Where(f => f.BuildingId == b.Id) + .Select(f => new + { + Id = f.Id, + FloorName = f.FloorName, + WorkAreas = workAreas + .Where(wa => wa.FloorId == f.Id) + .Select(wa => new { wa.Id, wa.AreaName }) + .ToList() + }).ToList(); + + return new + { + Id = b.Id, + BuildingName = b.Name, + Floors = selectedFloors + }; + }).ToList(); + + _logger.LogInfo("Infra details fetched successfully for ProjectId: {ProjectId}, EmployeeId: {EmployeeId}, Buildings: {Count}", + projectId, loggedInEmployee.Id, infraVM.Count); + + return Ok(ApiResponse.SuccessResponse(infraVM, "Infra details fetched successfully", 200)); + } + + [HttpGet("tasks/{workAreaId}")] + public async Task GetWorkItems(Guid workAreaId) + { + _logger.LogInfo("GetWorkItems called for WorkAreaId: {WorkAreaId}", workAreaId); + + // Step 1: Get the currently logged-in employee + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // Step 2: Check if the employee has ViewInfra permission + var hasViewInfraPermission = await _permission.HasPermission(ViewInfra, loggedInEmployee.Id); + if (!hasViewInfraPermission) + { + _logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); + return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have permission to view infrastructure", 403)); + } + + // Step 3: Check if the specified Work Area exists + var isWorkAreaExist = await _context.WorkAreas.AnyAsync(wa => wa.Id == workAreaId); + if (!isWorkAreaExist) + { + _logger.LogWarning("Work Area not found for WorkAreaId: {WorkAreaId}", workAreaId); + return NotFound(ApiResponse.ErrorResponse("Work Area not found", "Work Area not found in database", 404)); + } + + // Step 4: Fetch WorkItems with related Activity and Work Category data + var workItems = await _context.WorkItems + .Include(wi => wi.ActivityMaster) + .Include(wi => wi.WorkCategoryMaster) + .Where(wi => wi.WorkAreaId == workAreaId) + .ToListAsync(); + + _logger.LogInfo("{Count} work items fetched successfully for WorkAreaId: {WorkAreaId}", workItems.Count, workAreaId); + + // Step 5: Return result + return Ok(ApiResponse.SuccessResponse(workItems, $"{workItems.Count} records of tasks fetched successfully", 200)); + } + [HttpPost("task")] public async Task CreateProjectTask(List workItemDtos) { From ba1e644fd8f371149cb31fe5e46675c1a29d9c13 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 2 Jul 2025 09:59:56 +0530 Subject: [PATCH 003/124] Added new parameter uploaded by in documents table --- ..._ForeginKey_In_Decuments_Table.Designer.cs | 3426 +++++++++++++++++ ...ploadedBy_ForeginKey_In_Decuments_Table.cs | 50 + .../ApplicationDbContextModelSnapshot.cs | 11 + Marco.Pms.Model/DocumentManager/Document.cs | 12 +- 4 files changed, 3497 insertions(+), 2 deletions(-) create mode 100644 Marco.Pms.DataAccess/Migrations/20250702042830_Added_UploadedBy_ForeginKey_In_Decuments_Table.Designer.cs create mode 100644 Marco.Pms.DataAccess/Migrations/20250702042830_Added_UploadedBy_ForeginKey_In_Decuments_Table.cs diff --git a/Marco.Pms.DataAccess/Migrations/20250702042830_Added_UploadedBy_ForeginKey_In_Decuments_Table.Designer.cs b/Marco.Pms.DataAccess/Migrations/20250702042830_Added_UploadedBy_ForeginKey_In_Decuments_Table.Designer.cs new file mode 100644 index 0000000..c0c77b8 --- /dev/null +++ b/Marco.Pms.DataAccess/Migrations/20250702042830_Added_UploadedBy_ForeginKey_In_Decuments_Table.Designer.cs @@ -0,0 +1,3426 @@ +// +using System; +using Marco.Pms.DataAccess.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Marco.Pms.DataAccess.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250702042830_Added_UploadedBy_ForeginKey_In_Decuments_Table")] + partial class Added_UploadedBy_ForeginKey_In_Decuments_Table + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + //MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskAllocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApprovedById") + .HasColumnType("char(36)"); + + b.Property("ApprovedDate") + .HasColumnType("datetime(6)"); + + b.Property("AssignedBy") + .HasColumnType("char(36)"); + + b.Property("AssignmentDate") + .HasColumnType("datetime(6)"); + + b.Property("CompletedTask") + .HasColumnType("double"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ParentTaskId") + .HasColumnType("char(36)"); + + b.Property("PlannedTask") + .HasColumnType("double"); + + b.Property("ReportedById") + .HasColumnType("char(36)"); + + b.Property("ReportedDate") + .HasColumnType("datetime(6)"); + + b.Property("ReportedTask") + .HasColumnType("double"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("WorkItemId") + .HasColumnType("char(36)"); + + b.Property("WorkStatusId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedById"); + + b.HasIndex("AssignedBy"); + + b.HasIndex("ReportedById"); + + b.HasIndex("TenantId"); + + b.HasIndex("WorkItemId"); + + b.HasIndex("WorkStatusId"); + + b.ToTable("TaskAllocations"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("DocumentId") + .HasColumnType("char(36)"); + + b.Property("ReferenceId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("TaskAttachments"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CommentDate") + .HasColumnType("datetime(6)"); + + b.Property("CommentedBy") + .HasColumnType("char(36)"); + + b.Property("TaskAllocationId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("CommentedBy"); + + b.HasIndex("TaskAllocationId"); + + b.HasIndex("TenantId"); + + b.ToTable("TaskComments"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskMembers", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("EmployeeId") + .HasColumnType("char(36)"); + + b.Property("TaskAllocationId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("TaskAllocationId"); + + b.HasIndex("TenantId"); + + b.ToTable("TaskMembers"); + }); + + modelBuilder.Entity("Marco.Pms.Model.AttendanceModule.Attendance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Activity") + .HasColumnType("int"); + + b.Property("ApprovedBy") + .HasColumnType("char(36)"); + + b.Property("AttendanceDate") + .HasColumnType("datetime(6)"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("EmployeeID") + .HasColumnType("char(36)"); + + b.Property("InTime") + .HasColumnType("datetime(6)"); + + b.Property("IsApproved") + .HasColumnType("tinyint(1)"); + + b.Property("OutTime") + .HasColumnType("datetime(6)"); + + b.Property("ProjectID") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeID"); + + b.HasIndex("TenantId"); + + b.ToTable("Attendes"); + }); + + modelBuilder.Entity("Marco.Pms.Model.AttendanceModule.AttendanceLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Activity") + .HasColumnType("int"); + + b.Property("ActivityTime") + .HasColumnType("datetime(6)"); + + b.Property("AttendanceId") + .HasColumnType("char(36)"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("char(36)"); + + b.Property("EmployeeID") + .HasColumnType("char(36)"); + + b.Property("Latitude") + .HasColumnType("longtext"); + + b.Property("Longitude") + .HasColumnType("longtext"); + + b.Property("Photo") + .HasColumnType("longblob"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("UpdatedBy") + .HasColumnType("char(36)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AttendanceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("EmployeeID"); + + b.HasIndex("TenantId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("AttendanceLogs"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Authentication.MPINDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("MPIN") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MPINToken") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("TimeStamp") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("MPINDetails"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Authentication.OTPDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ExpriesInSec") + .HasColumnType("int"); + + b.Property("IsUsed") + .HasColumnType("tinyint(1)"); + + b.Property("OTP") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("TimeStamp") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("OTPDetails"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Authentication.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExpiryDate") + .HasColumnType("datetime(6)"); + + b.Property("IsRevoked") + .HasColumnType("tinyint(1)"); + + b.Property("IsUsed") + .HasColumnType("tinyint(1)"); + + b.Property("RevokedAt") + .HasColumnType("datetime(6)"); + + b.Property("Token") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.Bucket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedByID") + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByID"); + + b.HasIndex("TenantId"); + + b.ToTable("Buckets"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Address") + .HasColumnType("longtext"); + + b.Property("ContactCategoryId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedById") + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Organization") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedById") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ContactCategoryId"); + + b.HasIndex("CreatedById"); + + b.HasIndex("TenantId"); + + b.HasIndex("UpdatedById"); + + b.ToTable("Contacts"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactBucketMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BucketId") + .HasColumnType("char(36)"); + + b.Property("ContactId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("BucketId"); + + b.HasIndex("ContactId"); + + b.ToTable("ContactBucketMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactCategoryMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("ContactCategoryMasters"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactEmail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactId") + .HasColumnType("char(36)"); + + b.Property("EmailAddress") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)"); + + b.Property("Label") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.ToTable("ContactsEmails"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedById") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Note") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedById") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.HasIndex("CreatedById"); + + b.HasIndex("TenantId"); + + b.HasIndex("UpdatedById"); + + b.ToTable("ContactNotes"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactPhone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactId") + .HasColumnType("char(36)"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)"); + + b.Property("Label") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.ToTable("ContactsPhones"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactProjectMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactId") + .HasColumnType("char(36)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("TenantId"); + + b.ToTable("ContactProjectMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactTagMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactId") + .HasColumnType("char(36)"); + + b.Property("ContactTagId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.HasIndex("ContactTagId"); + + b.ToTable("ContactTagMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactTagMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("ContactTagMasters"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.DirectoryUpdateLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("RefereanceId") + .HasColumnType("char(36)"); + + b.Property("UpdateAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedById") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UpdatedById"); + + b.ToTable("DirectoryUpdateLogs"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.EmployeeBucketMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BucketId") + .HasColumnType("char(36)"); + + b.Property("EmployeeId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("BucketId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("EmployeeBucketMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.DocumentManager.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Base64Data") + .HasColumnType("longtext"); + + b.Property("BatchId") + .HasColumnType("char(36)"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("S3Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("ThumbS3Key") + .HasColumnType("longtext"); + + b.Property("UploadedAt") + .HasColumnType("datetime(6)"); + + b.Property("UploadedById") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("UploadedById"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Employees.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AadharNumber") + .HasColumnType("longtext"); + + b.Property("ApplicationUserId") + .HasColumnType("varchar(255)"); + + b.Property("BirthDate") + .HasColumnType("datetime(6)"); + + b.Property("CurrentAddress") + .HasColumnType("longtext"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("EmergencyContactPerson") + .HasColumnType("longtext"); + + b.Property("EmergencyPhoneNumber") + .HasColumnType("longtext"); + + b.Property("FirstName") + .HasColumnType("longtext"); + + b.Property("Gender") + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("JobRoleId") + .HasColumnType("char(36)"); + + b.Property("JoiningDate") + .HasColumnType("datetime(6)"); + + b.Property("LastName") + .HasColumnType("longtext"); + + b.Property("MiddleName") + .HasColumnType("longtext"); + + b.Property("PanNumber") + .HasColumnType("longtext"); + + b.Property("PermanentAddress") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("Photo") + .HasColumnType("longblob"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.HasIndex("JobRoleId"); + + b.HasIndex("TenantId"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Employees.EmployeeRoleMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("EmployeeId") + .HasColumnType("char(36)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("RoleId"); + + b.HasIndex("TenantId"); + + b.ToTable("EmployeeRoleMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Employees.WorkShift", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("EndTime") + .HasColumnType("time(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("StartTime") + .HasColumnType("time(6)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("WorkShifts"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.ActivityCheckList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ActivityId") + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IsChecked") + .HasColumnType("tinyint(1)"); + + b.Property("IsMandatory") + .HasColumnType("tinyint(1)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("ActivityCheckLists"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.CheckListMappings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CheckListId") + .HasColumnType("char(36)"); + + b.Property("TaskAllocationId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("CheckListMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.FeaturePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("FeatureId") + .HasColumnType("char(36)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FeatureId"); + + b.ToTable("FeaturePermissions"); + + b.HasData( + new + { + Id = new Guid("6ea44136-987e-44ba-9e5d-1cf8f5837ebc"), + Description = "Access all information related to the project.", + FeatureId = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), + IsEnabled = true, + Name = "View Project" + }, + new + { + Id = new Guid("172fc9b6-755b-4f62-ab26-55c34a330614"), + Description = "Potentially edit the project name, description, start/end dates, or status.", + FeatureId = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), + IsEnabled = true, + Name = "Manage Project" + }, + new + { + Id = new Guid("b94802ce-0689-4643-9e1d-11c86950c35b"), + Description = "The \"Manage Team\" feature allows authorized users to organize project personnel by adding, removing, and assigning employee to projects.", + FeatureId = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), + IsEnabled = true, + Name = "Manage Team" + }, + new + { + Id = new Guid("8d7cc6e3-9147-41f7-aaa7-fa507e450bd4"), + Description = "Grants a user comprehensive read-only access to all details concerning the project's underlying systems, technologies, resources, and configurations", + FeatureId = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), + IsEnabled = true, + Name = "View Project Infra" + }, + new + { + Id = new Guid("cf2825ad-453b-46aa-91d9-27c124d63373"), + Description = "This allows them to create, modify, and manage all aspects of the supporting infrastructure.", + FeatureId = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), + IsEnabled = true, + Name = "Manage Project Infra" + }, + new + { + Id = new Guid("9fcc5f87-25e3-4846-90ac-67a71ab92e3c"), + Description = "Grants a user comprehensive read-only access to all details associated with tasks within a project. This includes task descriptions, statuses, assignees, due dates, dependencies, progress, history, and any related attachments or discussions.", + FeatureId = new Guid("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"), + IsEnabled = true, + Name = "View Task" + }, + new + { + Id = new Guid("08752f33-3b29-4816-b76b-ea8a968ed3c5"), + Description = "This allows them to create new tasks, modify existing task attributes (description, status, assignee, due date, etc.),", + FeatureId = new Guid("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"), + IsEnabled = true, + Name = "Add/Edit Task" + }, + new + { + Id = new Guid("6a32379b-8b3f-49a6-8c48-4b7ac1b55dc2"), + Description = "Grants a user the ability to designate team members responsible for specific tasks and to update the completion status or provide progress updates for those tasks", + FeatureId = new Guid("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"), + IsEnabled = true, + Name = "Assign/Report Progress" + }, + new + { + Id = new Guid("db4e40c5-2ba9-4b6d-b8a6-a16a250ff99c"), + Description = "Grants a user the authority to officially confirm the completion or acceptance of a task, often signifying that it meets the required standards or criteria", + FeatureId = new Guid("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"), + IsEnabled = true, + Name = "Approve Task" + }, + new + { + Id = new Guid("60611762-7f8a-4fb5-b53f-b1139918796b"), + Description = "Grants a user read-only access to details about the all individuals within the system. This typically includes names, contact information, roles, departments, and potentially other relevant employee data", + FeatureId = new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), + IsEnabled = true, + Name = "View All Employees" + }, + new + { + Id = new Guid("b82d2b7e-0d52-45f3-997b-c008ea460e7f"), + Description = "Grants a user read-only access to details about the individuals within the system which are is assigned to same projects as user. This typically includes names, contact information, roles, departments, and potentially other relevant employee data", + FeatureId = new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), + IsEnabled = true, + Name = "View Team Members" + }, + new + { + Id = new Guid("a97d366a-c2bb-448d-be93-402bd2324566"), + Description = "Grants a user the authority to create new employee profiles and modify existing employee details within the system. This typically includes adding or updating information such as names, contact details, roles, departments, skills, and potentially other personal or professional data", + FeatureId = new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), + IsEnabled = true, + Name = "Add/Edit Employee" + }, + new + { + Id = new Guid("fbd213e0-0250-46f1-9f5f-4b2a1e6e76a3"), + Description = "Grants a user the authority to manage employee application roles, enabling them to assign or revoke access privileges within the system.", + FeatureId = new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), + IsEnabled = true, + Name = "Assign Roles" + }, + new + { + Id = new Guid("915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"), + Description = "Team Attendance refers to tracking and managing the attendance of all team members collectively, often monitored by a team lead or manager.", + FeatureId = new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), + IsEnabled = true, + Name = "Team Attendance " + }, + new + { + Id = new Guid("57802c4a-00aa-4a1f-a048-fd2f70dd44b6"), + Description = "Grants a user the authority to approve requests from employees to adjust or correct their recorded attendance. This typically involves reviewing the reason for the regularization, verifying any supporting documentation, and then officially accepting the changes to the employee's attendance records", + FeatureId = new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), + IsEnabled = true, + Name = "Regularize Attendance" + }, + new + { + Id = new Guid("ccb0589f-712b-43de-92ed-5b6088e7dc4e"), + Description = "Team Attendance refers to tracking and managing the attendance of all team members collectively, often monitored by a team lead or manager.", + FeatureId = new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), + IsEnabled = true, + Name = "Self Attendance" + }, + new + { + Id = new Guid("5ffbafe0-7ab0-48b1-bb50-c1bf76b65f9d"), + Description = "Grants a user read-only access to foundational or reference data within the system. \"Masters\" typically refer to predefined lists, categories, or templates that are used throughout the application to standardize information and maintain consistency", + FeatureId = new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), + IsEnabled = true, + Name = "View Masters" + }, + new + { + Id = new Guid("588a8824-f924-4955-82d8-fc51956cf323"), + Description = "Grants a user the authority to create, modify, and delete foundational or reference data within the system. These \"masters\" are typically the core lists, categories, and configurations that other data and functionalities rely upon, such as departments, job titles, product categories", + FeatureId = new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), + IsEnabled = true, + Name = "Manage Masters" + }, + new + { + Id = new Guid("4286a13b-bb40-4879-8c6d-18e9e393beda"), + Description = "Full control over all directories, including the ability to manage permissions for all directories in the system.", + FeatureId = new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), + IsEnabled = true, + Name = "Directory Admin" + }, + new + { + Id = new Guid("62668630-13ce-4f52-a0f0-db38af2230c5"), + Description = "Full control over directories they created or have been assigned. Can also manage permissions for those directories.", + FeatureId = new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), + IsEnabled = true, + Name = "Directory Manager" + }, + new + { + Id = new Guid("0f919170-92d4-4337-abd3-49b66fc871bb"), + Description = "Full control over directories they created. Can view contacts in directories they either created or were assigned to. Can manage permissions only for directories they created.", + FeatureId = new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), + IsEnabled = true, + Name = "Directory User" + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.RolePermissionMappings", b => + { + b.Property("ApplicationRoleId") + .HasColumnType("char(36)"); + + b.Property("FeaturePermissionId") + .HasColumnType("char(36)"); + + b.HasKey("ApplicationRoleId", "FeaturePermissionId"); + + b.HasIndex("FeaturePermissionId"); + + b.ToTable("RolePermissionMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactName") + .HasColumnType("longtext"); + + b.Property("ContactNumber") + .HasColumnType("longtext"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("DomainName") + .HasColumnType("longtext"); + + b.Property("IndustryId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OnBoardingDate") + .HasColumnType("datetime(6)"); + + b.Property("OragnizationSize") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("IndustryId"); + + b.ToTable("Tenants"); + + b.HasData( + new + { + Id = new Guid("b3466e83-7e11-464c-b93a-daf047838b26"), + ContactName = "Admin", + ContactNumber = "123456789", + Description = "", + DomainName = "www.marcobms.org", + IndustryId = new Guid("15436ee3-a650-469e-bfc2-59993f7514bb"), + IsActive = true, + Name = "MarcoBMS", + OnBoardingDate = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + OragnizationSize = "100-200" + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CommentId") + .HasColumnType("char(36)"); + + b.Property("FileId") + .HasColumnType("char(36)"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TicketId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("CommentId"); + + b.HasIndex("TicketId"); + + b.ToTable("TicketAttachments"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AuthorId") + .HasColumnType("char(36)"); + + b.Property("MessageText") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ParentMessageId") + .HasColumnType("char(36)"); + + b.Property("SentAt") + .HasColumnType("datetime(6)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("TicketId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("TicketComments"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketForum", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedById") + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LinkedActivityId") + .HasColumnType("char(36)"); + + b.Property("LinkedProjectId") + .HasColumnType("char(36)"); + + b.Property("PriorityId") + .HasColumnType("char(36)"); + + b.Property("StatusId") + .HasColumnType("char(36)"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("TypeId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("PriorityId"); + + b.HasIndex("StatusId"); + + b.HasIndex("TenantId"); + + b.HasIndex("TypeId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("TagId") + .HasColumnType("char(36)"); + + b.Property("TicketId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TagId"); + + b.HasIndex("TicketId"); + + b.ToTable("TicketTags"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketTypeMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IsDefault") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("TicketTypeMasters"); + + b.HasData( + new + { + Id = new Guid("c74e5480-2b71-483c-8f4a-1a9c69c32603"), + Description = "An identified problem that affects the performance, reliability, or standards of a product or service", + IsDefault = true, + Name = "Quality Issue", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("d1f55eab-9898-4e46-9f03-b263e33e5d38"), + Description = "A support service that assists users with technical issues, requests, or inquiries.", + IsDefault = true, + Name = "Help Desk", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Mail.MailDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("MailListId") + .HasColumnType("char(36)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("Recipient") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Schedule") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("MailListId"); + + b.ToTable("MailDetails"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Mail.MailLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("EmailId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("EmployeeId") + .HasColumnType("char(36)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("TimeStamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("MailLogs"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Mail.MailingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("MailingList"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.ActivityMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ActivityName") + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("UnitOfMeasurement") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("ActivityMasters"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.Feature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("ModuleId") + .HasColumnType("char(36)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ModuleId"); + + b.ToTable("Features"); + + b.HasData( + new + { + Id = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), + Description = "Manage Project", + IsActive = true, + ModuleId = new Guid("bf59fd88-b57a-4d67-bf01-3780f385896b"), + Name = "Project Management" + }, + new + { + Id = new Guid("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"), + Description = "Manage Tasks", + IsActive = true, + ModuleId = new Guid("bf59fd88-b57a-4d67-bf01-3780f385896b"), + Name = "Task Management" + }, + new + { + Id = new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), + Description = "Manage Employee", + IsActive = true, + ModuleId = new Guid("2a231490-bcb1-4bdd-91f1-f25fb7f25b23"), + Name = "Employee Management" + }, + new + { + Id = new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), + Description = "Attendance", + IsActive = true, + ModuleId = new Guid("2a231490-bcb1-4bdd-91f1-f25fb7f25b23"), + Name = "Attendance Management" + }, + new + { + Id = new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), + Description = "Global Masters", + IsActive = true, + ModuleId = new Guid("c43db8c7-ab73-47f4-9d3b-f83e81357924"), + Name = "Masters" + }, + new + { + Id = new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), + Description = "Managing all directory related rights", + IsActive = true, + ModuleId = new Guid("c43db8c7-ab73-47f4-9d3b-f83e81357924"), + Name = "Directory Management" + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.Industry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Industries"); + + b.HasData( + new + { + Id = new Guid("15436ee3-a650-469e-bfc2-59993f7514bb"), + Name = "Information Technology (IT) Services" + }, + new + { + Id = new Guid("0a63e657-2c5f-49b5-854b-42c978293154"), + Name = "Manufacturing & Production" + }, + new + { + Id = new Guid("bdc61e3b-69ea-4394-bab6-079ec135b5bd"), + Name = "Energy & Resources" + }, + new + { + Id = new Guid("5ca200ac-00d7-415e-a410-b948e27ac9d2"), + Name = "Finance & Professional Services" + }, + new + { + Id = new Guid("d5621700-cd87-441f-8cdb-6051ddfc83b4"), + Name = "Hospitals and Healthcare Services" + }, + new + { + Id = new Guid("23608891-657e-40f0-bbd4-2b0a2ec1a76f"), + Name = "Social Services" + }, + new + { + Id = new Guid("a493f4e3-16b1-4411-be3c-6bf2987a3168"), + Name = "Retail & Consumer Services" + }, + new + { + Id = new Guid("e9d8ce92-9371-4ed9-9831-83c07f78edec"), + Name = "Transportation & Logistics" + }, + new + { + Id = new Guid("8a0d6134-2dbe-4e0a-b250-ff34cb7b9df0"), + Name = "Education & Training" + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.Module", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Modules"); + + b.HasData( + new + { + Id = new Guid("bf59fd88-b57a-4d67-bf01-3780f385896b"), + Description = "Project Module", + Key = "b04da7e9-0406-409c-ac7f-b97256e6ea02", + Name = "Project" + }, + new + { + Id = new Guid("2a231490-bcb1-4bdd-91f1-f25fb7f25b23"), + Description = "Employee Module", + Key = "0971c7fb-6ce1-458a-ae3f-8d3205893637", + Name = "Employee" + }, + new + { + Id = new Guid("c43db8c7-ab73-47f4-9d3b-f83e81357924"), + Description = "Masters Module", + Key = "504ec132-e6a9-422f-8f85-050602cfce05", + Name = "Masters" + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.StatusMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("StatusMasters"); + + b.HasData( + new + { + Id = new Guid("b74da4c2-d07e-46f2-9919-e75e49b12731"), + Status = "Active", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("cdad86aa-8a56-4ff4-b633-9c629057dfef"), + Status = "In Progress", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("603e994b-a27f-4e5d-a251-f3d69b0498ba"), + Status = "On Hold", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("ef1c356e-0fe0-42df-a5d3-8daee355492d"), + Status = "In Active", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("33deaef9-9af1-4f2a-b443-681ea0d04f81"), + Status = "Completed", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.TicketPriorityMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ColorCode") + .HasColumnType("longtext"); + + b.Property("IsDefault") + .HasColumnType("tinyint(1)"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("TicketPriorityMasters"); + + b.HasData( + new + { + Id = new Guid("188d29b3-10f3-42d0-9587-1a46ae7a0320"), + ColorCode = "008000", + IsDefault = true, + Level = 1, + Name = "Low", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("0919bc84-9f82-4ecf-98c7-962755dd9a97"), + ColorCode = "FFFF00", + IsDefault = true, + Level = 2, + Name = "Medium", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("a13b7e59-16fd-4665-b5cf-a97399e8445a"), + ColorCode = "#FFA500", + IsDefault = true, + Level = 3, + Name = "High", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("f340fbc3-c9fd-46aa-b063-0093418830e4"), + ColorCode = "#FFA500", + IsDefault = true, + Level = 4, + Name = "Critical", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("44a7b91d-a0dd-45d1-8616-4d2f71e16401"), + ColorCode = "#FF0000", + IsDefault = true, + Level = 5, + Name = "Urgent", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.TicketStatusMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ColorCode") + .HasColumnType("longtext"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IsDefault") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("TicketStatusMasters"); + + b.HasData( + new + { + Id = new Guid("6b0c409b-3e80-4165-8b39-f3fcacb4c797"), + ColorCode = "#FFCC99", + Description = "This is a newly created issue.", + IsDefault = true, + Name = "New", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("6c5ac37d-5b7d-40f3-adec-2dabaa5cca86"), + ColorCode = "#E6FF99", + Description = "Assigned to employee or team of employees", + IsDefault = true, + Name = "Assigned", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("7f96bcd5-0c66-411b-8a1d-9d1a4785194e"), + ColorCode = "#99E6FF", + Description = "These issues are currently in progress", + IsDefault = true, + Name = "In Progress", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("5c72b630-6923-4215-bf2c-b1622afd76e7"), + ColorCode = "#6c757d", + Description = "These issues are currently under review", + IsDefault = true, + Name = "In Review", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("8ff85685-a875-4f21-aa95-d99551315fcc"), + ColorCode = "#B399FF", + Description = "The following issues are resolved and closed", + IsDefault = true, + Name = "Done", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.TicketTagMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ColorCode") + .HasColumnType("longtext"); + + b.Property("IsDefault") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("TicketTagMasters"); + + b.HasData( + new + { + Id = new Guid("ef6c2a65-f61d-4537-9650-a7ab7f8d98db"), + ColorCode = "#e59866", + IsDefault = true, + Name = "Quality Issue", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("5a168569-8ad7-4422-8db6-51ef25caddeb"), + ColorCode = "#85c1e9", + IsDefault = true, + Name = "Help Desk", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.WorkCategoryMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("WorkCategoryMasters"); + + b.HasData( + new + { + Id = new Guid("86bb2cc8-f6b5-4fdd-bbee-c389c713a44b"), + Description = "Created new task in a professional or creative context", + IsSystem = true, + Name = "Fresh Work", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("9ebfa19c-53b9-481b-b863-c25d2f843201"), + Description = "Revising, modifying, or correcting a task to improve its quality or fix issues", + IsSystem = true, + Name = "Rework", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("11a79929-1d07-42dc-9e98-82d0d2f4a240"), + Description = "Any defect, deviation, or non-conformance in a task that fails to meet established standards or customer expectations.", + IsSystem = true, + Name = "Quality Issue", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.WorkStatusMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("WorkStatusMasters"); + + b.HasData( + new + { + Id = new Guid("030bb085-e230-4370-aec7-9a74d652864e"), + Description = "Confirm the tasks are actually finished as reported", + IsSystem = true, + Name = "Approve", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("2a1a5b96-cf93-4111-b4b1-76c19d6333b4"), + Description = "Not all tasks are actually finished as reported", + IsSystem = true, + Name = "Partially Approve", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("00a062e6-62e6-42c5-b6b1-024328651b72"), + Description = "Tasks are not finished as reported or have any issues in al the tasks", + IsSystem = true, + Name = "NCR", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.Building", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("Buildings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.Floor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BuildingId") + .HasColumnType("char(36)"); + + b.Property("FloorName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("BuildingId"); + + b.HasIndex("TenantId"); + + b.ToTable("Floor"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactPerson") + .HasColumnType("longtext"); + + b.Property("EndDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProjectAddress") + .HasColumnType("longtext"); + + b.Property("ProjectStatusId") + .HasColumnType("char(36)"); + + b.Property("ShortName") + .HasColumnType("longtext"); + + b.Property("StartDate") + .HasColumnType("datetime(6)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ProjectStatusId"); + + b.HasIndex("TenantId"); + + b.ToTable("Projects"); + + b.HasData( + new + { + Id = new Guid("85bf587b-7ca9-4685-b77c-d817f5847e85"), + ContactPerson = "Project 1 Contact Person", + EndDate = new DateTime(2026, 4, 20, 10, 11, 17, 588, DateTimeKind.Unspecified), + Name = "Project 1", + ProjectAddress = "Project 1 Address", + ProjectStatusId = new Guid("b74da4c2-d07e-46f2-9919-e75e49b12731"), + StartDate = new DateTime(2025, 4, 20, 10, 11, 17, 588, DateTimeKind.Unspecified), + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.ProjectAllocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllocationDate") + .HasColumnType("datetime(6)"); + + b.Property("EmployeeId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("JobRoleId") + .HasColumnType("char(36)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("ReAllocationDate") + .HasColumnType("datetime(6)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("TenantId"); + + b.ToTable("ProjectAllocations"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.WorkArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AreaName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FloorId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("FloorId"); + + b.HasIndex("TenantId"); + + b.ToTable("WorkAreas"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.WorkItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ActivityId") + .HasColumnType("char(36)"); + + b.Property("CompletedWork") + .HasColumnType("double"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ParentTaskId") + .HasColumnType("char(36)"); + + b.Property("PlannedWork") + .HasColumnType("double"); + + b.Property("TaskDate") + .HasColumnType("datetime(6)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("WorkAreaId") + .HasColumnType("char(36)"); + + b.Property("WorkCategoryId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId"); + + b.HasIndex("TenantId"); + + b.HasIndex("WorkAreaId"); + + b.HasIndex("WorkCategoryId"); + + b.ToTable("WorkItems"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Roles.ApplicationRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("Role") + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("ApplicationRoles"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Roles.JobRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("JobRoles"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Utilities.Inquiries", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("About") + .HasColumnType("longtext"); + + b.Property("ContactNumber") + .HasColumnType("longtext"); + + b.Property("ContactPerson") + .HasColumnType("longtext"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("IndustryId") + .HasColumnType("char(36)"); + + b.Property("OragnizationSize") + .HasColumnType("longtext"); + + b.Property("OrganizatioinName") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Inquiries"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(21) + .HasColumnType("varchar(21)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("SecurityStamp") + .HasColumnType("longtext"); + + b.Property("TwoFactorEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + + b.HasDiscriminator().HasValue("IdentityUser"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("RoleId") + .HasColumnType("varchar(255)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.ApplicationUser", b => + { + b.HasBaseType("Microsoft.AspNetCore.Identity.IdentityUser"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRootUser") + .HasColumnType("tinyint(1)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasDiscriminator().HasValue("ApplicationUser"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskAllocation", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "ApprovedBy") + .WithMany() + .HasForeignKey("ApprovedById"); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("AssignedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "ReportedBy") + .WithMany() + .HasForeignKey("ReportedById"); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Projects.WorkItem", "WorkItem") + .WithMany() + .HasForeignKey("WorkItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Master.WorkStatusMaster", "WorkStatus") + .WithMany() + .HasForeignKey("WorkStatusId"); + + b.Navigation("ApprovedBy"); + + b.Navigation("Employee"); + + b.Navigation("ReportedBy"); + + b.Navigation("Tenant"); + + b.Navigation("WorkItem"); + + b.Navigation("WorkStatus"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskComment", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("CommentedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Activities.TaskAllocation", "TaskAllocation") + .WithMany() + .HasForeignKey("TaskAllocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("TaskAllocation"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskMembers", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Activities.TaskAllocation", "TaskAllocation") + .WithMany() + .HasForeignKey("TaskAllocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("TaskAllocation"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.AttendanceModule.Attendance", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "Approver") + .WithMany() + .HasForeignKey("EmployeeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Approver"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.AttendanceModule.AttendanceLog", b => + { + b.HasOne("Marco.Pms.Model.AttendanceModule.Attendance", "Attendance") + .WithMany() + .HasForeignKey("AttendanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.DocumentManager.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId"); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "UpdatedByEmployee") + .WithMany() + .HasForeignKey("UpdatedBy"); + + b.Navigation("Attendance"); + + b.Navigation("Document"); + + b.Navigation("Employee"); + + b.Navigation("Tenant"); + + b.Navigation("UpdatedByEmployee"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Authentication.MPINDetails", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Authentication.OTPDetails", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Authentication.RefreshToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.Bucket", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedByID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.Contact", b => + { + b.HasOne("Marco.Pms.Model.Directory.ContactCategoryMaster", "ContactCategory") + .WithMany() + .HasForeignKey("ContactCategoryId"); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.Navigation("ContactCategory"); + + b.Navigation("CreatedBy"); + + b.Navigation("Tenant"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactBucketMapping", b => + { + b.HasOne("Marco.Pms.Model.Directory.Bucket", "Bucket") + .WithMany() + .HasForeignKey("BucketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Directory.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Bucket"); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactCategoryMaster", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactEmail", b => + { + b.HasOne("Marco.Pms.Model.Directory.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactNote", b => + { + b.HasOne("Marco.Pms.Model.Directory.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "Createdby") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.Navigation("Contact"); + + b.Navigation("Createdby"); + + b.Navigation("Tenant"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactPhone", b => + { + b.HasOne("Marco.Pms.Model.Directory.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactProjectMapping", b => + { + b.HasOne("Marco.Pms.Model.Directory.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Projects.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contact"); + + b.Navigation("Project"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactTagMapping", b => + { + b.HasOne("Marco.Pms.Model.Directory.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Directory.ContactTagMaster", "ContactTag") + .WithMany() + .HasForeignKey("ContactTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contact"); + + b.Navigation("ContactTag"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactTagMaster", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.DirectoryUpdateLog", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.EmployeeBucketMapping", b => + { + b.HasOne("Marco.Pms.Model.Directory.Bucket", "Bucket") + .WithMany() + .HasForeignKey("BucketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Bucket"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Marco.Pms.Model.DocumentManager.Document", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "UploadedBy") + .WithMany() + .HasForeignKey("UploadedById"); + + b.Navigation("Tenant"); + + b.Navigation("UploadedBy"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Employees.Employee", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.ApplicationUser", "ApplicationUser") + .WithMany() + .HasForeignKey("ApplicationUserId"); + + b.HasOne("Marco.Pms.Model.Roles.JobRole", "JobRole") + .WithMany() + .HasForeignKey("JobRoleId"); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApplicationUser"); + + b.Navigation("JobRole"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Employees.EmployeeRoleMapping", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Roles.ApplicationRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("Role"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Employees.WorkShift", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.FeaturePermission", b => + { + b.HasOne("Marco.Pms.Model.Master.Feature", "Feature") + .WithMany("FeaturePermissions") + .HasForeignKey("FeatureId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Feature"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.RolePermissionMappings", b => + { + b.HasOne("Marco.Pms.Model.Roles.ApplicationRole", null) + .WithMany() + .HasForeignKey("ApplicationRoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.FeaturePermission", null) + .WithMany() + .HasForeignKey("FeaturePermissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.Tenant", b => + { + b.HasOne("Marco.Pms.Model.Master.Industry", "Industry") + .WithMany() + .HasForeignKey("IndustryId"); + + b.Navigation("Industry"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketAttachment", b => + { + b.HasOne("Marco.Pms.Model.Forum.TicketComment", "TicketComment") + .WithMany("Attachments") + .HasForeignKey("CommentId"); + + b.HasOne("Marco.Pms.Model.Forum.TicketForum", "Ticket") + .WithMany() + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ticket"); + + b.Navigation("TicketComment"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketComment", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketForum", b => + { + b.HasOne("Marco.Pms.Model.Master.TicketPriorityMaster", "Priority") + .WithMany() + .HasForeignKey("PriorityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Master.TicketStatusMaster", "TicketStatusMaster") + .WithMany() + .HasForeignKey("StatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Forum.TicketTypeMaster", "TicketTypeMaster") + .WithMany() + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Priority"); + + b.Navigation("Tenant"); + + b.Navigation("TicketStatusMaster"); + + b.Navigation("TicketTypeMaster"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketTag", b => + { + b.HasOne("Marco.Pms.Model.Master.TicketTagMaster", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Forum.TicketForum", "Ticket") + .WithMany() + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tag"); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Mail.MailDetails", b => + { + b.HasOne("Marco.Pms.Model.Mail.MailingList", "MailBody") + .WithMany() + .HasForeignKey("MailListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MailBody"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.ActivityMaster", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.Feature", b => + { + b.HasOne("Marco.Pms.Model.Master.Module", "Module") + .WithMany() + .HasForeignKey("ModuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Module"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.StatusMaster", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.WorkCategoryMaster", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.WorkStatusMaster", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.Building", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.Floor", b => + { + b.HasOne("Marco.Pms.Model.Projects.Building", "Building") + .WithMany() + .HasForeignKey("BuildingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Building"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.Project", b => + { + b.HasOne("Marco.Pms.Model.Master.StatusMaster", "ProjectStatus") + .WithMany() + .HasForeignKey("ProjectStatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProjectStatus"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.ProjectAllocation", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Projects.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("Project"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.WorkArea", b => + { + b.HasOne("Marco.Pms.Model.Projects.Floor", "Floor") + .WithMany() + .HasForeignKey("FloorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Floor"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.WorkItem", b => + { + b.HasOne("Marco.Pms.Model.Master.ActivityMaster", "ActivityMaster") + .WithMany() + .HasForeignKey("ActivityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Projects.WorkArea", "WorkArea") + .WithMany() + .HasForeignKey("WorkAreaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Master.WorkCategoryMaster", "WorkCategoryMaster") + .WithMany() + .HasForeignKey("WorkCategoryId"); + + b.Navigation("ActivityMaster"); + + b.Navigation("Tenant"); + + b.Navigation("WorkArea"); + + b.Navigation("WorkCategoryMaster"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Roles.ApplicationRole", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Marco.Pms.Model.Roles.JobRole", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketComment", b => + { + b.Navigation("Attachments"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.Feature", b => + { + b.Navigation("FeaturePermissions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Marco.Pms.DataAccess/Migrations/20250702042830_Added_UploadedBy_ForeginKey_In_Decuments_Table.cs b/Marco.Pms.DataAccess/Migrations/20250702042830_Added_UploadedBy_ForeginKey_In_Decuments_Table.cs new file mode 100644 index 0000000..fd31771 --- /dev/null +++ b/Marco.Pms.DataAccess/Migrations/20250702042830_Added_UploadedBy_ForeginKey_In_Decuments_Table.cs @@ -0,0 +1,50 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Marco.Pms.DataAccess.Migrations +{ + /// + public partial class Added_UploadedBy_ForeginKey_In_Decuments_Table : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UploadedById", + table: "Documents", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci"); + + migrationBuilder.CreateIndex( + name: "IX_Documents_UploadedById", + table: "Documents", + column: "UploadedById"); + + migrationBuilder.AddForeignKey( + name: "FK_Documents_Employees_UploadedById", + table: "Documents", + column: "UploadedById", + principalTable: "Employees", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Documents_Employees_UploadedById", + table: "Documents"); + + migrationBuilder.DropIndex( + name: "IX_Documents_UploadedById", + table: "Documents"); + + migrationBuilder.DropColumn( + name: "UploadedById", + table: "Documents"); + } + } +} diff --git a/Marco.Pms.DataAccess/Migrations/ApplicationDbContextModelSnapshot.cs b/Marco.Pms.DataAccess/Migrations/ApplicationDbContextModelSnapshot.cs index 26a3bdd..258f8bd 100644 --- a/Marco.Pms.DataAccess/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Marco.Pms.DataAccess/Migrations/ApplicationDbContextModelSnapshot.cs @@ -752,10 +752,15 @@ namespace Marco.Pms.DataAccess.Migrations b.Property("UploadedAt") .HasColumnType("datetime(6)"); + b.Property("UploadedById") + .HasColumnType("char(36)"); + b.HasKey("Id"); b.HasIndex("TenantId"); + b.HasIndex("UploadedById"); + b.ToTable("Documents"); }); @@ -2951,7 +2956,13 @@ namespace Marco.Pms.DataAccess.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("Marco.Pms.Model.Employees.Employee", "UploadedBy") + .WithMany() + .HasForeignKey("UploadedById"); + b.Navigation("Tenant"); + + b.Navigation("UploadedBy"); }); modelBuilder.Entity("Marco.Pms.Model.Employees.Employee", b => diff --git a/Marco.Pms.Model/DocumentManager/Document.cs b/Marco.Pms.Model/DocumentManager/Document.cs index 3652774..bcbe7a7 100644 --- a/Marco.Pms.Model/DocumentManager/Document.cs +++ b/Marco.Pms.Model/DocumentManager/Document.cs @@ -1,4 +1,7 @@ -using Marco.Pms.Model.Utilities; +using System.ComponentModel.DataAnnotations.Schema; +using Marco.Pms.Model.Employees; +using Marco.Pms.Model.Utilities; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; namespace Marco.Pms.Model.DocumentManager { @@ -16,10 +19,15 @@ namespace Marco.Pms.Model.DocumentManager /// public string? ThumbS3Key { get; set; } - public string? Base64Data { get; set; } + public string? Base64Data { get; set; } = null; public long FileSize { get; set; } public string ContentType { get; set; } = string.Empty; + public Guid? UploadedById { get; set; } + + [ValidateNever] + [ForeignKey("UploadedById")] + public Employee? UploadedBy { get; set; } public DateTime UploadedAt { get; set; } } } From b77a5b16cdbb3fcd19532c64ede073b82eed7346 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 2 Jul 2025 10:01:10 +0530 Subject: [PATCH 004/124] Storing batch ID when saving any images as well stop storing base64 in database --- Marco.Pms.Model/Mapper/ForumMapper.cs | 14 ++++++--- .../Controllers/AttendanceController.cs | 11 ++++--- .../Controllers/ForumController.cs | 31 +++++++++++++++---- .../Controllers/TaskController.cs | 20 +++++++++--- 4 files changed, 57 insertions(+), 19 deletions(-) diff --git a/Marco.Pms.Model/Mapper/ForumMapper.cs b/Marco.Pms.Model/Mapper/ForumMapper.cs index c5f84ba..cf15331 100644 --- a/Marco.Pms.Model/Mapper/ForumMapper.cs +++ b/Marco.Pms.Model/Mapper/ForumMapper.cs @@ -90,29 +90,35 @@ namespace Marco.Pms.Model.Mapper }; } - public static Document ToDocumentFromForumAttachmentDto(this ForumAttachmentDto AttachmentDto, string objectKey, string thumbS3Key, DateTime uploadedAt, Guid tenantId) + public static Document ToDocumentFromForumAttachmentDto(this ForumAttachmentDto AttachmentDto, string objectKey, string thumbS3Key, DateTime uploadedAt, + Guid tenantId, Guid batchId, Guid loggedInEmployeeId) { return new Document { + BatchId = batchId, + UploadedById = loggedInEmployeeId, FileName = AttachmentDto.FileName, ContentType = AttachmentDto.ContentType, S3Key = objectKey, ThumbS3Key = thumbS3Key, - Base64Data = AttachmentDto.Base64Data, + //Base64Data = AttachmentDto.Base64Data, FileSize = AttachmentDto.FileSize, UploadedAt = uploadedAt, TenantId = tenantId }; } - public static Document ToDocumentFromUpdateAttachmentDto(this UpdateAttachmentDto AttachmentDto, string objectKey, string thumbS3Key, DateTime uploadedAt, Guid tenantId) + public static Document ToDocumentFromUpdateAttachmentDto(this UpdateAttachmentDto AttachmentDto, string objectKey, string thumbS3Key, DateTime uploadedAt, + Guid tenantId, Guid batchId, Guid loggedInEmployeeId) { return new Document { + BatchId = batchId, + UploadedById = loggedInEmployeeId, FileName = AttachmentDto.FileName, ContentType = AttachmentDto.ContentType, S3Key = objectKey, ThumbS3Key = thumbS3Key, - Base64Data = AttachmentDto.Base64Data, + //Base64Data = AttachmentDto.Base64Data, FileSize = AttachmentDto.FileSize, UploadedAt = uploadedAt, TenantId = tenantId diff --git a/Marco.Pms.Services/Controllers/AttendanceController.cs b/Marco.Pms.Services/Controllers/AttendanceController.cs index d23a007..2622323 100644 --- a/Marco.Pms.Services/Controllers/AttendanceController.cs +++ b/Marco.Pms.Services/Controllers/AttendanceController.cs @@ -603,7 +603,8 @@ namespace MarcoBMS.Services.Controllers } Guid tenantId = GetTenantId(); - var currentEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var batchId = Guid.NewGuid(); using var transaction = await _context.Database.BeginTransactionAsync(); try @@ -704,10 +705,12 @@ namespace MarcoBMS.Services.Controllers document = new Document { + BatchId = batchId, + UploadedById = loggedInEmployee.Id, FileName = recordAttendanceDot.Image.FileName ?? "", ContentType = recordAttendanceDot.Image.ContentType, S3Key = objectKey, - Base64Data = recordAttendanceDot.Image.Base64Data, + //Base64Data = recordAttendanceDot.Image.Base64Data, FileSize = recordAttendanceDot.Image.FileSize, UploadedAt = recordAttendanceDot.Date, TenantId = tenantId @@ -728,7 +731,7 @@ namespace MarcoBMS.Services.Controllers Longitude = recordAttendanceDot.Longitude, DocumentId = document?.Id, TenantId = tenantId, - UpdatedBy = recordAttendanceDot.EmployeeID, + UpdatedBy = loggedInEmployee.Id, UpdatedOn = recordAttendanceDot.Date }; _context.AttendanceLogs.Add(attendanceLog); @@ -755,7 +758,7 @@ namespace MarcoBMS.Services.Controllers var notification = new { - LoggedInUserId = currentEmployee.Id, + LoggedInUserId = loggedInEmployee.Id, Keyword = "Attendance", Activity = recordAttendanceDot.Id == Guid.Empty ? 1 : 0, ProjectId = attendance.ProjectID, diff --git a/Marco.Pms.Services/Controllers/ForumController.cs b/Marco.Pms.Services/Controllers/ForumController.cs index f50a077..769c08a 100644 --- a/Marco.Pms.Services/Controllers/ForumController.cs +++ b/Marco.Pms.Services/Controllers/ForumController.cs @@ -48,6 +48,8 @@ namespace Marco.Pms.Services.Controllers return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } Guid tenantId = _userHelper.GetTenantId(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var batchId = Guid.NewGuid(); TicketForum ticketForum = createTicketDto.ToTicketForumFromCreateTicketDto(tenantId); _context.Tickets.Add(ticketForum); await _context.SaveChangesAsync(); @@ -79,7 +81,7 @@ namespace Marco.Pms.Services.Controllers string objectKey = $"tenant-{tenantId}/project-{createTicketDto.LinkedProjectId}/froum/{fileName}"; await _s3Service.UploadFileAsync(base64, fileType, objectKey); - Document document = attachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, createTicketDto.CreatedAt, tenantId); + Document document = attachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, createTicketDto.CreatedAt, tenantId, batchId, loggedInEmployee.Id); _context.Documents.Add(document); await _context.SaveChangesAsync(); @@ -162,7 +164,15 @@ namespace Marco.Pms.Services.Controllers return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } Guid tenantId = _userHelper.GetTenantId(); - var existingTicket = await _context.Tickets.Include(t => t.TicketTypeMaster).Include(t => t.TicketStatusMaster).Include(t => t.Priority).AsNoTracking().FirstOrDefaultAsync(t => t.Id == updateTicketDto.Id); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var batchId = Guid.NewGuid(); + + var existingTicket = await _context.Tickets + .Include(t => t.TicketTypeMaster) + .Include(t => t.TicketStatusMaster) + .Include(t => t.Priority) + .AsNoTracking() + .FirstOrDefaultAsync(t => t.Id == updateTicketDto.Id); if (existingTicket != null) { TicketForum ticketForum = updateTicketDto.ToTicketForumFromUpdateTicketDto(existingTicket); @@ -202,7 +212,7 @@ namespace Marco.Pms.Services.Controllers string objectKey = $"tenant-{tenantId}/project-{updateTicketDto.LinkedProjectId}/froum/{fileName}"; await _s3Service.UploadFileAsync(base64, fileType, objectKey); - Document document = attachmentDto.ToDocumentFromUpdateAttachmentDto(objectKey, objectKey, updateTicketDto.CreatedAt, tenantId); + Document document = attachmentDto.ToDocumentFromUpdateAttachmentDto(objectKey, objectKey, updateTicketDto.CreatedAt, tenantId, batchId, loggedInEmployee.Id); _context.Documents.Add(document); await _context.SaveChangesAsync(); @@ -344,6 +354,9 @@ namespace Marco.Pms.Services.Controllers } Guid tenantId = _userHelper.GetTenantId(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var batchId = Guid.NewGuid(); + List attachments = new List(); List documents = new List(); @@ -381,7 +394,7 @@ namespace Marco.Pms.Services.Controllers string objectKey = $"tenant-{tenantId}/project-{ticket.LinkedProjectId}/froum/{fileName}"; await _s3Service.UploadFileAsync(base64, fileType, objectKey); - Document document = attachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, addCommentDto.SentAt, tenantId); + Document document = attachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, addCommentDto.SentAt, tenantId, batchId, loggedInEmployee.Id); _context.Documents.Add(document); await _context.SaveChangesAsync(); @@ -429,6 +442,9 @@ namespace Marco.Pms.Services.Controllers } Guid tenantId = _userHelper.GetTenantId(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var batchId = Guid.NewGuid(); + List attachments = new List(); TicketForum? ticket = await _context.Tickets.FirstOrDefaultAsync(t => t.Id == updateCommentDto.TicketId); @@ -473,7 +489,7 @@ namespace Marco.Pms.Services.Controllers string objectKey = $"tenant-{tenantId}/project-{ticket.LinkedProjectId}/froum/{fileName}"; await _s3Service.UploadFileAsync(base64, fileType, objectKey); - Document document = attachmentDto.ToDocumentFromUpdateAttachmentDto(objectKey, objectKey, existingComment.SentAt, tenantId); + Document document = attachmentDto.ToDocumentFromUpdateAttachmentDto(objectKey, objectKey, existingComment.SentAt, tenantId, batchId, loggedInEmployee.Id); _context.Documents.Add(document); await _context.SaveChangesAsync(); @@ -541,6 +557,9 @@ namespace Marco.Pms.Services.Controllers } Guid tenantId = _userHelper.GetTenantId(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var batchId = Guid.NewGuid(); + List ticketAttachmentVMs = new List(); List ticketIds = forumAttachmentDtos.Select(f => f.TicketId.HasValue ? f.TicketId.Value : Guid.Empty).ToList(); @@ -579,7 +598,7 @@ namespace Marco.Pms.Services.Controllers string objectKey = $"tenant-{tenantId}/project-{ticket?.LinkedProjectId}/froum/{fileName}"; await _s3Service.UploadFileAsync(base64, fileType, objectKey); - Document document = forumAttachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, forumAttachmentDto.SentAt, tenantId); + Document document = forumAttachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, forumAttachmentDto.SentAt, tenantId, batchId, loggedInEmployee.Id); _context.Documents.Add(document); await _context.SaveChangesAsync(); diff --git a/Marco.Pms.Services/Controllers/TaskController.cs b/Marco.Pms.Services/Controllers/TaskController.cs index 6b55c3f..5a35baf 100644 --- a/Marco.Pms.Services/Controllers/TaskController.cs +++ b/Marco.Pms.Services/Controllers/TaskController.cs @@ -204,6 +204,7 @@ namespace MarcoBMS.Services.Controllers var building = await _context.Buildings .FirstOrDefaultAsync(b => b.Id == buildingId); + var batchId = Guid.NewGuid(); foreach (var image in reportTask.Images) { @@ -225,10 +226,12 @@ namespace MarcoBMS.Services.Controllers var document = new Document { + BatchId = batchId, + UploadedById = loggedInEmployee.Id, FileName = image.FileName ?? "", ContentType = image.ContentType ?? "", S3Key = objectKey, - Base64Data = image.Base64Data, + //Base64Data = image.Base64Data, FileSize = image.FileSize, UploadedAt = DateTime.UtcNow, TenantId = tenantId @@ -265,7 +268,7 @@ namespace MarcoBMS.Services.Controllers _logger.LogInfo("AddCommentForTask called for TaskAllocationId: {TaskId}", createComment.TaskAllocationId); var tenantId = GetTenantId(); - var employee = await _userHelper.GetCurrentEmployeeAsync(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); // Validate Task Allocation and associated WorkItem var taskAllocation = await _context.TaskAllocations @@ -287,13 +290,14 @@ namespace MarcoBMS.Services.Controllers var building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == buildingId); // Save comment - var comment = createComment.ToCommentFromCommentDto(tenantId, employee.Id); + var comment = createComment.ToCommentFromCommentDto(tenantId, loggedInEmployee.Id); _context.TaskComments.Add(comment); await _context.SaveChangesAsync(); _logger.LogInfo("Comment saved with Id: {CommentId}", comment.Id); // Process image uploads var images = createComment.Images; + var batchId = Guid.NewGuid(); if (images != null && images.Any()) { @@ -319,10 +323,12 @@ namespace MarcoBMS.Services.Controllers var document = new Document { + BatchId = batchId, + UploadedById = loggedInEmployee.Id, FileName = image.FileName ?? string.Empty, ContentType = image.ContentType ?? fileType, S3Key = objectKey, - Base64Data = image.Base64Data, + //Base64Data = image.Base64Data, FileSize = image.FileSize, UploadedAt = DateTime.UtcNow, TenantId = tenantId @@ -731,6 +737,8 @@ namespace MarcoBMS.Services.Controllers var building = await _context.Buildings .FirstOrDefaultAsync(b => b.Id == buildingId); + var batchId = Guid.NewGuid(); + foreach (var image in approveTask.Images) { if (string.IsNullOrEmpty(image.Base64Data)) @@ -749,10 +757,12 @@ namespace MarcoBMS.Services.Controllers var document = new Document { + BatchId = batchId, + UploadedById = loggedInEmployee.Id, FileName = fileName, ContentType = image.ContentType ?? string.Empty, S3Key = objectKey, - Base64Data = image.Base64Data, + //Base64Data = image.Base64Data, FileSize = image.FileSize, UploadedAt = DateTime.UtcNow, TenantId = tenantId From 587e8d2b0b6c341b31122bab4ebd8105e3d10964 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 2 Jul 2025 10:02:22 +0530 Subject: [PATCH 005/124] Added an API to get list of images in provided project. --- .../Controllers/ImageController.cs | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 Marco.Pms.Services/Controllers/ImageController.cs diff --git a/Marco.Pms.Services/Controllers/ImageController.cs b/Marco.Pms.Services/Controllers/ImageController.cs new file mode 100644 index 0000000..7a4c556 --- /dev/null +++ b/Marco.Pms.Services/Controllers/ImageController.cs @@ -0,0 +1,171 @@ +using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.Employees; +using Marco.Pms.Model.Mapper; +using Marco.Pms.Model.Utilities; +using Marco.Pms.Services.Service; +using MarcoBMS.Services.Helpers; +using MarcoBMS.Services.Service; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Marco.Pms.Services.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class ImageController : ControllerBase + { + private readonly ApplicationDbContext _context; + private readonly S3UploadService _s3Service; + private readonly UserHelper _userHelper; + private readonly ILoggingService _logger; + private readonly PermissionServices _permission; + private readonly Guid tenantId; + public ImageController(ApplicationDbContext context, S3UploadService s3Service, UserHelper userHelper, ILoggingService logger, PermissionServices permission) + { + _context = context; + _s3Service = s3Service; + _userHelper = userHelper; + _logger = logger; + tenantId = userHelper.GetTenantId(); + _permission = permission; + } + + [HttpGet("images/{projectId}")] + public async Task GetImageList(Guid projectId) + { + _logger.LogInfo("GetImageList called for ProjectId: {ProjectId}", projectId); + + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // Step 1: Validate project existence + var isProjectExist = await _context.Projects.AnyAsync(p => p.Id == projectId && p.TenantId == tenantId); + if (!isProjectExist) + { + _logger.LogWarning("Project not found for ProjectId: {ProjectId}", projectId); + return BadRequest(ApiResponse.ErrorResponse("Project not found", "Project not found in database", 400)); + } + + // Step 2: Check permission + var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.ToString()); + if (!hasPermission) + { + _logger.LogWarning("No access to ProjectId: {ProjectId} for EmployeeId: {EmployeeId}", projectId, loggedInEmployee.Id); + return StatusCode(403, ApiResponse.ErrorResponse("You don't have access", "You don't have access", 403)); + } + + // Step 3: Fetch building > floor > work area > work item hierarchy + var buildings = await _context.Buildings + .Where(b => b.ProjectId == projectId) + .Select(b => new { b.Id, b.Name }) + .ToListAsync(); + + var buildingIds = buildings.Select(b => b.Id).ToList(); + + var floors = await _context.Floor + .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 + .Where(wa => floorIds.Contains(wa.FloorId)) + .Select(wa => new { wa.Id, wa.FloorId, wa.AreaName }) + .ToListAsync(); + + var workAreaIds = workAreas.Select(wa => wa.Id).ToList(); + + var workItems = await _context.WorkItems + .Include(wi => wi.ActivityMaster) + .Where(wi => workAreaIds.Contains(wi.WorkAreaId)) + .Select(wi => new { wi.Id, wi.WorkAreaId, wi.ActivityMaster }) + .ToListAsync(); + + var workItemIds = workItems.Select(wi => wi.Id).ToList(); + + // Step 4: Fetch task and comment data + var tasks = await _context.TaskAllocations + .Include(t => t.ReportedBy) + .Where(t => workItemIds.Contains(t.WorkItemId)) + .ToListAsync(); + + var taskIds = tasks.Select(t => t.Id).ToList(); + + var comments = await _context.TaskComments + .Include(c => c.Employee) + .Where(c => taskIds.Contains(c.TaskAllocationId)) + .ToListAsync(); + + var commentIds = comments.Select(c => c.Id).ToList(); + + // Step 5: Fetch attachments and related documents + var attachments = await _context.TaskAttachments + .Where(ta => taskIds.Contains(ta.ReferenceId) || commentIds.Contains(ta.ReferenceId)) + .ToListAsync(); + + var documentIds = attachments.Select(ta => ta.DocumentId).ToList(); + + var documents = await _context.Documents + .Include(d => d.UploadedBy) + .Where(d => documentIds.Contains(d.Id)) + .ToListAsync(); + + // Step 6: Prepare view models + var documentVM = documents + .Select(d => + { + var referenceId = attachments + .Where(ta => ta.DocumentId == d.Id) + .Select(ta => ta.ReferenceId) + .FirstOrDefault(); + + var task = tasks.FirstOrDefault(t => t.Id == referenceId); + var comment = comments.FirstOrDefault(c => c.Id == referenceId); + + string source = ""; + Employee? uploadedBy = null; + if (task != null) + { + uploadedBy = task.ReportedBy; + source = "Report"; + } + else if (comment != null) + { + task = tasks.FirstOrDefault(t => t.Id == comment.TaskAllocationId); + uploadedBy = comment.Employee; + source = "Comment"; + } + + var workItem = workItems.FirstOrDefault(wi => wi.Id == task?.WorkItemId); + var workArea = workAreas.FirstOrDefault(wa => wa.Id == workItem?.WorkAreaId); + var floor = floors.FirstOrDefault(f => f.Id == workArea?.FloorId); + var building = buildings.FirstOrDefault(b => b.Id == floor?.BuildingId); + + return new + { + Id = d.Id, + BatchId = d.BatchId, + thumbnailUrl = d.ThumbS3Key != null ? _s3Service.GeneratePreSignedUrlAsync(d.ThumbS3Key) : (d.S3Key != null ? _s3Service.GeneratePreSignedUrlAsync(d.S3Key) : null), + ImageUrl = d.S3Key != null ? _s3Service.GeneratePreSignedUrlAsync(d.S3Key) : null, + UploadedBy = d.UploadedBy?.ToBasicEmployeeVMFromEmployee() ?? uploadedBy?.ToBasicEmployeeVMFromEmployee(), + UploadedAt = d.UploadedAt, + Source = source, + ProjectId = projectId, + BuildingId = building?.Id, + BuildingName = building?.Name, + FloorIds = floor?.Id, + FloorName = floor?.FloorName, + WorkAreaId = workArea?.Id, + WorkAreaName = workArea?.AreaName, + TaskId = task?.Id, + ActivityName = workItem?.ActivityMaster?.ActivityName, + CommentId = comment?.Id, + Comment = comment?.Comment + }; + }).ToList(); + + _logger.LogInfo("Image list fetched for ProjectId: {ProjectId}. Total documents: {Count}", projectId, documentVM.Count); + return Ok(ApiResponse.SuccessResponse(documentVM, $"{documentVM.Count} image records fetched successfully", 200)); + } + } +} From afdf51eae3b3a0353b41e0ac6b27f8f718b3e2a4 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 2 Jul 2025 10:03:20 +0530 Subject: [PATCH 006/124] Added an API to get list of Images for provided batch ID --- .../Controllers/ImageController.cs | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/Marco.Pms.Services/Controllers/ImageController.cs b/Marco.Pms.Services/Controllers/ImageController.cs index 7a4c556..2a1b057 100644 --- a/Marco.Pms.Services/Controllers/ImageController.cs +++ b/Marco.Pms.Services/Controllers/ImageController.cs @@ -1,6 +1,8 @@ using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.Activities; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Mapper; +using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; using Marco.Pms.Services.Service; using MarcoBMS.Services.Helpers; @@ -167,5 +169,108 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Image list fetched for ProjectId: {ProjectId}. Total documents: {Count}", projectId, documentVM.Count); return Ok(ApiResponse.SuccessResponse(documentVM, $"{documentVM.Count} image records fetched successfully", 200)); } + + [HttpGet("batch/{batchId}")] + public async Task GetImagesByBatch(Guid batchId) + { + _logger.LogInfo("GetImagesByBatch called for BatchId: {BatchId}", batchId); + + // Step 1: Get the logged-in employee + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // Step 2: Retrieve all documents in the batch + var documents = await _context.Documents + .Include(d => d.UploadedBy) + .Where(d => d.BatchId == batchId) + .ToListAsync(); + + if (!documents.Any()) + { + _logger.LogWarning("No documents found for BatchId: {BatchId}", batchId); + return NotFound(ApiResponse.ErrorResponse("No images found", "No images associated with this batch", 404)); + } + + var documentIds = documents.Select(d => d.Id).ToList(); + + // Step 3: Get task/comment reference IDs linked to these documents + var referenceIds = await _context.TaskAttachments + .Where(ta => documentIds.Contains(ta.DocumentId)) + .Select(ta => ta.ReferenceId) + .Distinct() + .ToListAsync(); + + // Step 4: Try to identify the source of the attachment (task or comment) + var task = await _context.TaskAllocations + .Include(t => t.ReportedBy) + .FirstOrDefaultAsync(t => referenceIds.Contains(t.Id)); + + TaskComment? comment = null; + WorkItem? workItem = null; + Employee? uploadedBy = null; + string source = ""; + + if (task != null) + { + uploadedBy = task.ReportedBy; + workItem = await _context.WorkItems + .Include(wi => wi.ActivityMaster) + .FirstOrDefaultAsync(wi => wi.Id == task.WorkItemId); + source = "Report"; + } + else + { + comment = await _context.TaskComments + .Include(tc => tc.TaskAllocation) + .Include(tc => tc.Employee) + .FirstOrDefaultAsync(tc => referenceIds.Contains(tc.Id)); + var workItemId = comment?.TaskAllocation?.WorkItemId; + + uploadedBy = comment?.Employee; + workItem = await _context.WorkItems + .Include(wi => wi.ActivityMaster) + .FirstOrDefaultAsync(wi => wi.Id == workItemId); + source = "Comment"; + } + + // Step 5: Traverse up to building level + var workAreaId = workItem?.WorkAreaId; + var workArea = await _context.WorkAreas + .Include(wa => wa.Floor) + .FirstOrDefaultAsync(wa => wa.Id == workAreaId); + + var buildingId = workArea?.Floor?.BuildingId; + var building = await _context.Buildings + .FirstOrDefaultAsync(b => b.Id == buildingId); + + // Step 6: Construct the response + var response = documents.Select(d => new + { + Id = d.Id, + BatchId = d.BatchId, + thumbnailUrl = d.ThumbS3Key != null + ? _s3Service.GeneratePreSignedUrlAsync(d.ThumbS3Key) + : (d.S3Key != null ? _s3Service.GeneratePreSignedUrlAsync(d.S3Key) : null), + ImageUrl = d.S3Key != null ? _s3Service.GeneratePreSignedUrlAsync(d.S3Key) : null, + UploadedBy = d.UploadedBy?.ToBasicEmployeeVMFromEmployee() ?? uploadedBy?.ToBasicEmployeeVMFromEmployee(), + UploadedAt = d.UploadedAt, + Source = source, + ProjectId = building?.ProjectId, + BuildingId = building?.Id, + BuildingName = building?.Name, + FloorIds = workArea?.Floor?.Id, + FloorName = workArea?.Floor?.FloorName, + WorkAreaId = workArea?.Id, + WorkAreaName = workArea?.AreaName, + TaskId = task?.Id, + ActivityName = workItem?.ActivityMaster?.ActivityName, + CommentId = comment?.Id, + Comment = comment?.Comment + }).ToList(); + + _logger.LogInfo("Fetched {Count} image(s) for BatchId: {BatchId}", response.Count, batchId); + + return Ok(ApiResponse.SuccessResponse(response, "Images for provided batchId fetched successfully", 200)); + } + } } From 85911c4536a0c12443cc6ca5dcb7374534cb1504 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 2 Jul 2025 10:04:00 +0530 Subject: [PATCH 007/124] Added an API to get persigned url for provided document --- .../Controllers/ImageController.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/Marco.Pms.Services/Controllers/ImageController.cs b/Marco.Pms.Services/Controllers/ImageController.cs index 2a1b057..fedc067 100644 --- a/Marco.Pms.Services/Controllers/ImageController.cs +++ b/Marco.Pms.Services/Controllers/ImageController.cs @@ -272,5 +272,44 @@ namespace Marco.Pms.Services.Controllers return Ok(ApiResponse.SuccessResponse(response, "Images for provided batchId fetched successfully", 200)); } + [HttpGet("{documentId}")] + public async Task GetImage(Guid documentId) + { + // Log the start of the image fetch process + _logger.LogInfo("GetImage called for DocumentId: {DocumentId}", documentId); + + // Step 1: Get the currently logged-in employee (for future use like permission checks or auditing) + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // Step 2: Fetch the document from the database based on the provided ID + var document = await _context.Documents.FirstOrDefaultAsync(d => d.Id == documentId); + + // Step 3: If document doesn't exist, return a 400 Bad Request response + if (document == null) + { + _logger.LogWarning("Document not found for DocumentId: {DocumentId}", documentId); + return BadRequest(ApiResponse.ErrorResponse("Document not found", "Document not found", 400)); + } + + // Step 4: Generate pre-signed URLs for thumbnail and full image (if keys exist) + string? thumbnailUrl = document.ThumbS3Key != null + ? _s3Service.GeneratePreSignedUrlAsync(document.ThumbS3Key) + : null; + + string? imageUrl = document.S3Key != null + ? _s3Service.GeneratePreSignedUrlAsync(document.S3Key) + : null; + + // Step 5: Prepare the response object + var response = new + { + ThumbnailUrl = thumbnailUrl, + ImageUrl = imageUrl + }; + + // Step 6: Log successful fetch and return the result + _logger.LogInfo("Image fetched successfully for DocumentId: {DocumentId}", documentId); + return Ok(ApiResponse.SuccessResponse(response, "Image fetched successfully", 200)); + } } } From 8353c384a5916a3fb0d5b205e98f9ebccfcb864e Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 2 Jul 2025 10:15:00 +0530 Subject: [PATCH 008/124] Added athorization in controller --- Marco.Pms.Services/Controllers/ImageController.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Marco.Pms.Services/Controllers/ImageController.cs b/Marco.Pms.Services/Controllers/ImageController.cs index fedc067..635d357 100644 --- a/Marco.Pms.Services/Controllers/ImageController.cs +++ b/Marco.Pms.Services/Controllers/ImageController.cs @@ -7,6 +7,7 @@ using Marco.Pms.Model.Utilities; using Marco.Pms.Services.Service; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -14,6 +15,7 @@ namespace Marco.Pms.Services.Controllers { [Route("api/[controller]")] [ApiController] + [Authorize] public class ImageController : ControllerBase { private readonly ApplicationDbContext _context; From 1f5a71ef092ad64b72899cad369c35c49b9775a6 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 2 Jul 2025 13:15:28 +0530 Subject: [PATCH 009/124] Added WorkCategoryName and WorkCategoryId Parameters in view models --- Marco.Pms.Services/Controllers/ImageController.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Marco.Pms.Services/Controllers/ImageController.cs b/Marco.Pms.Services/Controllers/ImageController.cs index 635d357..19af70f 100644 --- a/Marco.Pms.Services/Controllers/ImageController.cs +++ b/Marco.Pms.Services/Controllers/ImageController.cs @@ -81,8 +81,9 @@ namespace Marco.Pms.Services.Controllers var workItems = await _context.WorkItems .Include(wi => wi.ActivityMaster) + .Include(wi => wi.WorkCategoryMaster) .Where(wi => workAreaIds.Contains(wi.WorkAreaId)) - .Select(wi => new { wi.Id, wi.WorkAreaId, wi.ActivityMaster }) + .Select(wi => new { wi.Id, wi.WorkAreaId, wi.ActivityMaster, wi.WorkCategoryMaster }) .ToListAsync(); var workItemIds = workItems.Select(wi => wi.Id).ToList(); @@ -163,6 +164,8 @@ namespace Marco.Pms.Services.Controllers WorkAreaName = workArea?.AreaName, TaskId = task?.Id, ActivityName = workItem?.ActivityMaster?.ActivityName, + WorkCategoryId = workItem?.WorkCategoryMaster?.Id, + WorkCategoryName = workItem?.WorkCategoryMaster?.Name, CommentId = comment?.Id, Comment = comment?.Comment }; @@ -216,6 +219,7 @@ namespace Marco.Pms.Services.Controllers uploadedBy = task.ReportedBy; workItem = await _context.WorkItems .Include(wi => wi.ActivityMaster) + .Include(wi => wi.WorkCategoryMaster) .FirstOrDefaultAsync(wi => wi.Id == task.WorkItemId); source = "Report"; } @@ -230,6 +234,7 @@ namespace Marco.Pms.Services.Controllers uploadedBy = comment?.Employee; workItem = await _context.WorkItems .Include(wi => wi.ActivityMaster) + .Include(wi => wi.WorkCategoryMaster) .FirstOrDefaultAsync(wi => wi.Id == workItemId); source = "Comment"; } @@ -265,6 +270,8 @@ namespace Marco.Pms.Services.Controllers WorkAreaName = workArea?.AreaName, TaskId = task?.Id, ActivityName = workItem?.ActivityMaster?.ActivityName, + WorkCategoryId = workItem?.WorkCategoryMaster?.Id, + WorkCategoryName = workItem?.WorkCategoryMaster?.Name, CommentId = comment?.Id, Comment = comment?.Comment }).ToList(); From 3216318acb02fc31dd8c2e6c77c24cb1e1a6726a Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 2 Jul 2025 15:07:08 +0530 Subject: [PATCH 010/124] Added Designation Parameter in Contacts and Implement in Related APIs --- ...on_Paraneter_In_Contacts_Table.Designer.cs | 3419 +++++++++++++++++ ...Designation_Paraneter_In_Contacts_Table.cs | 29 + .../ApplicationDbContextModelSnapshot.cs | 4 + Marco.Pms.Model/Directory/Contact.cs | 1 + .../Dtos/Directory/CreateContactDto.cs | 1 + .../Dtos/Directory/UpdateContactDto.cs | 1 + Marco.Pms.Model/Mapper/DirectoryMapper.cs | 4 + .../ViewModels/Directory/ContactProfileVM.cs | 1 + .../ViewModels/Directory/ContactVM.cs | 1 + .../Controllers/DirectoryController.cs | 75 +- Marco.Pms.Services/Helpers/DirectoryHelper.cs | 126 +- 11 files changed, 3571 insertions(+), 91 deletions(-) create mode 100644 Marco.Pms.DataAccess/Migrations/20250702045931_Added_Designation_Paraneter_In_Contacts_Table.Designer.cs create mode 100644 Marco.Pms.DataAccess/Migrations/20250702045931_Added_Designation_Paraneter_In_Contacts_Table.cs diff --git a/Marco.Pms.DataAccess/Migrations/20250702045931_Added_Designation_Paraneter_In_Contacts_Table.Designer.cs b/Marco.Pms.DataAccess/Migrations/20250702045931_Added_Designation_Paraneter_In_Contacts_Table.Designer.cs new file mode 100644 index 0000000..3cd5d28 --- /dev/null +++ b/Marco.Pms.DataAccess/Migrations/20250702045931_Added_Designation_Paraneter_In_Contacts_Table.Designer.cs @@ -0,0 +1,3419 @@ +// +using System; +using Marco.Pms.DataAccess.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Marco.Pms.DataAccess.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250702045931_Added_Designation_Paraneter_In_Contacts_Table")] + partial class Added_Designation_Paraneter_In_Contacts_Table + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + //MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskAllocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApprovedById") + .HasColumnType("char(36)"); + + b.Property("ApprovedDate") + .HasColumnType("datetime(6)"); + + b.Property("AssignedBy") + .HasColumnType("char(36)"); + + b.Property("AssignmentDate") + .HasColumnType("datetime(6)"); + + b.Property("CompletedTask") + .HasColumnType("double"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ParentTaskId") + .HasColumnType("char(36)"); + + b.Property("PlannedTask") + .HasColumnType("double"); + + b.Property("ReportedById") + .HasColumnType("char(36)"); + + b.Property("ReportedDate") + .HasColumnType("datetime(6)"); + + b.Property("ReportedTask") + .HasColumnType("double"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("WorkItemId") + .HasColumnType("char(36)"); + + b.Property("WorkStatusId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedById"); + + b.HasIndex("AssignedBy"); + + b.HasIndex("ReportedById"); + + b.HasIndex("TenantId"); + + b.HasIndex("WorkItemId"); + + b.HasIndex("WorkStatusId"); + + b.ToTable("TaskAllocations"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("DocumentId") + .HasColumnType("char(36)"); + + b.Property("ReferenceId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("TaskAttachments"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CommentDate") + .HasColumnType("datetime(6)"); + + b.Property("CommentedBy") + .HasColumnType("char(36)"); + + b.Property("TaskAllocationId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("CommentedBy"); + + b.HasIndex("TaskAllocationId"); + + b.HasIndex("TenantId"); + + b.ToTable("TaskComments"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskMembers", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("EmployeeId") + .HasColumnType("char(36)"); + + b.Property("TaskAllocationId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("TaskAllocationId"); + + b.HasIndex("TenantId"); + + b.ToTable("TaskMembers"); + }); + + modelBuilder.Entity("Marco.Pms.Model.AttendanceModule.Attendance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Activity") + .HasColumnType("int"); + + b.Property("ApprovedBy") + .HasColumnType("char(36)"); + + b.Property("AttendanceDate") + .HasColumnType("datetime(6)"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("EmployeeID") + .HasColumnType("char(36)"); + + b.Property("InTime") + .HasColumnType("datetime(6)"); + + b.Property("IsApproved") + .HasColumnType("tinyint(1)"); + + b.Property("OutTime") + .HasColumnType("datetime(6)"); + + b.Property("ProjectID") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeID"); + + b.HasIndex("TenantId"); + + b.ToTable("Attendes"); + }); + + modelBuilder.Entity("Marco.Pms.Model.AttendanceModule.AttendanceLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Activity") + .HasColumnType("int"); + + b.Property("ActivityTime") + .HasColumnType("datetime(6)"); + + b.Property("AttendanceId") + .HasColumnType("char(36)"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("char(36)"); + + b.Property("EmployeeID") + .HasColumnType("char(36)"); + + b.Property("Latitude") + .HasColumnType("longtext"); + + b.Property("Longitude") + .HasColumnType("longtext"); + + b.Property("Photo") + .HasColumnType("longblob"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("UpdatedBy") + .HasColumnType("char(36)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AttendanceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("EmployeeID"); + + b.HasIndex("TenantId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("AttendanceLogs"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Authentication.MPINDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("MPIN") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MPINToken") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("TimeStamp") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("MPINDetails"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Authentication.OTPDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ExpriesInSec") + .HasColumnType("int"); + + b.Property("IsUsed") + .HasColumnType("tinyint(1)"); + + b.Property("OTP") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("TimeStamp") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("OTPDetails"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Authentication.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExpiryDate") + .HasColumnType("datetime(6)"); + + b.Property("IsRevoked") + .HasColumnType("tinyint(1)"); + + b.Property("IsUsed") + .HasColumnType("tinyint(1)"); + + b.Property("RevokedAt") + .HasColumnType("datetime(6)"); + + b.Property("Token") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.Bucket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedByID") + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByID"); + + b.HasIndex("TenantId"); + + b.ToTable("Buckets"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Address") + .HasColumnType("longtext"); + + b.Property("ContactCategoryId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedById") + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Designation") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Organization") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedById") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ContactCategoryId"); + + b.HasIndex("CreatedById"); + + b.HasIndex("TenantId"); + + b.HasIndex("UpdatedById"); + + b.ToTable("Contacts"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactBucketMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BucketId") + .HasColumnType("char(36)"); + + b.Property("ContactId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("BucketId"); + + b.HasIndex("ContactId"); + + b.ToTable("ContactBucketMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactCategoryMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("ContactCategoryMasters"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactEmail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactId") + .HasColumnType("char(36)"); + + b.Property("EmailAddress") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)"); + + b.Property("Label") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.ToTable("ContactsEmails"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedById") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Note") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedById") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.HasIndex("CreatedById"); + + b.HasIndex("TenantId"); + + b.HasIndex("UpdatedById"); + + b.ToTable("ContactNotes"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactPhone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactId") + .HasColumnType("char(36)"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)"); + + b.Property("Label") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.ToTable("ContactsPhones"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactProjectMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactId") + .HasColumnType("char(36)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("TenantId"); + + b.ToTable("ContactProjectMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactTagMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactId") + .HasColumnType("char(36)"); + + b.Property("ContactTagId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.HasIndex("ContactTagId"); + + b.ToTable("ContactTagMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactTagMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("ContactTagMasters"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.DirectoryUpdateLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("RefereanceId") + .HasColumnType("char(36)"); + + b.Property("UpdateAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedById") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UpdatedById"); + + b.ToTable("DirectoryUpdateLogs"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.EmployeeBucketMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BucketId") + .HasColumnType("char(36)"); + + b.Property("EmployeeId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("BucketId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("EmployeeBucketMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.DocumentManager.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Base64Data") + .HasColumnType("longtext"); + + b.Property("BatchId") + .HasColumnType("char(36)"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("S3Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("ThumbS3Key") + .HasColumnType("longtext"); + + b.Property("UploadedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Employees.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AadharNumber") + .HasColumnType("longtext"); + + b.Property("ApplicationUserId") + .HasColumnType("varchar(255)"); + + b.Property("BirthDate") + .HasColumnType("datetime(6)"); + + b.Property("CurrentAddress") + .HasColumnType("longtext"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("EmergencyContactPerson") + .HasColumnType("longtext"); + + b.Property("EmergencyPhoneNumber") + .HasColumnType("longtext"); + + b.Property("FirstName") + .HasColumnType("longtext"); + + b.Property("Gender") + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("JobRoleId") + .HasColumnType("char(36)"); + + b.Property("JoiningDate") + .HasColumnType("datetime(6)"); + + b.Property("LastName") + .HasColumnType("longtext"); + + b.Property("MiddleName") + .HasColumnType("longtext"); + + b.Property("PanNumber") + .HasColumnType("longtext"); + + b.Property("PermanentAddress") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("Photo") + .HasColumnType("longblob"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.HasIndex("JobRoleId"); + + b.HasIndex("TenantId"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Employees.EmployeeRoleMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("EmployeeId") + .HasColumnType("char(36)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("RoleId"); + + b.HasIndex("TenantId"); + + b.ToTable("EmployeeRoleMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Employees.WorkShift", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("EndTime") + .HasColumnType("time(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("StartTime") + .HasColumnType("time(6)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("WorkShifts"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.ActivityCheckList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ActivityId") + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IsChecked") + .HasColumnType("tinyint(1)"); + + b.Property("IsMandatory") + .HasColumnType("tinyint(1)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("ActivityCheckLists"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.CheckListMappings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CheckListId") + .HasColumnType("char(36)"); + + b.Property("TaskAllocationId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("CheckListMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.FeaturePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("FeatureId") + .HasColumnType("char(36)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FeatureId"); + + b.ToTable("FeaturePermissions"); + + b.HasData( + new + { + Id = new Guid("6ea44136-987e-44ba-9e5d-1cf8f5837ebc"), + Description = "Access all information related to the project.", + FeatureId = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), + IsEnabled = true, + Name = "View Project" + }, + new + { + Id = new Guid("172fc9b6-755b-4f62-ab26-55c34a330614"), + Description = "Potentially edit the project name, description, start/end dates, or status.", + FeatureId = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), + IsEnabled = true, + Name = "Manage Project" + }, + new + { + Id = new Guid("b94802ce-0689-4643-9e1d-11c86950c35b"), + Description = "The \"Manage Team\" feature allows authorized users to organize project personnel by adding, removing, and assigning employee to projects.", + FeatureId = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), + IsEnabled = true, + Name = "Manage Team" + }, + new + { + Id = new Guid("8d7cc6e3-9147-41f7-aaa7-fa507e450bd4"), + Description = "Grants a user comprehensive read-only access to all details concerning the project's underlying systems, technologies, resources, and configurations", + FeatureId = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), + IsEnabled = true, + Name = "View Project Infra" + }, + new + { + Id = new Guid("cf2825ad-453b-46aa-91d9-27c124d63373"), + Description = "This allows them to create, modify, and manage all aspects of the supporting infrastructure.", + FeatureId = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), + IsEnabled = true, + Name = "Manage Project Infra" + }, + new + { + Id = new Guid("9fcc5f87-25e3-4846-90ac-67a71ab92e3c"), + Description = "Grants a user comprehensive read-only access to all details associated with tasks within a project. This includes task descriptions, statuses, assignees, due dates, dependencies, progress, history, and any related attachments or discussions.", + FeatureId = new Guid("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"), + IsEnabled = true, + Name = "View Task" + }, + new + { + Id = new Guid("08752f33-3b29-4816-b76b-ea8a968ed3c5"), + Description = "This allows them to create new tasks, modify existing task attributes (description, status, assignee, due date, etc.),", + FeatureId = new Guid("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"), + IsEnabled = true, + Name = "Add/Edit Task" + }, + new + { + Id = new Guid("6a32379b-8b3f-49a6-8c48-4b7ac1b55dc2"), + Description = "Grants a user the ability to designate team members responsible for specific tasks and to update the completion status or provide progress updates for those tasks", + FeatureId = new Guid("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"), + IsEnabled = true, + Name = "Assign/Report Progress" + }, + new + { + Id = new Guid("db4e40c5-2ba9-4b6d-b8a6-a16a250ff99c"), + Description = "Grants a user the authority to officially confirm the completion or acceptance of a task, often signifying that it meets the required standards or criteria", + FeatureId = new Guid("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"), + IsEnabled = true, + Name = "Approve Task" + }, + new + { + Id = new Guid("60611762-7f8a-4fb5-b53f-b1139918796b"), + Description = "Grants a user read-only access to details about the all individuals within the system. This typically includes names, contact information, roles, departments, and potentially other relevant employee data", + FeatureId = new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), + IsEnabled = true, + Name = "View All Employees" + }, + new + { + Id = new Guid("b82d2b7e-0d52-45f3-997b-c008ea460e7f"), + Description = "Grants a user read-only access to details about the individuals within the system which are is assigned to same projects as user. This typically includes names, contact information, roles, departments, and potentially other relevant employee data", + FeatureId = new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), + IsEnabled = true, + Name = "View Team Members" + }, + new + { + Id = new Guid("a97d366a-c2bb-448d-be93-402bd2324566"), + Description = "Grants a user the authority to create new employee profiles and modify existing employee details within the system. This typically includes adding or updating information such as names, contact details, roles, departments, skills, and potentially other personal or professional data", + FeatureId = new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), + IsEnabled = true, + Name = "Add/Edit Employee" + }, + new + { + Id = new Guid("fbd213e0-0250-46f1-9f5f-4b2a1e6e76a3"), + Description = "Grants a user the authority to manage employee application roles, enabling them to assign or revoke access privileges within the system.", + FeatureId = new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), + IsEnabled = true, + Name = "Assign Roles" + }, + new + { + Id = new Guid("915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"), + Description = "Team Attendance refers to tracking and managing the attendance of all team members collectively, often monitored by a team lead or manager.", + FeatureId = new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), + IsEnabled = true, + Name = "Team Attendance " + }, + new + { + Id = new Guid("57802c4a-00aa-4a1f-a048-fd2f70dd44b6"), + Description = "Grants a user the authority to approve requests from employees to adjust or correct their recorded attendance. This typically involves reviewing the reason for the regularization, verifying any supporting documentation, and then officially accepting the changes to the employee's attendance records", + FeatureId = new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), + IsEnabled = true, + Name = "Regularize Attendance" + }, + new + { + Id = new Guid("ccb0589f-712b-43de-92ed-5b6088e7dc4e"), + Description = "Team Attendance refers to tracking and managing the attendance of all team members collectively, often monitored by a team lead or manager.", + FeatureId = new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), + IsEnabled = true, + Name = "Self Attendance" + }, + new + { + Id = new Guid("5ffbafe0-7ab0-48b1-bb50-c1bf76b65f9d"), + Description = "Grants a user read-only access to foundational or reference data within the system. \"Masters\" typically refer to predefined lists, categories, or templates that are used throughout the application to standardize information and maintain consistency", + FeatureId = new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), + IsEnabled = true, + Name = "View Masters" + }, + new + { + Id = new Guid("588a8824-f924-4955-82d8-fc51956cf323"), + Description = "Grants a user the authority to create, modify, and delete foundational or reference data within the system. These \"masters\" are typically the core lists, categories, and configurations that other data and functionalities rely upon, such as departments, job titles, product categories", + FeatureId = new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), + IsEnabled = true, + Name = "Manage Masters" + }, + new + { + Id = new Guid("4286a13b-bb40-4879-8c6d-18e9e393beda"), + Description = "Full control over all directories, including the ability to manage permissions for all directories in the system.", + FeatureId = new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), + IsEnabled = true, + Name = "Directory Admin" + }, + new + { + Id = new Guid("62668630-13ce-4f52-a0f0-db38af2230c5"), + Description = "Full control over directories they created or have been assigned. Can also manage permissions for those directories.", + FeatureId = new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), + IsEnabled = true, + Name = "Directory Manager" + }, + new + { + Id = new Guid("0f919170-92d4-4337-abd3-49b66fc871bb"), + Description = "Full control over directories they created. Can view contacts in directories they either created or were assigned to. Can manage permissions only for directories they created.", + FeatureId = new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), + IsEnabled = true, + Name = "Directory User" + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.RolePermissionMappings", b => + { + b.Property("ApplicationRoleId") + .HasColumnType("char(36)"); + + b.Property("FeaturePermissionId") + .HasColumnType("char(36)"); + + b.HasKey("ApplicationRoleId", "FeaturePermissionId"); + + b.HasIndex("FeaturePermissionId"); + + b.ToTable("RolePermissionMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactName") + .HasColumnType("longtext"); + + b.Property("ContactNumber") + .HasColumnType("longtext"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("DomainName") + .HasColumnType("longtext"); + + b.Property("IndustryId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OnBoardingDate") + .HasColumnType("datetime(6)"); + + b.Property("OragnizationSize") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("IndustryId"); + + b.ToTable("Tenants"); + + b.HasData( + new + { + Id = new Guid("b3466e83-7e11-464c-b93a-daf047838b26"), + ContactName = "Admin", + ContactNumber = "123456789", + Description = "", + DomainName = "www.marcobms.org", + IndustryId = new Guid("15436ee3-a650-469e-bfc2-59993f7514bb"), + IsActive = true, + Name = "MarcoBMS", + OnBoardingDate = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + OragnizationSize = "100-200" + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CommentId") + .HasColumnType("char(36)"); + + b.Property("FileId") + .HasColumnType("char(36)"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TicketId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("CommentId"); + + b.HasIndex("TicketId"); + + b.ToTable("TicketAttachments"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AuthorId") + .HasColumnType("char(36)"); + + b.Property("MessageText") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ParentMessageId") + .HasColumnType("char(36)"); + + b.Property("SentAt") + .HasColumnType("datetime(6)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("TicketId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("TicketComments"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketForum", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedById") + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LinkedActivityId") + .HasColumnType("char(36)"); + + b.Property("LinkedProjectId") + .HasColumnType("char(36)"); + + b.Property("PriorityId") + .HasColumnType("char(36)"); + + b.Property("StatusId") + .HasColumnType("char(36)"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("TypeId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("PriorityId"); + + b.HasIndex("StatusId"); + + b.HasIndex("TenantId"); + + b.HasIndex("TypeId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("TagId") + .HasColumnType("char(36)"); + + b.Property("TicketId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TagId"); + + b.HasIndex("TicketId"); + + b.ToTable("TicketTags"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketTypeMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IsDefault") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("TicketTypeMasters"); + + b.HasData( + new + { + Id = new Guid("c74e5480-2b71-483c-8f4a-1a9c69c32603"), + Description = "An identified problem that affects the performance, reliability, or standards of a product or service", + IsDefault = true, + Name = "Quality Issue", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("d1f55eab-9898-4e46-9f03-b263e33e5d38"), + Description = "A support service that assists users with technical issues, requests, or inquiries.", + IsDefault = true, + Name = "Help Desk", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Mail.MailDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("MailListId") + .HasColumnType("char(36)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("Recipient") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Schedule") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("MailListId"); + + b.ToTable("MailDetails"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Mail.MailLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("EmailId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("EmployeeId") + .HasColumnType("char(36)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("TimeStamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("MailLogs"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Mail.MailingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("MailingList"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.ActivityMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ActivityName") + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("UnitOfMeasurement") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("ActivityMasters"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.Feature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("ModuleId") + .HasColumnType("char(36)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ModuleId"); + + b.ToTable("Features"); + + b.HasData( + new + { + Id = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), + Description = "Manage Project", + IsActive = true, + ModuleId = new Guid("bf59fd88-b57a-4d67-bf01-3780f385896b"), + Name = "Project Management" + }, + new + { + Id = new Guid("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"), + Description = "Manage Tasks", + IsActive = true, + ModuleId = new Guid("bf59fd88-b57a-4d67-bf01-3780f385896b"), + Name = "Task Management" + }, + new + { + Id = new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), + Description = "Manage Employee", + IsActive = true, + ModuleId = new Guid("2a231490-bcb1-4bdd-91f1-f25fb7f25b23"), + Name = "Employee Management" + }, + new + { + Id = new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), + Description = "Attendance", + IsActive = true, + ModuleId = new Guid("2a231490-bcb1-4bdd-91f1-f25fb7f25b23"), + Name = "Attendance Management" + }, + new + { + Id = new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), + Description = "Global Masters", + IsActive = true, + ModuleId = new Guid("c43db8c7-ab73-47f4-9d3b-f83e81357924"), + Name = "Masters" + }, + new + { + Id = new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), + Description = "Managing all directory related rights", + IsActive = true, + ModuleId = new Guid("c43db8c7-ab73-47f4-9d3b-f83e81357924"), + Name = "Directory Management" + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.Industry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Industries"); + + b.HasData( + new + { + Id = new Guid("15436ee3-a650-469e-bfc2-59993f7514bb"), + Name = "Information Technology (IT) Services" + }, + new + { + Id = new Guid("0a63e657-2c5f-49b5-854b-42c978293154"), + Name = "Manufacturing & Production" + }, + new + { + Id = new Guid("bdc61e3b-69ea-4394-bab6-079ec135b5bd"), + Name = "Energy & Resources" + }, + new + { + Id = new Guid("5ca200ac-00d7-415e-a410-b948e27ac9d2"), + Name = "Finance & Professional Services" + }, + new + { + Id = new Guid("d5621700-cd87-441f-8cdb-6051ddfc83b4"), + Name = "Hospitals and Healthcare Services" + }, + new + { + Id = new Guid("23608891-657e-40f0-bbd4-2b0a2ec1a76f"), + Name = "Social Services" + }, + new + { + Id = new Guid("a493f4e3-16b1-4411-be3c-6bf2987a3168"), + Name = "Retail & Consumer Services" + }, + new + { + Id = new Guid("e9d8ce92-9371-4ed9-9831-83c07f78edec"), + Name = "Transportation & Logistics" + }, + new + { + Id = new Guid("8a0d6134-2dbe-4e0a-b250-ff34cb7b9df0"), + Name = "Education & Training" + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.Module", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Modules"); + + b.HasData( + new + { + Id = new Guid("bf59fd88-b57a-4d67-bf01-3780f385896b"), + Description = "Project Module", + Key = "b04da7e9-0406-409c-ac7f-b97256e6ea02", + Name = "Project" + }, + new + { + Id = new Guid("2a231490-bcb1-4bdd-91f1-f25fb7f25b23"), + Description = "Employee Module", + Key = "0971c7fb-6ce1-458a-ae3f-8d3205893637", + Name = "Employee" + }, + new + { + Id = new Guid("c43db8c7-ab73-47f4-9d3b-f83e81357924"), + Description = "Masters Module", + Key = "504ec132-e6a9-422f-8f85-050602cfce05", + Name = "Masters" + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.StatusMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("StatusMasters"); + + b.HasData( + new + { + Id = new Guid("b74da4c2-d07e-46f2-9919-e75e49b12731"), + Status = "Active", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("cdad86aa-8a56-4ff4-b633-9c629057dfef"), + Status = "In Progress", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("603e994b-a27f-4e5d-a251-f3d69b0498ba"), + Status = "On Hold", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("ef1c356e-0fe0-42df-a5d3-8daee355492d"), + Status = "In Active", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("33deaef9-9af1-4f2a-b443-681ea0d04f81"), + Status = "Completed", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.TicketPriorityMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ColorCode") + .HasColumnType("longtext"); + + b.Property("IsDefault") + .HasColumnType("tinyint(1)"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("TicketPriorityMasters"); + + b.HasData( + new + { + Id = new Guid("188d29b3-10f3-42d0-9587-1a46ae7a0320"), + ColorCode = "008000", + IsDefault = true, + Level = 1, + Name = "Low", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("0919bc84-9f82-4ecf-98c7-962755dd9a97"), + ColorCode = "FFFF00", + IsDefault = true, + Level = 2, + Name = "Medium", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("a13b7e59-16fd-4665-b5cf-a97399e8445a"), + ColorCode = "#FFA500", + IsDefault = true, + Level = 3, + Name = "High", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("f340fbc3-c9fd-46aa-b063-0093418830e4"), + ColorCode = "#FFA500", + IsDefault = true, + Level = 4, + Name = "Critical", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("44a7b91d-a0dd-45d1-8616-4d2f71e16401"), + ColorCode = "#FF0000", + IsDefault = true, + Level = 5, + Name = "Urgent", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.TicketStatusMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ColorCode") + .HasColumnType("longtext"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IsDefault") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("TicketStatusMasters"); + + b.HasData( + new + { + Id = new Guid("6b0c409b-3e80-4165-8b39-f3fcacb4c797"), + ColorCode = "#FFCC99", + Description = "This is a newly created issue.", + IsDefault = true, + Name = "New", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("6c5ac37d-5b7d-40f3-adec-2dabaa5cca86"), + ColorCode = "#E6FF99", + Description = "Assigned to employee or team of employees", + IsDefault = true, + Name = "Assigned", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("7f96bcd5-0c66-411b-8a1d-9d1a4785194e"), + ColorCode = "#99E6FF", + Description = "These issues are currently in progress", + IsDefault = true, + Name = "In Progress", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("5c72b630-6923-4215-bf2c-b1622afd76e7"), + ColorCode = "#6c757d", + Description = "These issues are currently under review", + IsDefault = true, + Name = "In Review", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("8ff85685-a875-4f21-aa95-d99551315fcc"), + ColorCode = "#B399FF", + Description = "The following issues are resolved and closed", + IsDefault = true, + Name = "Done", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.TicketTagMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ColorCode") + .HasColumnType("longtext"); + + b.Property("IsDefault") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("TicketTagMasters"); + + b.HasData( + new + { + Id = new Guid("ef6c2a65-f61d-4537-9650-a7ab7f8d98db"), + ColorCode = "#e59866", + IsDefault = true, + Name = "Quality Issue", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("5a168569-8ad7-4422-8db6-51ef25caddeb"), + ColorCode = "#85c1e9", + IsDefault = true, + Name = "Help Desk", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.WorkCategoryMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("WorkCategoryMasters"); + + b.HasData( + new + { + Id = new Guid("86bb2cc8-f6b5-4fdd-bbee-c389c713a44b"), + Description = "Created new task in a professional or creative context", + IsSystem = true, + Name = "Fresh Work", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("9ebfa19c-53b9-481b-b863-c25d2f843201"), + Description = "Revising, modifying, or correcting a task to improve its quality or fix issues", + IsSystem = true, + Name = "Rework", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("11a79929-1d07-42dc-9e98-82d0d2f4a240"), + Description = "Any defect, deviation, or non-conformance in a task that fails to meet established standards or customer expectations.", + IsSystem = true, + Name = "Quality Issue", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.WorkStatusMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("WorkStatusMasters"); + + b.HasData( + new + { + Id = new Guid("030bb085-e230-4370-aec7-9a74d652864e"), + Description = "Confirm the tasks are actually finished as reported", + IsSystem = true, + Name = "Approve", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("2a1a5b96-cf93-4111-b4b1-76c19d6333b4"), + Description = "Not all tasks are actually finished as reported", + IsSystem = true, + Name = "Partially Approve", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("00a062e6-62e6-42c5-b6b1-024328651b72"), + Description = "Tasks are not finished as reported or have any issues in al the tasks", + IsSystem = true, + Name = "NCR", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.Building", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("Buildings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.Floor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BuildingId") + .HasColumnType("char(36)"); + + b.Property("FloorName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("BuildingId"); + + b.HasIndex("TenantId"); + + b.ToTable("Floor"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactPerson") + .HasColumnType("longtext"); + + b.Property("EndDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProjectAddress") + .HasColumnType("longtext"); + + b.Property("ProjectStatusId") + .HasColumnType("char(36)"); + + b.Property("ShortName") + .HasColumnType("longtext"); + + b.Property("StartDate") + .HasColumnType("datetime(6)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ProjectStatusId"); + + b.HasIndex("TenantId"); + + b.ToTable("Projects"); + + b.HasData( + new + { + Id = new Guid("85bf587b-7ca9-4685-b77c-d817f5847e85"), + ContactPerson = "Project 1 Contact Person", + EndDate = new DateTime(2026, 4, 20, 10, 11, 17, 588, DateTimeKind.Unspecified), + Name = "Project 1", + ProjectAddress = "Project 1 Address", + ProjectStatusId = new Guid("b74da4c2-d07e-46f2-9919-e75e49b12731"), + StartDate = new DateTime(2025, 4, 20, 10, 11, 17, 588, DateTimeKind.Unspecified), + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.ProjectAllocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllocationDate") + .HasColumnType("datetime(6)"); + + b.Property("EmployeeId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("JobRoleId") + .HasColumnType("char(36)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("ReAllocationDate") + .HasColumnType("datetime(6)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("TenantId"); + + b.ToTable("ProjectAllocations"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.WorkArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AreaName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FloorId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("FloorId"); + + b.HasIndex("TenantId"); + + b.ToTable("WorkAreas"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.WorkItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ActivityId") + .HasColumnType("char(36)"); + + b.Property("CompletedWork") + .HasColumnType("double"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ParentTaskId") + .HasColumnType("char(36)"); + + b.Property("PlannedWork") + .HasColumnType("double"); + + b.Property("TaskDate") + .HasColumnType("datetime(6)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("WorkAreaId") + .HasColumnType("char(36)"); + + b.Property("WorkCategoryId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId"); + + b.HasIndex("TenantId"); + + b.HasIndex("WorkAreaId"); + + b.HasIndex("WorkCategoryId"); + + b.ToTable("WorkItems"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Roles.ApplicationRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("Role") + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("ApplicationRoles"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Roles.JobRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("JobRoles"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Utilities.Inquiries", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("About") + .HasColumnType("longtext"); + + b.Property("ContactNumber") + .HasColumnType("longtext"); + + b.Property("ContactPerson") + .HasColumnType("longtext"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("IndustryId") + .HasColumnType("char(36)"); + + b.Property("OragnizationSize") + .HasColumnType("longtext"); + + b.Property("OrganizatioinName") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Inquiries"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(21) + .HasColumnType("varchar(21)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("SecurityStamp") + .HasColumnType("longtext"); + + b.Property("TwoFactorEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + + b.HasDiscriminator().HasValue("IdentityUser"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("RoleId") + .HasColumnType("varchar(255)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.ApplicationUser", b => + { + b.HasBaseType("Microsoft.AspNetCore.Identity.IdentityUser"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRootUser") + .HasColumnType("tinyint(1)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasDiscriminator().HasValue("ApplicationUser"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskAllocation", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "ApprovedBy") + .WithMany() + .HasForeignKey("ApprovedById"); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("AssignedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "ReportedBy") + .WithMany() + .HasForeignKey("ReportedById"); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Projects.WorkItem", "WorkItem") + .WithMany() + .HasForeignKey("WorkItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Master.WorkStatusMaster", "WorkStatus") + .WithMany() + .HasForeignKey("WorkStatusId"); + + b.Navigation("ApprovedBy"); + + b.Navigation("Employee"); + + b.Navigation("ReportedBy"); + + b.Navigation("Tenant"); + + b.Navigation("WorkItem"); + + b.Navigation("WorkStatus"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskComment", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("CommentedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Activities.TaskAllocation", "TaskAllocation") + .WithMany() + .HasForeignKey("TaskAllocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("TaskAllocation"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskMembers", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Activities.TaskAllocation", "TaskAllocation") + .WithMany() + .HasForeignKey("TaskAllocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("TaskAllocation"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.AttendanceModule.Attendance", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "Approver") + .WithMany() + .HasForeignKey("EmployeeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Approver"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.AttendanceModule.AttendanceLog", b => + { + b.HasOne("Marco.Pms.Model.AttendanceModule.Attendance", "Attendance") + .WithMany() + .HasForeignKey("AttendanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.DocumentManager.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId"); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "UpdatedByEmployee") + .WithMany() + .HasForeignKey("UpdatedBy"); + + b.Navigation("Attendance"); + + b.Navigation("Document"); + + b.Navigation("Employee"); + + b.Navigation("Tenant"); + + b.Navigation("UpdatedByEmployee"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Authentication.MPINDetails", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Authentication.OTPDetails", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Authentication.RefreshToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.Bucket", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedByID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.Contact", b => + { + b.HasOne("Marco.Pms.Model.Directory.ContactCategoryMaster", "ContactCategory") + .WithMany() + .HasForeignKey("ContactCategoryId"); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.Navigation("ContactCategory"); + + b.Navigation("CreatedBy"); + + b.Navigation("Tenant"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactBucketMapping", b => + { + b.HasOne("Marco.Pms.Model.Directory.Bucket", "Bucket") + .WithMany() + .HasForeignKey("BucketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Directory.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Bucket"); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactCategoryMaster", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactEmail", b => + { + b.HasOne("Marco.Pms.Model.Directory.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactNote", b => + { + b.HasOne("Marco.Pms.Model.Directory.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "Createdby") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.Navigation("Contact"); + + b.Navigation("Createdby"); + + b.Navigation("Tenant"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactPhone", b => + { + b.HasOne("Marco.Pms.Model.Directory.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactProjectMapping", b => + { + b.HasOne("Marco.Pms.Model.Directory.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Projects.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contact"); + + b.Navigation("Project"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactTagMapping", b => + { + b.HasOne("Marco.Pms.Model.Directory.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Directory.ContactTagMaster", "ContactTag") + .WithMany() + .HasForeignKey("ContactTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contact"); + + b.Navigation("ContactTag"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactTagMaster", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.DirectoryUpdateLog", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.EmployeeBucketMapping", b => + { + b.HasOne("Marco.Pms.Model.Directory.Bucket", "Bucket") + .WithMany() + .HasForeignKey("BucketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Bucket"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Marco.Pms.Model.DocumentManager.Document", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Employees.Employee", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.ApplicationUser", "ApplicationUser") + .WithMany() + .HasForeignKey("ApplicationUserId"); + + b.HasOne("Marco.Pms.Model.Roles.JobRole", "JobRole") + .WithMany() + .HasForeignKey("JobRoleId"); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApplicationUser"); + + b.Navigation("JobRole"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Employees.EmployeeRoleMapping", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Roles.ApplicationRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("Role"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Employees.WorkShift", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.FeaturePermission", b => + { + b.HasOne("Marco.Pms.Model.Master.Feature", "Feature") + .WithMany("FeaturePermissions") + .HasForeignKey("FeatureId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Feature"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.RolePermissionMappings", b => + { + b.HasOne("Marco.Pms.Model.Roles.ApplicationRole", null) + .WithMany() + .HasForeignKey("ApplicationRoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.FeaturePermission", null) + .WithMany() + .HasForeignKey("FeaturePermissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.Tenant", b => + { + b.HasOne("Marco.Pms.Model.Master.Industry", "Industry") + .WithMany() + .HasForeignKey("IndustryId"); + + b.Navigation("Industry"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketAttachment", b => + { + b.HasOne("Marco.Pms.Model.Forum.TicketComment", "TicketComment") + .WithMany("Attachments") + .HasForeignKey("CommentId"); + + b.HasOne("Marco.Pms.Model.Forum.TicketForum", "Ticket") + .WithMany() + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ticket"); + + b.Navigation("TicketComment"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketComment", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketForum", b => + { + b.HasOne("Marco.Pms.Model.Master.TicketPriorityMaster", "Priority") + .WithMany() + .HasForeignKey("PriorityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Master.TicketStatusMaster", "TicketStatusMaster") + .WithMany() + .HasForeignKey("StatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Forum.TicketTypeMaster", "TicketTypeMaster") + .WithMany() + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Priority"); + + b.Navigation("Tenant"); + + b.Navigation("TicketStatusMaster"); + + b.Navigation("TicketTypeMaster"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketTag", b => + { + b.HasOne("Marco.Pms.Model.Master.TicketTagMaster", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Forum.TicketForum", "Ticket") + .WithMany() + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tag"); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Mail.MailDetails", b => + { + b.HasOne("Marco.Pms.Model.Mail.MailingList", "MailBody") + .WithMany() + .HasForeignKey("MailListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MailBody"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.ActivityMaster", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.Feature", b => + { + b.HasOne("Marco.Pms.Model.Master.Module", "Module") + .WithMany() + .HasForeignKey("ModuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Module"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.StatusMaster", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.WorkCategoryMaster", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.WorkStatusMaster", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.Building", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.Floor", b => + { + b.HasOne("Marco.Pms.Model.Projects.Building", "Building") + .WithMany() + .HasForeignKey("BuildingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Building"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.Project", b => + { + b.HasOne("Marco.Pms.Model.Master.StatusMaster", "ProjectStatus") + .WithMany() + .HasForeignKey("ProjectStatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProjectStatus"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.ProjectAllocation", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Projects.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("Project"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.WorkArea", b => + { + b.HasOne("Marco.Pms.Model.Projects.Floor", "Floor") + .WithMany() + .HasForeignKey("FloorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Floor"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.WorkItem", b => + { + b.HasOne("Marco.Pms.Model.Master.ActivityMaster", "ActivityMaster") + .WithMany() + .HasForeignKey("ActivityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Projects.WorkArea", "WorkArea") + .WithMany() + .HasForeignKey("WorkAreaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Master.WorkCategoryMaster", "WorkCategoryMaster") + .WithMany() + .HasForeignKey("WorkCategoryId"); + + b.Navigation("ActivityMaster"); + + b.Navigation("Tenant"); + + b.Navigation("WorkArea"); + + b.Navigation("WorkCategoryMaster"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Roles.ApplicationRole", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Marco.Pms.Model.Roles.JobRole", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketComment", b => + { + b.Navigation("Attachments"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.Feature", b => + { + b.Navigation("FeaturePermissions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Marco.Pms.DataAccess/Migrations/20250702045931_Added_Designation_Paraneter_In_Contacts_Table.cs b/Marco.Pms.DataAccess/Migrations/20250702045931_Added_Designation_Paraneter_In_Contacts_Table.cs new file mode 100644 index 0000000..45231d4 --- /dev/null +++ b/Marco.Pms.DataAccess/Migrations/20250702045931_Added_Designation_Paraneter_In_Contacts_Table.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Marco.Pms.DataAccess.Migrations +{ + /// + public partial class Added_Designation_Paraneter_In_Contacts_Table : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Designation", + table: "Contacts", + type: "longtext", + nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Designation", + table: "Contacts"); + } + } +} diff --git a/Marco.Pms.DataAccess/Migrations/ApplicationDbContextModelSnapshot.cs b/Marco.Pms.DataAccess/Migrations/ApplicationDbContextModelSnapshot.cs index 26a3bdd..72ed45c 100644 --- a/Marco.Pms.DataAccess/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Marco.Pms.DataAccess/Migrations/ApplicationDbContextModelSnapshot.cs @@ -420,6 +420,10 @@ namespace Marco.Pms.DataAccess.Migrations .IsRequired() .HasColumnType("longtext"); + b.Property("Designation") + .IsRequired() + .HasColumnType("longtext"); + b.Property("IsActive") .HasColumnType("tinyint(1)"); diff --git a/Marco.Pms.Model/Directory/Contact.cs b/Marco.Pms.Model/Directory/Contact.cs index fe82711..4cbcb37 100644 --- a/Marco.Pms.Model/Directory/Contact.cs +++ b/Marco.Pms.Model/Directory/Contact.cs @@ -12,6 +12,7 @@ namespace Marco.Pms.Model.Directory //public Guid? ProjectId { get; set; } public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; + public string Designation { get; set; } = string.Empty; public string Organization { get; set; } = string.Empty; public string? Address { get; set; } public bool IsActive { get; set; } = true; diff --git a/Marco.Pms.Model/Dtos/Directory/CreateContactDto.cs b/Marco.Pms.Model/Dtos/Directory/CreateContactDto.cs index 577f405..f581fe3 100644 --- a/Marco.Pms.Model/Dtos/Directory/CreateContactDto.cs +++ b/Marco.Pms.Model/Dtos/Directory/CreateContactDto.cs @@ -9,6 +9,7 @@ public List? BucketIds { get; set; } public Guid? ContactCategoryId { get; set; } public string? Description { get; set; } + public string? Designation { get; set; } public string? Organization { get; set; } public string? Address { get; set; } public List? Tags { get; set; } diff --git a/Marco.Pms.Model/Dtos/Directory/UpdateContactDto.cs b/Marco.Pms.Model/Dtos/Directory/UpdateContactDto.cs index 16c8645..b4d18d8 100644 --- a/Marco.Pms.Model/Dtos/Directory/UpdateContactDto.cs +++ b/Marco.Pms.Model/Dtos/Directory/UpdateContactDto.cs @@ -10,6 +10,7 @@ public List? BucketIds { get; set; } public Guid? ContactCategoryId { get; set; } public string? Description { get; set; } + public string? Designation { get; set; } public string? Organization { get; set; } public string? Address { get; set; } public List? Tags { get; set; } diff --git a/Marco.Pms.Model/Mapper/DirectoryMapper.cs b/Marco.Pms.Model/Mapper/DirectoryMapper.cs index b175cb7..c9965c8 100644 --- a/Marco.Pms.Model/Mapper/DirectoryMapper.cs +++ b/Marco.Pms.Model/Mapper/DirectoryMapper.cs @@ -16,6 +16,7 @@ namespace Marco.Pms.Model.Mapper Name = createContactDto.Name ?? string.Empty, ContactCategoryId = createContactDto.ContactCategoryId, Description = createContactDto.Description ?? string.Empty, + Designation = createContactDto.Designation ?? string.Empty, Organization = createContactDto?.Organization ?? string.Empty, Address = createContactDto != null ? createContactDto.Address : string.Empty, CreatedById = employeeId, @@ -34,6 +35,7 @@ namespace Marco.Pms.Model.Mapper CreatedAt = contact.CreatedAt, CreatedById = contact.CreatedById, Description = updateContactDto.Description ?? string.Empty, + Designation = updateContactDto.Designation ?? string.Empty, Organization = updateContactDto?.Organization ?? string.Empty, Address = updateContactDto != null ? updateContactDto.Address : string.Empty, TenantId = tenantId @@ -47,6 +49,7 @@ namespace Marco.Pms.Model.Mapper Name = contact.Name, ContactCategory = contact.ContactCategory != null ? contact.ContactCategory.ToContactCategoryVMFromContactCategoryMaster() : null, Description = contact.Description ?? string.Empty, + Designation = contact.Designation ?? string.Empty, Organization = contact.Organization ?? string.Empty, Address = contact.Address ?? string.Empty }; @@ -59,6 +62,7 @@ namespace Marco.Pms.Model.Mapper Name = contact.Name, ContactCategory = contact.ContactCategory != null ? contact.ContactCategory.ToContactCategoryVMFromContactCategoryMaster() : null, Description = contact.Description ?? string.Empty, + Designation = contact.Designation ?? string.Empty, Organization = contact.Organization ?? string.Empty, Address = contact.Address ?? string.Empty, CreatedAt = contact.CreatedAt, diff --git a/Marco.Pms.Model/ViewModels/Directory/ContactProfileVM.cs b/Marco.Pms.Model/ViewModels/Directory/ContactProfileVM.cs index 9e8f4cb..de53d25 100644 --- a/Marco.Pms.Model/ViewModels/Directory/ContactProfileVM.cs +++ b/Marco.Pms.Model/ViewModels/Directory/ContactProfileVM.cs @@ -9,6 +9,7 @@ namespace Marco.Pms.Model.ViewModels.Directory public Guid Id { get; set; } public string? Name { get; set; } public string? Description { get; set; } + public string? Designation { get; set; } public string? Organization { get; set; } public string? Address { get; set; } public DateTime CreatedAt { get; set; } diff --git a/Marco.Pms.Model/ViewModels/Directory/ContactVM.cs b/Marco.Pms.Model/ViewModels/Directory/ContactVM.cs index d394f73..4b212ae 100644 --- a/Marco.Pms.Model/ViewModels/Directory/ContactVM.cs +++ b/Marco.Pms.Model/ViewModels/Directory/ContactVM.cs @@ -12,6 +12,7 @@ namespace Marco.Pms.Model.ViewModels.Directory public ContactCategoryVM? ContactCategory { get; set; } public List? BucketIds { get; set; } public string? Description { get; set; } + public string? Designation { get; set; } public string? Organization { get; set; } public string? Address { get; set; } public List? Tags { get; set; } diff --git a/Marco.Pms.Services/Controllers/DirectoryController.cs b/Marco.Pms.Services/Controllers/DirectoryController.cs index 4a0e41e..65bb039 100644 --- a/Marco.Pms.Services/Controllers/DirectoryController.cs +++ b/Marco.Pms.Services/Controllers/DirectoryController.cs @@ -33,20 +33,7 @@ namespace Marco.Pms.Services.Controllers CategoryIds = categoryIds }; var response = await _directoryHelper.GetListOfContacts(search, active, filterDto, projectId); - - - if (response.StatusCode == 200) - { - return Ok(response); - } - else if (response.StatusCode == 401) - { - return Unauthorized(response); - } - else - { - return BadRequest(response); - } + return StatusCode(response.StatusCode, response); } @@ -54,18 +41,7 @@ namespace Marco.Pms.Services.Controllers public async Task GetContactsListByBucketId(Guid bucketId) { var response = await _directoryHelper.GetContactsListByBucketId(bucketId); - if (response.StatusCode == 200) - { - return Ok(response); - } - else if (response.StatusCode == 401) - { - return Unauthorized(response); - } - else - { - return BadRequest(response); - } + return StatusCode(response.StatusCode, response); } [HttpPost] @@ -81,61 +57,34 @@ namespace Marco.Pms.Services.Controllers return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } var response = await _directoryHelper.CreateContact(createContact); - if (response.StatusCode == 200) - { - return Ok(response); - } - else - { - return BadRequest(response); - } + return StatusCode(response.StatusCode, response); } [HttpPut("{id}")] public async Task UpdateContact(Guid id, [FromBody] UpdateContactDto updateContact) { var response = await _directoryHelper.UpdateContact(id, updateContact); - if (response.StatusCode == 200) - { - return Ok(response); - } - else if (response.StatusCode == 404) - { - return NotFound(response); - } - else if (response.StatusCode == 401) - { - return Unauthorized(response); - } - else - { - return BadRequest(response); - } + return StatusCode(response.StatusCode, response); } [HttpGet("profile/{id}")] public async Task GetContactProfile(Guid id) { var response = await _directoryHelper.GetContactProfile(id); - if (response.StatusCode == 200) - { - return Ok(response); - } - else if (response.StatusCode == 404) - { - return NotFound(response); - } - else - { - return BadRequest(response); - } + return StatusCode(response.StatusCode, response); } [HttpGet("organization")] public async Task GetOrganizationList() { var response = await _directoryHelper.GetOrganizationList(); - return Ok(response); + return StatusCode(response.StatusCode, response); + } + [HttpGet("designations")] + public async Task GetDesignationList() + { + var response = await _directoryHelper.GetDesignationList(); + return StatusCode(response.StatusCode, response); } [HttpDelete("{id}")] diff --git a/Marco.Pms.Services/Helpers/DirectoryHelper.cs b/Marco.Pms.Services/Helpers/DirectoryHelper.cs index bafa36f..b5ccb5c 100644 --- a/Marco.Pms.Services/Helpers/DirectoryHelper.cs +++ b/Marco.Pms.Services/Helpers/DirectoryHelper.cs @@ -747,6 +747,7 @@ namespace Marco.Pms.Services.Helpers { Guid tenantId = _userHelper.GetTenantId(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var hasAdminPermission = await _permissionServices.HasPermission(directoryAdmin, LoggedInEmployee.Id); if (id != Guid.Empty) { Contact? contact = await _context.Contacts.Include(c => c.ContactCategory).Include(c => c.CreatedBy).FirstOrDefaultAsync(c => c.Id == id && c.IsActive); @@ -806,11 +807,19 @@ namespace Marco.Pms.Services.Helpers } List? contactBuckets = await _context.ContactBucketMappings.Where(cb => cb.ContactId == contact.Id).ToListAsync(); List? employeeBuckets = await _context.EmployeeBucketMappings.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).ToListAsync(); - if (contactBuckets.Any() && employeeBuckets.Any()) + if (contactBuckets.Any() && (employeeBuckets.Any() || hasAdminPermission)) { List contactBucketIds = contactBuckets.Select(cb => cb.BucketId).ToList(); List employeeBucketIds = employeeBuckets.Select(eb => eb.BucketId).ToList(); - List? buckets = await _context.Buckets.Where(b => contactBucketIds.Contains(b.Id) && employeeBucketIds.Contains(b.Id)).ToListAsync(); + List? buckets = null; + if (hasAdminPermission) + { + buckets = await _context.Buckets.Where(b => contactBucketIds.Contains(b.Id)).ToListAsync(); + } + else + { + buckets = await _context.Buckets.Where(b => contactBucketIds.Contains(b.Id) && employeeBucketIds.Contains(b.Id)).ToListAsync(); + } List? bucketVMs = new List(); foreach (var bucket in buckets) { @@ -860,40 +869,101 @@ namespace Marco.Pms.Services.Helpers } public async Task> GetOrganizationList() { + // Step 1: Retrieve tenant and employee context Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var organizationList = await _context.Contacts.Where(c => c.TenantId == tenantId).Select(c => c.Organization).Distinct().ToListAsync(); - _logger.LogInfo("Employee {EmployeeId} fetched list of organizations in a tenant {TenantId}", LoggedInEmployee.Id, tenantId); - return ApiResponse.SuccessResponse(organizationList, $"{organizationList.Count} records of organization names fetched from contacts", 200); + _logger.LogInfo("GetOrganizationList called by EmployeeId: {EmployeeId} for TenantId: {TenantId}", + loggedInEmployee.Id, tenantId); + + // Step 2: Fetch distinct, non-empty organization names + var organizationList = await _context.Contacts + .Where(c => c.TenantId == tenantId && !string.IsNullOrWhiteSpace(c.Organization)) + .Select(c => c.Organization.Trim()) + .Distinct() + .ToListAsync(); + + _logger.LogInfo("EmployeeId: {EmployeeId} fetched {Count} organization names from TenantId: {TenantId}", + loggedInEmployee.Id, organizationList.Count, tenantId); + + // Step 3: Return success response + return ApiResponse.SuccessResponse( + organizationList, + $"{organizationList.Count} records of organization names fetched from contacts", + 200 + ); + } + public async Task> GetDesignationList() + { + // Step 1: Get tenant and logged-in employee details + Guid tenantId = _userHelper.GetTenantId(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + _logger.LogInfo("GetDesignationList called by EmployeeId: {EmployeeId} in TenantId: {TenantId}", + loggedInEmployee.Id, tenantId); + + // Step 2: Fetch distinct, non-null designations from contacts + var designationList = await _context.Contacts + .Where(c => c.TenantId == tenantId && !string.IsNullOrWhiteSpace(c.Designation)) + .Select(c => c.Designation.Trim()) + .Distinct() + .ToListAsync(); + + _logger.LogInfo("EmployeeId: {EmployeeId} fetched {Count} designations from TenantId: {TenantId}", + loggedInEmployee.Id, designationList.Count, tenantId); + + // Step 3: Return result + return ApiResponse.SuccessResponse( + designationList, + $"{designationList.Count} records of designation fetched from contacts", + 200 + ); } public async Task> DeleteContact(Guid id, bool active) { + // Step 1: Get tenant and logged-in employee info Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - if (id != Guid.Empty) + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + _logger.LogInfo("DeleteContact called by EmployeeId: {EmployeeId} for ContactId: {ContactId} with Active: {IsActive}", + loggedInEmployee.Id, id, active); + + // Step 2: Validate contact ID + if (id == Guid.Empty) { - Contact? contact = await _context.Contacts.FirstOrDefaultAsync(c => c.Id == id && c.TenantId == tenantId); - if (contact == null) - { - _logger.LogWarning("Employee with ID {LoggedInEmployeeId} tries to delete contact with ID {ContactId} is not found in database", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("Contact not found", "Contact not found", 404); - } - contact.IsActive = active; - - _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog - { - RefereanceId = contact.Id, - UpdatedById = LoggedInEmployee.Id, - UpdateAt = DateTime.UtcNow - }); - - await _context.SaveChangesAsync(); - _logger.LogInfo("Contact {ContactId} has been deleted by Employee {Employee}", id, LoggedInEmployee.Id); - return ApiResponse.SuccessResponse(new { }, "Contact is deleted Successfully", 200); + _logger.LogWarning("Empty contact ID received from EmployeeId: {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Contact ID is empty", "Contact ID is empty", 400); } - _logger.LogInfo("Employee ID {EmployeeId} sent an empty contact id", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("Contact ID is empty", "Contact ID is empty", 400); + + // Step 3: Check if contact exists under current tenant + var contact = await _context.Contacts + .FirstOrDefaultAsync(c => c.Id == id && c.TenantId == tenantId); + + if (contact == null) + { + _logger.LogWarning("EmployeeId {EmployeeId} attempted to delete non-existent contact Id: {ContactId}", loggedInEmployee.Id, id); + return ApiResponse.ErrorResponse("Contact not found", "Contact not found", 404); + } + + // Step 4: Soft delete or restore contact + contact.IsActive = active; + + // Step 5: Log the update in DirectoryUpdateLog + _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog + { + RefereanceId = contact.Id, + UpdatedById = loggedInEmployee.Id, + UpdateAt = DateTime.UtcNow + }); + + await _context.SaveChangesAsync(); + + string status = active ? "restored" : "deleted"; + _logger.LogInfo("Contact {ContactId} successfully {Status} by EmployeeId: {EmployeeId}", + contact.Id, status, loggedInEmployee.Id); + + // Step 6: Return success response + return ApiResponse.SuccessResponse(new { }, $"Contact {status} successfully", 200); } // -------------------------------- Contact Notes -------------------------------- From 62eb914456c0e64f57d1c67070d81e00aa575dfd Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 2 Jul 2025 17:42:42 +0530 Subject: [PATCH 011/124] Added filter in get image list API --- Marco.Pms.Model/Utilities/ImageFilter.cs | 14 ++ .../Controllers/ImageController.cs | 236 +++++++++++------- 2 files changed, 156 insertions(+), 94 deletions(-) create mode 100644 Marco.Pms.Model/Utilities/ImageFilter.cs diff --git a/Marco.Pms.Model/Utilities/ImageFilter.cs b/Marco.Pms.Model/Utilities/ImageFilter.cs new file mode 100644 index 0000000..a5cb7f7 --- /dev/null +++ b/Marco.Pms.Model/Utilities/ImageFilter.cs @@ -0,0 +1,14 @@ +namespace Marco.Pms.Model.Utilities +{ + public class ImageFilter + { + public List? BuildingIds { get; set; } + public List? FloorIds { get; set; } + public List? WorkAreaIds { get; set; } + public List? WorkCategoryIds { get; set; } + public List? ActivityIds { get; set; } + public List? UploadedByIds { get; set; } + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + } +} diff --git a/Marco.Pms.Services/Controllers/ImageController.cs b/Marco.Pms.Services/Controllers/ImageController.cs index 19af70f..eaab3c6 100644 --- a/Marco.Pms.Services/Controllers/ImageController.cs +++ b/Marco.Pms.Services/Controllers/ImageController.cs @@ -1,4 +1,5 @@ -using Marco.Pms.DataAccess.Data; +using System.Text.Json; +using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Activities; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Mapper; @@ -35,9 +36,10 @@ namespace Marco.Pms.Services.Controllers } [HttpGet("images/{projectId}")] - public async Task GetImageList(Guid projectId) + + public async Task GetImageList(Guid projectId, [FromQuery] string? filter) { - _logger.LogInfo("GetImageList called for ProjectId: {ProjectId}", projectId); + _logger.LogInfo("[GetImageList] Called by Employee for ProjectId: {ProjectId}", projectId); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); @@ -45,133 +47,178 @@ namespace Marco.Pms.Services.Controllers var isProjectExist = await _context.Projects.AnyAsync(p => p.Id == projectId && p.TenantId == tenantId); if (!isProjectExist) { - _logger.LogWarning("Project not found for ProjectId: {ProjectId}", projectId); + _logger.LogWarning("[GetImageList] ProjectId: {ProjectId} not found", projectId); return BadRequest(ApiResponse.ErrorResponse("Project not found", "Project not found in database", 400)); } - // Step 2: Check permission + // Step 2: Check project access permission var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.ToString()); if (!hasPermission) { - _logger.LogWarning("No access to ProjectId: {ProjectId} for EmployeeId: {EmployeeId}", projectId, loggedInEmployee.Id); + _logger.LogWarning("[GetImageList] Access denied for EmployeeId: {EmployeeId} on ProjectId: {ProjectId}", loggedInEmployee.Id, projectId); return StatusCode(403, ApiResponse.ErrorResponse("You don't have access", "You don't have access", 403)); } + // Step 3: Deserialize filter + ImageFilter? imageFilter = null; + if (!string.IsNullOrWhiteSpace(filter)) + { + try + { + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + string unescapedJsonString = JsonSerializer.Deserialize(filter, options) ?? ""; + imageFilter = JsonSerializer.Deserialize(unescapedJsonString, options); + } + catch (Exception ex) + { + _logger.LogWarning("[GetImageList] Failed to parse filter: {Message}", ex.Message); + } + } + + // Step 4: Extract filter values + var buildingIds = imageFilter?.BuildingIds; + var floorIds = imageFilter?.FloorIds; + var workAreaIds = imageFilter?.WorkAreaIds; + var activityIds = imageFilter?.ActivityIds; + var workCategoryIds = imageFilter?.WorkCategoryIds; + var startDate = imageFilter?.StartDate; + var endDate = imageFilter?.EndDate; + var uploadedByIds = imageFilter?.UploadedByIds; + + // Step 5: Fetch building > floor > area > work item hierarchy // Step 3: Fetch building > floor > work area > work item hierarchy - var buildings = await _context.Buildings + List? buildings = null; + List? floors = null; + List? workAreas = null; + //List? workItems = null; + //List? documents = null; + + if (buildingIds != null && buildingIds.Count > 0) + { + + buildings = await _context.Buildings + .Where(b => b.ProjectId == projectId && buildingIds.Contains(b.Id)) + .ToListAsync(); + } + else + { + buildings = await _context.Buildings .Where(b => b.ProjectId == projectId) - .Select(b => new { b.Id, b.Name }) .ToListAsync(); - var buildingIds = buildings.Select(b => b.Id).ToList(); + buildingIds = buildings.Select(b => b.Id).ToList(); + } - var floors = await _context.Floor + if (floorIds != null && floorIds.Count > 0) + { + floors = await _context.Floor + .Where(f => buildingIds.Contains(f.BuildingId) && floorIds.Contains(f.Id)) + .ToListAsync(); + } + else + { + floors = await _context.Floor .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 + floorIds = floors.Select(f => f.Id).ToList(); + } + if (workAreaIds != null && workAreaIds.Count > 0) + { + workAreas = await _context.WorkAreas + .Where(wa => floorIds.Contains(wa.FloorId) && workAreaIds.Contains(wa.Id)) + .ToListAsync(); + } + else + { + workAreas = await _context.WorkAreas .Where(wa => floorIds.Contains(wa.FloorId)) - .Select(wa => new { wa.Id, wa.FloorId, wa.AreaName }) .ToListAsync(); - var workAreaIds = workAreas.Select(wa => wa.Id).ToList(); + workAreaIds = workAreas.Select(wa => wa.Id).ToList(); + } - var workItems = await _context.WorkItems - .Include(wi => wi.ActivityMaster) - .Include(wi => wi.WorkCategoryMaster) - .Where(wi => workAreaIds.Contains(wi.WorkAreaId)) - .Select(wi => new { wi.Id, wi.WorkAreaId, wi.ActivityMaster, wi.WorkCategoryMaster }) - .ToListAsync(); + var workItemsQuery = _context.WorkItems.Include(w => w.ActivityMaster).Include(w => w.WorkCategoryMaster) + .Where(wi => workAreaIds.Contains(wi.WorkAreaId)); + if (activityIds?.Any() == true) workItemsQuery = workItemsQuery.Where(wi => activityIds.Contains(wi.ActivityId)); + if (workCategoryIds?.Any() == true) + { + workItemsQuery = workItemsQuery.Where(wi => wi.WorkCategoryMaster != null && workCategoryIds.Contains(wi.WorkCategoryMaster.Id)); + } + var workItems = await workItemsQuery.ToListAsync(); var workItemIds = workItems.Select(wi => wi.Id).ToList(); - // Step 4: Fetch task and comment data - var tasks = await _context.TaskAllocations - .Include(t => t.ReportedBy) - .Where(t => workItemIds.Contains(t.WorkItemId)) - .ToListAsync(); - + // Step 6: Fetch task allocations and comments + var tasks = await _context.TaskAllocations.Include(t => t.ReportedBy) + .Where(t => workItemIds.Contains(t.WorkItemId)).ToListAsync(); var taskIds = tasks.Select(t => t.Id).ToList(); - var comments = await _context.TaskComments - .Include(c => c.Employee) - .Where(c => taskIds.Contains(c.TaskAllocationId)) - .ToListAsync(); - + var comments = await _context.TaskComments.Include(c => c.Employee) + .Where(c => taskIds.Contains(c.TaskAllocationId)).ToListAsync(); var commentIds = comments.Select(c => c.Id).ToList(); - // Step 5: Fetch attachments and related documents var attachments = await _context.TaskAttachments - .Where(ta => taskIds.Contains(ta.ReferenceId) || commentIds.Contains(ta.ReferenceId)) - .ToListAsync(); + .Where(ta => taskIds.Contains(ta.ReferenceId) || commentIds.Contains(ta.ReferenceId)).ToListAsync(); var documentIds = attachments.Select(ta => ta.DocumentId).ToList(); - var documents = await _context.Documents - .Include(d => d.UploadedBy) - .Where(d => documentIds.Contains(d.Id)) - .ToListAsync(); + // Step 7: Fetch and filter documents + var docQuery = _context.Documents.Include(d => d.UploadedBy) + .Where(d => documentIds.Contains(d.Id) && d.TenantId == tenantId); + if (startDate != null && endDate != null) + { + docQuery = docQuery.Where(d => d.UploadedAt.Date >= startDate.Value.Date && d.UploadedAt.Date <= endDate.Value.Date); + } + var documents = await docQuery.ToListAsync(); - // Step 6: Prepare view models - var documentVM = documents - .Select(d => + // Step 8: Build response + var documentVM = documents.Select(d => + { + var refId = attachments.FirstOrDefault(ta => ta.DocumentId == d.Id)?.ReferenceId; + var task = tasks.FirstOrDefault(t => t.Id == refId); + var comment = comments.FirstOrDefault(c => c.Id == refId); + + var source = task != null ? "Report" : comment != null ? "Comment" : ""; + var uploadedBy = task?.ReportedBy ?? comment?.Employee; + + var workItem = workItems.FirstOrDefault(w => w.Id == task?.WorkItemId); + var workArea = workAreas.FirstOrDefault(wa => wa.Id == workItem?.WorkAreaId); + var floor = floors.FirstOrDefault(f => f.Id == workArea?.FloorId); + var building = buildings.FirstOrDefault(b => b.Id == floor?.BuildingId); + + return new { - var referenceId = attachments - .Where(ta => ta.DocumentId == d.Id) - .Select(ta => ta.ReferenceId) - .FirstOrDefault(); + Id = d.Id, + BatchId = d.BatchId, + thumbnailUrl = d.ThumbS3Key != null ? _s3Service.GeneratePreSignedUrlAsync(d.ThumbS3Key) : (d.S3Key != null ? _s3Service.GeneratePreSignedUrlAsync(d.S3Key) : null), + ImageUrl = d.S3Key != null ? _s3Service.GeneratePreSignedUrlAsync(d.S3Key) : null, + UploadedBy = d.UploadedBy?.ToBasicEmployeeVMFromEmployee() ?? uploadedBy?.ToBasicEmployeeVMFromEmployee(), + UploadedAt = d.UploadedAt, + Source = source, + ProjectId = projectId, + BuildingId = building?.Id, + BuildingName = building?.Name, + FloorIds = floor?.Id, + FloorName = floor?.FloorName, + WorkAreaId = workArea?.Id, + WorkAreaName = workArea?.AreaName, + TaskId = task?.Id, + ActivityId = workItem?.ActivityMaster?.Id, + ActivityName = workItem?.ActivityMaster?.ActivityName, + WorkCategoryId = workItem?.WorkCategoryMaster?.Id, + WorkCategoryName = workItem?.WorkCategoryMaster?.Name, + CommentId = comment?.Id, + Comment = comment?.Comment + }; + }).ToList(); - var task = tasks.FirstOrDefault(t => t.Id == referenceId); - var comment = comments.FirstOrDefault(c => c.Id == referenceId); + if (uploadedByIds?.Any() == true) + { + documentVM = documentVM.Where(d => uploadedByIds.Contains(d.UploadedBy?.Id ?? Guid.Empty)).ToList(); + } - string source = ""; - Employee? uploadedBy = null; - if (task != null) - { - uploadedBy = task.ReportedBy; - source = "Report"; - } - else if (comment != null) - { - task = tasks.FirstOrDefault(t => t.Id == comment.TaskAllocationId); - uploadedBy = comment.Employee; - source = "Comment"; - } - - var workItem = workItems.FirstOrDefault(wi => wi.Id == task?.WorkItemId); - var workArea = workAreas.FirstOrDefault(wa => wa.Id == workItem?.WorkAreaId); - var floor = floors.FirstOrDefault(f => f.Id == workArea?.FloorId); - var building = buildings.FirstOrDefault(b => b.Id == floor?.BuildingId); - - return new - { - Id = d.Id, - BatchId = d.BatchId, - thumbnailUrl = d.ThumbS3Key != null ? _s3Service.GeneratePreSignedUrlAsync(d.ThumbS3Key) : (d.S3Key != null ? _s3Service.GeneratePreSignedUrlAsync(d.S3Key) : null), - ImageUrl = d.S3Key != null ? _s3Service.GeneratePreSignedUrlAsync(d.S3Key) : null, - UploadedBy = d.UploadedBy?.ToBasicEmployeeVMFromEmployee() ?? uploadedBy?.ToBasicEmployeeVMFromEmployee(), - UploadedAt = d.UploadedAt, - Source = source, - ProjectId = projectId, - BuildingId = building?.Id, - BuildingName = building?.Name, - FloorIds = floor?.Id, - FloorName = floor?.FloorName, - WorkAreaId = workArea?.Id, - WorkAreaName = workArea?.AreaName, - TaskId = task?.Id, - ActivityName = workItem?.ActivityMaster?.ActivityName, - WorkCategoryId = workItem?.WorkCategoryMaster?.Id, - WorkCategoryName = workItem?.WorkCategoryMaster?.Name, - CommentId = comment?.Id, - Comment = comment?.Comment - }; - }).ToList(); - - _logger.LogInfo("Image list fetched for ProjectId: {ProjectId}. Total documents: {Count}", projectId, documentVM.Count); + _logger.LogInfo("[GetImageList] Fetched {Count} documents for ProjectId: {ProjectId}", documentVM.Count, projectId); return Ok(ApiResponse.SuccessResponse(documentVM, $"{documentVM.Count} image records fetched successfully", 200)); } @@ -269,6 +316,7 @@ namespace Marco.Pms.Services.Controllers WorkAreaId = workArea?.Id, WorkAreaName = workArea?.AreaName, TaskId = task?.Id, + ActivityId = workItem?.ActivityMaster?.Id, ActivityName = workItem?.ActivityMaster?.ActivityName, WorkCategoryId = workItem?.WorkCategoryMaster?.Id, WorkCategoryName = workItem?.WorkCategoryMaster?.Name, From a303625d593b4e3ff3def4a7066972ac0210f781 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 3 Jul 2025 11:32:06 +0530 Subject: [PATCH 012/124] Added a line which is missed while optimizing --- Marco.Pms.Services/Controllers/ImageController.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Marco.Pms.Services/Controllers/ImageController.cs b/Marco.Pms.Services/Controllers/ImageController.cs index eaab3c6..efbd134 100644 --- a/Marco.Pms.Services/Controllers/ImageController.cs +++ b/Marco.Pms.Services/Controllers/ImageController.cs @@ -182,6 +182,11 @@ namespace Marco.Pms.Services.Controllers var source = task != null ? "Report" : comment != null ? "Comment" : ""; var uploadedBy = task?.ReportedBy ?? comment?.Employee; + if (comment != null) + { + task = tasks.FirstOrDefault(t => t.Id == comment.TaskAllocationId); + } + var workItem = workItems.FirstOrDefault(w => w.Id == task?.WorkItemId); var workArea = workAreas.FirstOrDefault(wa => wa.Id == workItem?.WorkAreaId); var floor = floors.FirstOrDefault(f => f.Id == workArea?.FloorId); From c9ff53a7acdf7d16ef0512a29099cacdc56d43cd Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 3 Jul 2025 12:46:49 +0530 Subject: [PATCH 013/124] Removed double Deserialize --- Marco.Pms.Services/Controllers/ImageController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Marco.Pms.Services/Controllers/ImageController.cs b/Marco.Pms.Services/Controllers/ImageController.cs index efbd134..44952c6 100644 --- a/Marco.Pms.Services/Controllers/ImageController.cs +++ b/Marco.Pms.Services/Controllers/ImageController.cs @@ -66,8 +66,8 @@ namespace Marco.Pms.Services.Controllers try { var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; - string unescapedJsonString = JsonSerializer.Deserialize(filter, options) ?? ""; - imageFilter = JsonSerializer.Deserialize(unescapedJsonString, options); + //string unescapedJsonString = JsonSerializer.Deserialize(filter, options) ?? ""; + imageFilter = JsonSerializer.Deserialize(filter, options); } catch (Exception ex) { From cd4ad6f4ac5002f94f628adf7d93762418c6b9f6 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Fri, 4 Jul 2025 17:49:25 +0530 Subject: [PATCH 014/124] Saving project details with infrastructure, employee permissions and assigned project for that employee in mongodb --- Marco.Pms.CacheHelper/EmployeeCache.cs | 158 +++++++ .../Marco.Pms.CacheHelper.csproj | 18 + Marco.Pms.CacheHelper/ProjectCache.cs | 434 ++++++++++++++++++ Marco.Pms.Model/Marco.Pms.Model.csproj | 1 + .../MongoDBModels/ActivityMasterMongoDB.cs | 9 + .../MongoDBModels/BuildingMongoDB.cs | 18 + .../EmployeePermissionMongoDB.cs | 13 + Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs | 17 + .../MongoDBModels/ProjectMongoDB.cs | 18 + .../MongoDBModels/StatusMasterMongoDB.cs | 8 + .../MongoDBModels/WorkAreaMongoDB.cs | 15 + .../WorkCategoryMasterMongoDB.cs | 9 + .../MongoDBModels/WorkItemMongoDB.cs | 15 + .../Controllers/ProjectController.cs | 221 +++++++-- .../Controllers/RolesController.cs | 12 +- Marco.Pms.Services/Dockerfile | 1 + .../Helpers/CacheUpdateHelper.cs | 98 ++++ Marco.Pms.Services/Helpers/ProjectsHelper.cs | 69 +-- Marco.Pms.Services/Helpers/RolesHelper.cs | 7 +- Marco.Pms.Services/Marco.Pms.Services.csproj | 1 + Marco.Pms.Services/Program.cs | 6 +- .../Service/PermissionServices.cs | 18 +- .../appsettings.Development.json | 4 +- .../appsettings.Production.json | 5 +- marco.pms.api.sln | 6 + 25 files changed, 1090 insertions(+), 91 deletions(-) create mode 100644 Marco.Pms.CacheHelper/EmployeeCache.cs create mode 100644 Marco.Pms.CacheHelper/Marco.Pms.CacheHelper.csproj create mode 100644 Marco.Pms.CacheHelper/ProjectCache.cs create mode 100644 Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs create mode 100644 Marco.Pms.Services/Helpers/CacheUpdateHelper.cs diff --git a/Marco.Pms.CacheHelper/EmployeeCache.cs b/Marco.Pms.CacheHelper/EmployeeCache.cs new file mode 100644 index 0000000..7d75407 --- /dev/null +++ b/Marco.Pms.CacheHelper/EmployeeCache.cs @@ -0,0 +1,158 @@ +using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.MongoDBModels; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using MongoDB.Driver; + +namespace Marco.Pms.CacheHelper +{ + public class EmployeeCache + { + private readonly ApplicationDbContext _context; + //private readonly IMongoDatabase _mongoDB; + private readonly IMongoCollection _collection; + public EmployeeCache(ApplicationDbContext context, IConfiguration configuration) + { + var connectionString = configuration["MongoDB:ConnectionString"]; + _context = context; + var mongoUrl = new MongoUrl(connectionString); + var client = new MongoClient(mongoUrl); // Your MongoDB connection string + var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name + _collection = mongoDB.GetCollection("EmployeeProfile"); + } + public async Task AddApplicationRoleToCache(Guid employeeId, List roleIds) + { + var newRoleIds = roleIds.Select(r => r.ToString()).ToList(); + var newPermissionIds = await _context.RolePermissionMappings + .Where(rp => roleIds.Contains(rp.ApplicationRoleId)) + .Select(p => p.FeaturePermissionId.ToString()) + .Distinct() + .ToListAsync(); + + var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + + var update = Builders.Update + .AddToSetEach(e => e.ApplicationRoleIds, newRoleIds) + .AddToSetEach(e => e.PermissionIds, newPermissionIds); + + var result = await _collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); + if (result.MatchedCount == 0) + { + return false; + } + return true; + } + public async Task AddProjectsToCache(Guid employeeId, List projectIds) + { + var newprojectIds = projectIds.Select(p => p.ToString()).ToList(); + + var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + + var update = Builders.Update + .AddToSetEach(e => e.ProjectIds, newprojectIds); + + var result = await _collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); + if (result.MatchedCount == 0) + { + return false; + } + return true; + } + public async Task> GetProjectsFromCache(Guid employeeId) + { + var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + + + var result = await _collection + .Find(filter) + .FirstOrDefaultAsync(); + + var projectIds = new List(); + if (result != null) + { + projectIds = result.ProjectIds.Select(Guid.Parse).ToList(); + } + + return projectIds; + } + public async Task> GetPermissionsFromCache(Guid employeeId) + { + var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + + + var result = await _collection + .Find(filter) + .FirstOrDefaultAsync(); + + var permissionIds = new List(); + if (result != null) + { + permissionIds = result.PermissionIds.Select(Guid.Parse).ToList(); + } + + return permissionIds; + } + public async Task ClearAllProjectIdsFromCache(Guid employeeId) + { + var filter = Builders.Filter + .Eq(e => e.EmployeeId, employeeId.ToString()); + + var update = Builders.Update + .Set(e => e.ProjectIds, new List()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + return true; + } + public async Task RemoveRoleIdFromCache(Guid employeeId, Guid roleId) + { + var filter = Builders.Filter + .Eq(e => e.EmployeeId, employeeId.ToString()); + + var update = Builders.Update + .Pull(e => e.ApplicationRoleIds, roleId.ToString()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + if (result.ModifiedCount == 0) + return false; + + return true; + } + public async Task ClearAllPermissionIdsByEmployeeIDFromCache(Guid employeeId) + { + var filter = Builders.Filter + .Eq(e => e.EmployeeId, employeeId.ToString()); + + var update = Builders.Update + .Set(e => e.PermissionIds, new List()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + return true; + } + public async Task ClearAllPermissionIdsByRoleIdFromCache(Guid roleId) + { + var filter = Builders.Filter.AnyEq(e => e.ApplicationRoleIds, roleId.ToString()); + + var update = Builders.Update + .Set(e => e.PermissionIds, new List()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + return true; + } + } +} diff --git a/Marco.Pms.CacheHelper/Marco.Pms.CacheHelper.csproj b/Marco.Pms.CacheHelper/Marco.Pms.CacheHelper.csproj new file mode 100644 index 0000000..e12ac6c --- /dev/null +++ b/Marco.Pms.CacheHelper/Marco.Pms.CacheHelper.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs new file mode 100644 index 0000000..b667694 --- /dev/null +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -0,0 +1,434 @@ +using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.MongoDBModels; +using Marco.Pms.Model.Projects; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Marco.Pms.CacheHelper +{ + public class ProjectCache + { + private readonly ApplicationDbContext _context; + private readonly IMongoDatabase _mongoDB; + //private readonly ILoggingService _logger; + public ProjectCache(ApplicationDbContext context, IConfiguration configuration) + { + var connectionString = configuration["MongoDB:ConnectionString"]; + _context = context; + var mongoUrl = new MongoUrl(connectionString); + var client = new MongoClient(mongoUrl); // Your MongoDB connection string + _mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name + } + public async Task AddProjectDetailsToCache(Project project) + { + var projectCollection = _mongoDB.GetCollection("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(), + AreaName = wa.AreaName, + PlannedWork = waPlanned, + CompletedWork = waCompleted + }); + + floorPlanned += waPlanned; + floorCompleted += waCompleted; + } + + floorMongoList.Add(new FloorMongoDB + { + Id = floor.Id.ToString(), + FloorName = floor.FloorName, + PlannedWork = floorPlanned, + CompletedWork = floorCompleted, + WorkAreas = workAreaMongoList + }); + + buildingPlanned += floorPlanned; + buildingCompleted += floorCompleted; + } + + buildingMongoList.Add(new BuildingMongoDB + { + Id = building.Id.ToString(), + BuildingName = building.Name, + Description = building.Description, + PlannedWork = buildingPlanned, + CompletedWork = buildingCompleted, + Floors = floorMongoList + }); + + totalPlannedWork += buildingPlanned; + totalCompletedWork += buildingCompleted; + } + + projectDetails.Buildings = buildingMongoList; + projectDetails.PlannedWork = totalPlannedWork; + projectDetails.CompletedWork = totalCompletedWork; + + await projectCollection.InsertOneAsync(projectDetails); + //_logger.LogInfo("[AddProjectDetails] Project details inserted in MongoDB for ProjectId: {ProjectId}", project.Id); + } + public async Task UpdateProjectDetailsOnlyToCache(Project project) + { + //_logger.LogInfo("Starting update for project: {ProjectId}", project.Id); + + var projectStatus = await _context.StatusMasters + .FirstOrDefaultAsync(s => s.Id == project.ProjectStatusId); + + if (projectStatus == null) + { + //_logger.LogWarning("StatusMaster not found for ProjectStatusId: {StatusId}", project.ProjectStatusId); + } + + var projectCollection = _mongoDB.GetCollection("ProjectDetails"); + + // Build the update definition + var updates = Builders.Update.Combine( + Builders.Update.Set(r => r.Name, project.Name), + Builders.Update.Set(r => r.ProjectAddress, project.ProjectAddress), + Builders.Update.Set(r => r.ShortName, project.ShortName), + Builders.Update.Set(r => r.ProjectStatus, new StatusMasterMongoDB + { + Id = projectStatus?.Id.ToString(), + Status = projectStatus?.Status + }), + Builders.Update.Set(r => r.StartDate, project.StartDate), + Builders.Update.Set(r => r.EndDate, project.EndDate), + Builders.Update.Set(r => r.ContactPerson, project.ContactPerson) + ); + + // Perform the update + var result = await projectCollection.UpdateOneAsync( + filter: r => r.Id == project.Id.ToString(), + update: updates + ); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("No project matched in MongoDB for update. ProjectId: {ProjectId}", project.Id); + return false; + } + + //_logger.LogInfo("Project {ProjectId} successfully updated in MongoDB", project.Id); + return true; + } + public async Task GetProjectDetailsFromCache(Guid projectId) + { + var projectCollection = _mongoDB.GetCollection("ProjectDetails"); + + // Build filter and projection to exclude large 'Buildings' list + var filter = Builders.Filter.Eq(p => p.Id, projectId.ToString()); + var projection = Builders.Projection.Exclude(p => p.Buildings); + + //_logger.LogInfo("Fetching project details for ProjectId: {ProjectId} from MongoDB", projectId); + + // Perform query + var project = await projectCollection + .Find(filter) + .Project(projection) + .FirstOrDefaultAsync(); + + if (project == null) + { + //_logger.LogWarning("No project found in MongoDB for ProjectId: {ProjectId}", projectId); + return null; + } + + //// Deserialize the result manually + //var project = BsonSerializer.Deserialize(result); + + //_logger.LogInfo("Successfully fetched project details (excluding Buildings) for ProjectId: {ProjectId}", projectId); + return project; + } + public async Task AddBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId) + { + var stringProjectId = projectId.ToString(); + var projectCollection = _mongoDB.GetCollection("ProjectDetails"); + + // Add Building + if (building != null) + { + var buildingMongo = new BuildingMongoDB + { + Id = building.Id.ToString(), + BuildingName = building.Name, + Description = building.Description, + PlannedWork = 0, + CompletedWork = 0, + Floors = new List() + }; + + var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); + var update = Builders.Update.Push("Buildings", buildingMongo); + + var result = await projectCollection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("Project not found while adding building. ProjectId: {ProjectId}", projectId); + return; + } + + //_logger.LogInfo("Building {BuildingId} added to project {ProjectId}", building.Id, projectId); + return; + } + + // Add Floor + if (floor != null) + { + var floorMongo = new FloorMongoDB + { + Id = floor.Id.ToString(), + FloorName = floor.FloorName, + PlannedWork = 0, + CompletedWork = 0, + WorkAreas = new List() + }; + + var filter = Builders.Filter.And( + Builders.Filter.Eq(p => p.Id, stringProjectId), + Builders.Filter.Eq("Buildings._id", floor.BuildingId.ToString()) + ); + + var update = Builders.Update.Push("Buildings.$.Floors", floorMongo); + var result = await projectCollection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("Project or building not found while adding floor. ProjectId: {ProjectId}, BuildingId: {BuildingId}", projectId, floor.BuildingId); + return; + } + + //_logger.LogInfo("Floor {FloorId} added to building {BuildingId} in project {ProjectId}", floor.Id, floor.BuildingId, projectId); + return; + } + + // Add WorkArea + if (workArea != null && buildingId != null) + { + var workAreaMongo = new WorkAreaMongoDB + { + Id = workArea.Id.ToString(), + AreaName = workArea.AreaName, + PlannedWork = 0, + CompletedWork = 0 + }; + + var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); + + var arrayFilters = new List + { + new JsonArrayFilterDefinition("{ 'b._id': '" + buildingId + "' }"), + new JsonArrayFilterDefinition("{ 'f._id': '" + workArea.FloorId + "' }") + }; + + var update = Builders.Update.Push("Buildings.$[b].Floors.$[f].WorkAreas", workAreaMongo); + var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; + + var result = await projectCollection.UpdateOneAsync(filter, update, updateOptions); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("Project or nested structure not found while adding work area. ProjectId: {ProjectId}, BuildingId: {BuildingId}, FloorId: {FloorId}", projectId, buildingId, workArea.FloorId); + return; + } + + //_logger.LogInfo("WorkArea {WorkAreaId} added to floor {FloorId} in building {BuildingId}, ProjectId: {ProjectId}", workArea.Id, workArea.FloorId, buildingId, projectId); + return; + } + + // Fallback case when no valid data was passed + //_logger.LogWarning("No valid infra data provided to add for ProjectId: {ProjectId}", projectId); + } + public async Task UpdateBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId) + { + var stringProjectId = projectId.ToString(); + var projectCollection = _mongoDB.GetCollection("ProjectDetails"); + + // Update Building + if (building != null) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(p => p.Id, stringProjectId), + Builders.Filter.Eq("Buildings._id", building.Id.ToString()) + ); + + var update = Builders.Update.Combine( + Builders.Update.Set("Buildings.$.BuildingName", building.Name), + Builders.Update.Set("Buildings.$.Description", building.Description) + ); + + var result = await projectCollection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("Update failed: Project or Building not found. ProjectId: {ProjectId}, BuildingId: {BuildingId}", projectId, building.Id); + return false; + } + + //_logger.LogInfo("Building {BuildingId} updated successfully in project {ProjectId}", building.Id, projectId); + return true; + } + + // Update Floor + if (floor != null) + { + var arrayFilters = new List + { + new JsonArrayFilterDefinition("{ 'b._id': '" + floor.BuildingId + "' }"), + new JsonArrayFilterDefinition("{ 'f._id': '" + floor.Id + "' }") + }; + + var update = Builders.Update.Set("Buildings.$[b].Floors.$[f].FloorName", floor.FloorName); + var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; + var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); + + var result = await projectCollection.UpdateOneAsync(filter, update, updateOptions); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("Update failed: Project or Floor not found. ProjectId: {ProjectId}, BuildingId: {BuildingId}, FloorId: {FloorId}", projectId, floor.BuildingId, floor.Id); + return false; + } + + //_logger.LogInfo("Floor {FloorId} updated successfully in Building {BuildingId}, ProjectId: {ProjectId}", floor.Id, floor.BuildingId, projectId); + return true; + } + + // Update WorkArea + if (workArea != null && buildingId != null) + { + var arrayFilters = new List + { + new JsonArrayFilterDefinition("{ 'b._id': '" + buildingId + "' }"), + new JsonArrayFilterDefinition("{ 'f._id': '" + workArea.FloorId + "' }"), + new JsonArrayFilterDefinition("{ 'a._id': '" + workArea.Id + "' }") + }; + + var update = Builders.Update.Set("Buildings.$[b].Floors.$[f].WorkAreas.$[a].AreaName", workArea.AreaName); + var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; + var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); + + var result = await projectCollection.UpdateOneAsync(filter, update, updateOptions); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("Update failed: Project or WorkArea not found. ProjectId: {ProjectId}, BuildingId: {BuildingId}, FloorId: {FloorId}, WorkAreaId: {WorkAreaId}", + //projectId, buildingId, workArea.FloorId, workArea.Id); + return false; + } + + //_logger.LogInfo("WorkArea {WorkAreaId} updated successfully in Floor {FloorId}, Building {BuildingId}, ProjectId: {ProjectId}", + //workArea.Id, workArea.FloorId, buildingId, projectId); + return true; + } + + //_logger.LogWarning("No update performed. Missing or invalid data for ProjectId: {ProjectId}", projectId); + return false; + } + public async Task?> GetBuildingInfraFromCache(Guid projectId) + { + var projectCollection = _mongoDB.GetCollection("ProjectDetails"); + + // Filter by project ID + var filter = Builders.Filter.Eq(p => p.Id, projectId.ToString()); + + // Project only the "Buildings" field from the document + var buildings = await projectCollection + .Find(filter) + .Project(p => p.Buildings) + .FirstOrDefaultAsync(); + + //if (buildings == null) + //{ + // _logger.LogWarning("No building infrastructure found for ProjectId: {ProjectId}", projectId); + //} + //else + //{ + // _logger.LogInfo("Fetched {Count} buildings for ProjectId: {ProjectId}", buildings.Count, projectId); + //} + + return buildings; + } + } +} diff --git a/Marco.Pms.Model/Marco.Pms.Model.csproj b/Marco.Pms.Model/Marco.Pms.Model.csproj index d5927ce..a1a21a5 100644 --- a/Marco.Pms.Model/Marco.Pms.Model.csproj +++ b/Marco.Pms.Model/Marco.Pms.Model.csproj @@ -10,6 +10,7 @@ + diff --git a/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs new file mode 100644 index 0000000..37218b7 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs @@ -0,0 +1,9 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class ActivityMasterMongoDB + { + public string? Id { get; set; } + public string? ActivityName { get; set; } + public string? UnitOfMeasurement { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs b/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs new file mode 100644 index 0000000..87ccb8d --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs @@ -0,0 +1,18 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class BuildingMongoDB + { + public string Id { get; set; } = string.Empty; + public string? BuildingName { get; set; } + public string? Description { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } + public List? Floors { get; set; } + } + public class BuildingMongoDBVM + { + public string Id { get; set; } = string.Empty; + public string? Name { get; set; } + public string? Description { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs b/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs new file mode 100644 index 0000000..f141798 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs @@ -0,0 +1,13 @@ +using MongoDB.Bson.Serialization.Attributes; + +namespace Marco.Pms.Model.MongoDBModels +{ + [BsonIgnoreExtraElements] + public class EmployeePermissionMongoDB + { + public string EmployeeId { get; set; } = string.Empty; + public List ApplicationRoleIds { get; set; } = new List(); + public List PermissionIds { get; set; } = new List(); + public List ProjectIds { get; set; } = new List(); + } +} diff --git a/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs b/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs new file mode 100644 index 0000000..ae3975f --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs @@ -0,0 +1,17 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class FloorMongoDB + { + public string Id { get; set; } = string.Empty; + public string? FloorName { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } + public List? WorkAreas { get; set; } + } + + public class FloorMongoDBVM + { + public string Id { get; set; } = string.Empty; + public string? FloorName { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs new file mode 100644 index 0000000..8bf1c9a --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs @@ -0,0 +1,18 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class ProjectMongoDB + { + public string? Id { get; set; } + public string? Name { get; set; } + public string? ShortName { get; set; } + public string? ProjectAddress { get; set; } + public string? ContactPerson { get; set; } + public List? Buildings { get; set; } + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + public StatusMasterMongoDB? ProjectStatus { get; set; } + public int TeamSize { get; set; } + public double CompletedWork { get; set; } + public double PlannedWork { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs new file mode 100644 index 0000000..01a0552 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs @@ -0,0 +1,8 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class StatusMasterMongoDB + { + public string? Id { get; set; } + public string? Status { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs new file mode 100644 index 0000000..d17f52c --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs @@ -0,0 +1,15 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class WorkAreaMongoDB + { + public string Id { get; set; } = string.Empty; + public string? AreaName { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } + } + public class WorkAreaMongoDBVM + { + public string Id { get; set; } = string.Empty; + public string? AreaName { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs new file mode 100644 index 0000000..aef0ada --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs @@ -0,0 +1,9 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class WorkCategoryMasterMongoDB + { + public string? Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + } +} diff --git a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs new file mode 100644 index 0000000..dc7fdb9 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs @@ -0,0 +1,15 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class WorkItemMongoDB + { + public string? Id { get; set; } + public string? WorkAreaId { get; set; } + public ActivityMasterMongoDB? ActivityMaster { get; set; } + public WorkCategoryMasterMongoDB? WorkCategoryMaster { get; set; } + public string? ParentTaskId { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } + public string? Description { get; set; } + public DateTime TaskDate { get; set; } + } +} diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 6490c54..a440c21 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -2,10 +2,13 @@ using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Employees; 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; using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Model.ViewModels.Projects; +using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Service; using MarcoBMS.Services.Helpers; @@ -14,6 +17,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; +using MongoDB.Driver; namespace MarcoBMS.Services.Controllers { @@ -29,6 +33,7 @@ namespace MarcoBMS.Services.Controllers private readonly ProjectsHelper _projectsHelper; private readonly IHubContext _signalR; private readonly PermissionServices _permission; + private readonly CacheUpdateHelper _cache; private readonly Guid ViewProjects; private readonly Guid ManageProject; private readonly Guid ViewInfra; @@ -37,7 +42,7 @@ namespace MarcoBMS.Services.Controllers public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, ProjectsHelper projectHelper, - IHubContext signalR, PermissionServices permission) + IHubContext signalR, PermissionServices permission, CacheUpdateHelper cache) { _context = context; _userHelper = userHelper; @@ -45,13 +50,13 @@ namespace MarcoBMS.Services.Controllers _rolesHelper = rolesHelper; _projectsHelper = projectHelper; _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"); tenantId = _userHelper.GetTenantId(); - } [HttpGet("list/basic")] @@ -222,24 +227,54 @@ namespace MarcoBMS.Services.Controllers } // Step 5: Fetch project with status - var project = await _context.Projects + var projectDetails = await _cache.GetProjectDetails(id); + ProjectVM? projectVM = null; + if (projectDetails == null) + { + var project = await _context.Projects .Include(c => c.ProjectStatus) .FirstOrDefaultAsync(c => c.TenantId == tenantId && c.Id == id); + projectVM = GetProjectViewModel(project); + } + else + { + 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 (project == null) + if (projectVM == null) { _logger.LogWarning("Project not found. ProjectId: {ProjectId}", id); return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); } - // Step 6: Map and return result - var projectVM = GetProjectViewModel(project); + // Step 6: Return result + _logger.LogInfo("Project details fetched successfully. ProjectId: {ProjectId}", id); return Ok(ApiResponse.SuccessResponse(projectVM, "Project details fetched successfully", 200)); } - private ProjectVM GetProjectViewModel(Project project) + private ProjectVM? GetProjectViewModel(Project? project) { + if (project == null) + { + return null; + } return new ProjectVM { Id = project.Id, @@ -280,6 +315,9 @@ namespace MarcoBMS.Services.Controllers _context.Projects.Add(project); await _context.SaveChangesAsync(); + + await _cache.AddProjectDetails(project); + var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Create_Project", Response = project.ToProjectDto() }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); @@ -310,6 +348,13 @@ namespace MarcoBMS.Services.Controllers await _context.SaveChangesAsync(); + // Cache functions + bool isUpdated = await _cache.UpdateProjectDetailsOnly(project); + if (!isUpdated) + { + await _cache.AddProjectDetails(project); + } + var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Update_Project", Response = project.ToProjectDto() }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); @@ -524,6 +569,7 @@ namespace MarcoBMS.Services.Controllers employeeIds.Add(projectAllocation.EmployeeId); projectIds.Add(projectAllocation.ProjectId); } + await _cache.ClearAllProjectIds(item.EmpID); } catch (Exception ex) @@ -565,53 +611,102 @@ namespace MarcoBMS.Services.Controllers _logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have access to view infra", 403)); } - - // Step 4: Fetch buildings for the project - var buildings = await _context.Buildings - .Where(b => b.ProjectId == projectId) - .ToListAsync(); - - var buildingIds = buildings.Select(b => b.Id).ToList(); - - // Step 5: Fetch floors associated with the buildings - var floors = await _context.Floor - .Where(f => buildingIds.Contains(f.BuildingId)) - .ToListAsync(); - - var floorIds = floors.Select(f => f.Id).ToList(); - - // Step 6: Fetch work areas associated with the floors - var workAreas = await _context.WorkAreas - .Where(wa => floorIds.Contains(wa.FloorId)) - .ToListAsync(); - - // Step 7: Build the infra hierarchy (Building > Floors > Work Areas) - var infraVM = buildings.Select(b => + var result = await _cache.GetBuildingInfra(projectId); + if (result == null) { - var selectedFloors = floors - .Where(f => f.BuildingId == b.Id) - .Select(f => new - { - Id = f.Id, - FloorName = f.FloorName, - WorkAreas = workAreas - .Where(wa => wa.FloorId == f.Id) - .Select(wa => new { wa.Id, wa.AreaName }) - .ToList() - }).ToList(); - return new + // Step 4: Fetch buildings for the project + var buildings = await _context.Buildings + .Where(b => b.ProjectId == projectId) + .ToListAsync(); + + var buildingIds = buildings.Select(b => b.Id).ToList(); + + // Step 5: Fetch floors associated with the buildings + var floors = await _context.Floor + .Where(f => buildingIds.Contains(f.BuildingId)) + .ToListAsync(); + + var floorIds = floors.Select(f => f.Id).ToList(); + + // Step 6: Fetch work areas associated with the floors + var workAreas = await _context.WorkAreas + .Where(wa => floorIds.Contains(wa.FloorId)) + .ToListAsync(); + var workAreaIds = workAreas.Select(wa => wa.Id).ToList(); + + // Step 7: Fetch work items associated with the work area + var workItems = await _context.WorkItems + .Where(wi => workAreaIds.Contains(wi.WorkAreaId)) + .ToListAsync(); + + // Step 8: Build the infra hierarchy (Building > Floors > Work Areas) + List Buildings = new List(); + foreach (var building in buildings) { - Id = b.Id, - BuildingName = b.Name, - Floors = selectedFloors - }; - }).ToList(); + double buildingPlannedWorks = 0; + double buildingCompletedWorks = 0; + + var selectedFloors = floors.Where(f => f.BuildingId == building.Id).ToList(); + List Floors = new List(); + foreach (var floor in selectedFloors) + { + double floorPlannedWorks = 0; + double floorCompletedWorks = 0; + var selectedWorkAreas = workAreas.Where(wa => wa.FloorId == floor.Id).ToList(); + List WorkAreas = new List(); + foreach (var workArea in selectedWorkAreas) + { + double workAreaPlannedWorks = 0; + double workAreaCompletedWorks = 0; + var selectedWorkItems = workItems.Where(wi => wi.WorkAreaId == workArea.Id).ToList(); + foreach (var workItem in selectedWorkItems) + { + workAreaPlannedWorks += workItem.PlannedWork; + workAreaCompletedWorks += workItem.CompletedWork; + } + WorkAreaMongoDB workAreaMongo = new WorkAreaMongoDB + { + Id = workArea.Id.ToString(), + AreaName = workArea.AreaName, + PlannedWork = workAreaPlannedWorks, + CompletedWork = workAreaCompletedWorks + }; + WorkAreas.Add(workAreaMongo); + floorPlannedWorks += workAreaPlannedWorks; + floorCompletedWorks += workAreaCompletedWorks; + } + FloorMongoDB floorMongoDB = new FloorMongoDB + { + Id = floor.Id.ToString(), + FloorName = floor.FloorName, + PlannedWork = floorPlannedWorks, + CompletedWork = floorCompletedWorks, + WorkAreas = WorkAreas + }; + Floors.Add(floorMongoDB); + buildingPlannedWorks += floorPlannedWorks; + buildingCompletedWorks += floorCompletedWorks; + } + + var buildingMongo = new BuildingMongoDB + { + Id = building.Id.ToString(), + BuildingName = building.Name, + Description = building.Description, + PlannedWork = buildingPlannedWorks, + CompletedWork = buildingCompletedWorks, + Floors = Floors + }; + Buildings.Add(buildingMongo); + } + result = Buildings; + } _logger.LogInfo("Infra details fetched successfully for ProjectId: {ProjectId}, EmployeeId: {EmployeeId}, Buildings: {Count}", - projectId, loggedInEmployee.Id, infraVM.Count); + projectId, loggedInEmployee.Id, result.Count); - return Ok(ApiResponse.SuccessResponse(infraVM, "Infra details fetched successfully", 200)); + return Ok(ApiResponse.SuccessResponse(result, "Infra details fetched successfully", 200)); } [HttpGet("tasks/{workAreaId}")] @@ -807,6 +902,7 @@ namespace MarcoBMS.Services.Controllers responseData.building = building; responseMessage = "Buliding Added Successfully"; message = "Building Added"; + await _cache.AddBuildngInfra(building.ProjectId, building); } else { @@ -816,7 +912,7 @@ namespace MarcoBMS.Services.Controllers responseData.building = building; responseMessage = "Buliding Updated Successfully"; message = "Building Updated"; - + await _cache.UpdateBuildngInfra(building.ProjectId, building); } projectIds.Add(building.ProjectId); } @@ -824,6 +920,7 @@ namespace MarcoBMS.Services.Controllers { Floor floor = item.Floor.ToFloorFromFloorDto(tenantId); floor.TenantId = GetTenantId(); + bool isCreated = false; if (item.Floor.Id == null) { @@ -833,6 +930,7 @@ namespace MarcoBMS.Services.Controllers responseData.floor = floor; responseMessage = "Floor Added Successfully"; message = "Floor Added"; + isCreated = true; } else { @@ -844,13 +942,23 @@ namespace MarcoBMS.Services.Controllers message = "Floor Updated"; } Building? building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == floor.BuildingId); - projectIds.Add(building?.ProjectId ?? Guid.Empty); + var projectId = building?.ProjectId ?? Guid.Empty; + projectIds.Add(projectId); message = $"{message} in Building: {building?.Name}"; + if (isCreated) + { + await _cache.AddBuildngInfra(projectId, floor: floor); + } + else + { + await _cache.UpdateBuildngInfra(projectId, floor: floor); + } } if (item.WorkArea != null) { WorkArea workArea = item.WorkArea.ToWorkAreaFromWorkAreaDto(tenantId); workArea.TenantId = GetTenantId(); + bool isCreated = false; if (item.WorkArea.Id == null) { @@ -860,6 +968,7 @@ namespace MarcoBMS.Services.Controllers responseData.workArea = workArea; responseMessage = "Work Area Added Successfully"; message = "Work Area Added"; + isCreated = true; } else { @@ -871,8 +980,17 @@ namespace MarcoBMS.Services.Controllers message = "Work Area Updated"; } Floor? floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == workArea.FloorId); - projectIds.Add(floor?.Building?.ProjectId ?? Guid.Empty); + var projectId = floor?.Building?.ProjectId ?? Guid.Empty; + projectIds.Add(projectId); message = $"{message} in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}"; + if (isCreated) + { + await _cache.AddBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); + } + else + { + await _cache.UpdateBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); + } } } message = $"{message} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}"; @@ -996,6 +1114,7 @@ namespace MarcoBMS.Services.Controllers return Ok(ApiResponse.ErrorResponse(ex.Message, ex, 400)); } } + await _cache.ClearAllProjectIds(employeeId); var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeId = employeeId }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); diff --git a/Marco.Pms.Services/Controllers/RolesController.cs b/Marco.Pms.Services/Controllers/RolesController.cs index 2ac2b07..4c75b3e 100644 --- a/Marco.Pms.Services/Controllers/RolesController.cs +++ b/Marco.Pms.Services/Controllers/RolesController.cs @@ -10,6 +10,7 @@ using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels; using Marco.Pms.Model.ViewModels.Master; using Marco.Pms.Model.ViewModels.Roles; +using Marco.Pms.Services.Helpers; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; @@ -29,14 +30,17 @@ namespace MarcoBMS.Services.Controllers private readonly UserHelper _userHelper; private readonly UserManager _userManager; private readonly ILoggingService _logger; + private readonly CacheUpdateHelper _cache; - public RolesController(UserManager userManager, ApplicationDbContext context, RolesHelper rolesHelper, UserHelper userHelper, ILoggingService logger) + public RolesController(UserManager userManager, ApplicationDbContext context, RolesHelper rolesHelper, UserHelper userHelper, ILoggingService logger, + CacheUpdateHelper cache) { _context = context; _userManager = userManager; _rolesHelper = rolesHelper; _userHelper = userHelper; _logger = logger; + _cache = cache; } private Guid GetTenantId() @@ -292,6 +296,8 @@ namespace MarcoBMS.Services.Controllers if (modified) await _context.SaveChangesAsync(); + await _cache.ClearAllPermissionIdsByRoleId(id); + ApplicationRolesVM response = role.ToRoleVMFromApplicationRole(); List permissions = await _rolesHelper.GetFeaturePermissionByRoleID(response.Id); response.FeaturePermission = permissions.Select(c => c.ToFeaturePermissionVMFromFeaturePermission()).ToList(); @@ -424,12 +430,16 @@ namespace MarcoBMS.Services.Controllers if (role.IsEnabled == true) { _context.EmployeeRoleMappings.Add(mapping); + await _cache.AddApplicationRole(role.EmployeeId, [mapping.RoleId]); } } else if (role.IsEnabled == false) { _context.EmployeeRoleMappings.Remove(existingItem); + await _cache.RemoveRoleId(existingItem.EmployeeId, existingItem.RoleId); + await _cache.ClearAllPermissionIdsByEmployeeID(existingItem.EmployeeId); } + await _cache.ClearAllProjectIds(role.EmployeeId); } await _context.SaveChangesAsync(); diff --git a/Marco.Pms.Services/Dockerfile b/Marco.Pms.Services/Dockerfile index 5444e56..77311ee 100644 --- a/Marco.Pms.Services/Dockerfile +++ b/Marco.Pms.Services/Dockerfile @@ -19,6 +19,7 @@ COPY ["Marco.Pms.Services/Marco.Pms.Services.csproj", "Marco.Pms.Services/"] COPY ["Marco.Pms.DataAccess/Marco.Pms.DataAccess.csproj", "Marco.Pms.DataAccess/"] COPY ["Marco.Pms.Model/Marco.Pms.Model.csproj", "Marco.Pms.Model/"] COPY ["Marco.Pms.Utility/Marco.Pms.Utility.csproj", "Marco.Pms.Utility/"] +COPY ["Marco.Pms.Utility/Marco.Pms.CacheHelper.csproj", "Marco.Pms.CacheHelper/"] RUN dotnet restore "./Marco.Pms.Services/Marco.Pms.Services.csproj" COPY . . WORKDIR "/src/Marco.Pms.Services" diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs new file mode 100644 index 0000000..1c3ee70 --- /dev/null +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -0,0 +1,98 @@ +using Marco.Pms.CacheHelper; +using Marco.Pms.Model.MongoDBModels; +using Marco.Pms.Model.Projects; +using Project = Marco.Pms.Model.Projects.Project; + +namespace Marco.Pms.Services.Helpers +{ + public class CacheUpdateHelper + { + private readonly ProjectCache _projectCache; + private readonly EmployeeCache _employeeCache; + + public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache) + { + _projectCache = projectCache; + _employeeCache = employeeCache; + } + + // ------------------------------------ Project Details and Infrastructure Cache --------------------------------------- + public async Task AddProjectDetails(Project project) + { + await _projectCache.AddProjectDetailsToCache(project); + } + public async Task UpdateProjectDetailsOnly(Project project) + { + bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project); + return response; + } + public async Task GetProjectDetails(Guid projectId) + { + var response = await _projectCache.GetProjectDetailsFromCache(projectId); + return response; + } + public async Task AddBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) + { + await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + } + public async Task UpdateBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) + { + var response = await _projectCache.UpdateBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + if (!response) + { + await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + } + } + public async Task?> GetBuildingInfra(Guid projectId) + { + var response = await _projectCache.GetBuildingInfraFromCache(projectId); + return response; + } + + + // ------------------------------------ Employee Profile Cache --------------------------------------- + public async Task AddApplicationRole(Guid employeeId, List roleIds) + { + var response = await _employeeCache.AddApplicationRoleToCache(employeeId, roleIds); + } + public async Task AddProjects(Guid employeeId, List projectIds) + { + var response = await _employeeCache.AddProjectsToCache(employeeId, projectIds); + return response; + } + public async Task?> GetProjects(Guid employeeId) + { + var response = await _employeeCache.GetProjectsFromCache(employeeId); + if (response.Count > 0) + { + return response; + } + return null; + } + public async Task?> GetPermissions(Guid employeeId) + { + var response = await _employeeCache.GetPermissionsFromCache(employeeId); + if (response.Count > 0) + { + return response; + } + return null; + } + public async Task ClearAllProjectIds(Guid employeeId) + { + var response = await _employeeCache.ClearAllProjectIdsFromCache(employeeId); + } + public async Task ClearAllPermissionIdsByEmployeeID(Guid employeeId) + { + var response = await _employeeCache.ClearAllPermissionIdsByEmployeeIDFromCache(employeeId); + } + public async Task ClearAllPermissionIdsByRoleId(Guid roleId) + { + var response = await _employeeCache.ClearAllPermissionIdsByRoleIdFromCache(roleId); + } + public async Task RemoveRoleId(Guid employeeId, Guid roleId) + { + var response = await _employeeCache.RemoveRoleIdFromCache(employeeId, roleId); + } + } +} diff --git a/Marco.Pms.Services/Helpers/ProjectsHelper.cs b/Marco.Pms.Services/Helpers/ProjectsHelper.cs index 8ccbc85..3ccddba 100644 --- a/Marco.Pms.Services/Helpers/ProjectsHelper.cs +++ b/Marco.Pms.Services/Helpers/ProjectsHelper.cs @@ -2,11 +2,8 @@ using Marco.Pms.Model.Employees; using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Projects; -using Marco.Pms.Model.Utilities; -using Marco.Pms.Model.ViewModels.Projects; -using Microsoft.AspNetCore.Mvc; +using Marco.Pms.Services.Helpers; using Microsoft.EntityFrameworkCore; -using ModelServices.Helpers; namespace MarcoBMS.Services.Helpers { @@ -14,12 +11,14 @@ namespace MarcoBMS.Services.Helpers { private readonly ApplicationDbContext _context; private readonly RolesHelper _rolesHelper; + private readonly CacheUpdateHelper _cache; - public ProjectsHelper(ApplicationDbContext context, RolesHelper rolesHelper) + public ProjectsHelper(ApplicationDbContext context, RolesHelper rolesHelper, CacheUpdateHelper cache) { _context = context; _rolesHelper = rolesHelper; + _cache = cache; } public async Task> GetAllProjectByTanentID(Guid tanentID) @@ -53,40 +52,56 @@ namespace MarcoBMS.Services.Helpers public async Task> GetMyProjects(Guid tenantId, Employee LoggedInEmployee) { - List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(LoggedInEmployee.Id); - string[] projectsId = []; List projects = new List(); - // Define a common queryable base for projects - IQueryable projectQuery = _context.Projects.Where(c => c.TenantId == tenantId); + var projectIds = await _cache.GetProjects(LoggedInEmployee.Id); - // 2. Optimized Project Retrieval Logic - // 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) { - // 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 + projects = await _context.Projects.Where(p => projectIds.Contains(p.Id)).ToListAsync(); } 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()) + var featurePermissionIds = await _cache.GetPermissions(LoggedInEmployee.Id); + if (featurePermissionIds == null) { - return new List(); + 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); - // Use LINQ's Contains for efficient filtering by ProjectId - var projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); // Get distinct Guids + // 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 + } + else + { + // 3. Efficiently get project allocations and then filter projects + // Load allocations only once + var allocation = await GetProjectByEmployeeID(LoggedInEmployee.Id); - // Filter projects based on the retrieved ProjectIds - projects = await projectQuery.Where(c => projectIds.Contains(c.Id)).ToListAsync(); + // If there are no allocations, return an empty list early + if (allocation == null || !allocation.Any()) + { + return new List(); + } + + // 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(); + + } + projectIds = projects.Select(p => p.Id).ToList(); + await _cache.AddProjects(LoggedInEmployee.Id, projectIds); } return projects; diff --git a/Marco.Pms.Services/Helpers/RolesHelper.cs b/Marco.Pms.Services/Helpers/RolesHelper.cs index b571d03..15bf0b1 100644 --- a/Marco.Pms.Services/Helpers/RolesHelper.cs +++ b/Marco.Pms.Services/Helpers/RolesHelper.cs @@ -2,6 +2,7 @@ using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Entitlements; +using Marco.Pms.Services.Helpers; using Microsoft.EntityFrameworkCore; namespace MarcoBMS.Services.Helpers @@ -9,15 +10,19 @@ namespace MarcoBMS.Services.Helpers public class RolesHelper { private readonly ApplicationDbContext _context; - public RolesHelper(ApplicationDbContext context) + private readonly CacheUpdateHelper _cache; + public RolesHelper(ApplicationDbContext context, CacheUpdateHelper cache) { _context = context; + _cache = cache; } public async Task> GetFeaturePermissionByEmployeeID(Guid EmployeeID) { List roleMappings = await _context.EmployeeRoleMappings.Where(c => c.EmployeeId == EmployeeID && c.IsEnabled == true).Select(c => c.RoleId).ToListAsync(); + await _cache.AddApplicationRole(EmployeeID, roleMappings); + // _context.RolePermissionMappings var result = await (from rpm in _context.RolePermissionMappings diff --git a/Marco.Pms.Services/Marco.Pms.Services.csproj b/Marco.Pms.Services/Marco.Pms.Services.csproj index 7bef32f..a235e6a 100644 --- a/Marco.Pms.Services/Marco.Pms.Services.csproj +++ b/Marco.Pms.Services/Marco.Pms.Services.csproj @@ -44,6 +44,7 @@ + diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index 17eb5c7..1d9b4b3 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -1,4 +1,5 @@ using System.Text; +using Marco.Pms.CacheHelper; using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Authentication; using Marco.Pms.Model.Entitlements; @@ -136,6 +137,9 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddSingleton(); @@ -225,7 +229,7 @@ app.UseStaticFiles(); // Enables serving static files app.UseHttpsRedirection(); - +app.UseAuthentication(); app.UseAuthorization(); app.MapHub("/hubs/marco"); app.MapControllers(); diff --git a/Marco.Pms.Services/Service/PermissionServices.cs b/Marco.Pms.Services/Service/PermissionServices.cs index f3ddb58..ce7476b 100644 --- a/Marco.Pms.Services/Service/PermissionServices.cs +++ b/Marco.Pms.Services/Service/PermissionServices.cs @@ -2,6 +2,7 @@ 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,21 +13,24 @@ namespace Marco.Pms.Services.Service private readonly ApplicationDbContext _context; private readonly RolesHelper _rolesHelper; private readonly ProjectsHelper _projectsHelper; - public PermissionServices(ApplicationDbContext context, RolesHelper rolesHelper, ProjectsHelper projectsHelper) + private readonly CacheUpdateHelper _cache; + public PermissionServices(ApplicationDbContext context, RolesHelper rolesHelper, ProjectsHelper projectsHelper, CacheUpdateHelper cache) { _context = context; _rolesHelper = rolesHelper; _projectsHelper = projectsHelper; + _cache = cache; } public async Task HasPermission(Guid featurePermissionId, Guid employeeId) { - var hasPermission = await _context.EmployeeRoleMappings - .Where(er => er.EmployeeId == employeeId) - .Select(er => er.RoleId) - .Distinct() - .AnyAsync(roleId => _context.RolePermissionMappings - .Any(rp => rp.FeaturePermissionId == featurePermissionId && rp.ApplicationRoleId == roleId)); + var featurePermissionIds = await _cache.GetPermissions(employeeId); + if (featurePermissionIds == null) + { + List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(employeeId); + featurePermissionIds = featurePermission.Select(fp => fp.Id).ToList(); + } + var hasPermission = featurePermissionIds.Contains(featurePermissionId); return hasPermission; } public async Task HasProjectPermission(Employee emp, string projectId) diff --git a/Marco.Pms.Services/appsettings.Development.json b/Marco.Pms.Services/appsettings.Development.json index 1565018..ce80dc0 100644 --- a/Marco.Pms.Services/appsettings.Development.json +++ b/Marco.Pms.Services/appsettings.Development.json @@ -47,6 +47,8 @@ "BucketName": "testenv-marco-pms-documents" }, "MongoDB": { - "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs" + "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs", + "ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches" + //"DatabaseName": "" } } diff --git a/Marco.Pms.Services/appsettings.Production.json b/Marco.Pms.Services/appsettings.Production.json index 81aa998..0abe3f1 100644 --- a/Marco.Pms.Services/appsettings.Production.json +++ b/Marco.Pms.Services/appsettings.Production.json @@ -6,7 +6,7 @@ }, "Environment": { "Name": "Production", - "Title": "" + "Title": "" }, "ConnectionStrings": { "DefaultConnectionString": "Server=147.93.98.152;User ID=devuser;Password=AppUser@123$;Database=MarcoBMS1" @@ -40,6 +40,7 @@ "BucketName": "testenv-marco-pms-documents" }, "MongoDB": { - "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs" + "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs", + "ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches" } } \ No newline at end of file diff --git a/marco.pms.api.sln b/marco.pms.api.sln index 49d3e8c..424b709 100644 --- a/marco.pms.api.sln +++ b/marco.pms.api.sln @@ -11,6 +11,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Marco.Pms.Utility", "Marco. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Marco.Pms.Services", "Marco.Pms.Services\Marco.Pms.Services.csproj", "{27A83653-5B7F-4135-9886-01594D54AFAE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Marco.Pms.CacheHelper", "Marco.Pms.CacheHelper\Marco.Pms.CacheHelper.csproj", "{1A105C22-4ED7-4F54-8834-6923DDD96852}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,6 +35,10 @@ Global {27A83653-5B7F-4135-9886-01594D54AFAE}.Debug|Any CPU.Build.0 = Debug|Any CPU {27A83653-5B7F-4135-9886-01594D54AFAE}.Release|Any CPU.ActiveCfg = Release|Any CPU {27A83653-5B7F-4135-9886-01594D54AFAE}.Release|Any CPU.Build.0 = Release|Any CPU + {1A105C22-4ED7-4F54-8834-6923DDD96852}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A105C22-4ED7-4F54-8834-6923DDD96852}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A105C22-4ED7-4F54-8834-6923DDD96852}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A105C22-4ED7-4F54-8834-6923DDD96852}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 7be7d6f13dedfa3c1cec127070a9e09c40eac60e Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Fri, 4 Jul 2025 17:50:27 +0530 Subject: [PATCH 015/124] removed comented code from appsetting file --- Marco.Pms.Services/appsettings.Development.json | 1 - 1 file changed, 1 deletion(-) diff --git a/Marco.Pms.Services/appsettings.Development.json b/Marco.Pms.Services/appsettings.Development.json index ce80dc0..5f5e19d 100644 --- a/Marco.Pms.Services/appsettings.Development.json +++ b/Marco.Pms.Services/appsettings.Development.json @@ -49,6 +49,5 @@ "MongoDB": { "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs", "ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches" - //"DatabaseName": "" } } From 6d8939d942e28b98df084a767ef5b7e58a2b8dcf Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Sat, 5 Jul 2025 12:23:28 +0530 Subject: [PATCH 016/124] Added Pagenation to Image List API --- .../Dtos/DocumentManager/DocumentBatchDto.cs | 10 +++ .../Controllers/ImageController.cs | 73 ++++++++++++------- 2 files changed, 58 insertions(+), 25 deletions(-) create mode 100644 Marco.Pms.Model/Dtos/DocumentManager/DocumentBatchDto.cs diff --git a/Marco.Pms.Model/Dtos/DocumentManager/DocumentBatchDto.cs b/Marco.Pms.Model/Dtos/DocumentManager/DocumentBatchDto.cs new file mode 100644 index 0000000..a3befae --- /dev/null +++ b/Marco.Pms.Model/Dtos/DocumentManager/DocumentBatchDto.cs @@ -0,0 +1,10 @@ +using Marco.Pms.Model.DocumentManager; + +namespace Marco.Pms.Model.Dtos.DocumentManager +{ + public class DocumentBatchDto + { + public Guid? BatchId { get; set; } + public List? Documents { get; set; } + } +} diff --git a/Marco.Pms.Services/Controllers/ImageController.cs b/Marco.Pms.Services/Controllers/ImageController.cs index 44952c6..5c3cdc5 100644 --- a/Marco.Pms.Services/Controllers/ImageController.cs +++ b/Marco.Pms.Services/Controllers/ImageController.cs @@ -1,6 +1,7 @@ using System.Text.Json; using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Activities; +using Marco.Pms.Model.Dtos.DocumentManager; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Projects; @@ -10,6 +11,7 @@ using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.CodeAnalysis; using Microsoft.EntityFrameworkCore; namespace Marco.Pms.Services.Controllers @@ -37,7 +39,7 @@ namespace Marco.Pms.Services.Controllers [HttpGet("images/{projectId}")] - public async Task GetImageList(Guid projectId, [FromQuery] string? filter) + public async Task GetImageList(Guid projectId, [FromQuery] string? filter, [FromQuery] int? pageNumber = 1, [FromQuery] int? pageSize = 10) { _logger.LogInfo("[GetImageList] Called by Employee for ProjectId: {ProjectId}", projectId); @@ -66,8 +68,9 @@ namespace Marco.Pms.Services.Controllers try { var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; - //string unescapedJsonString = JsonSerializer.Deserialize(filter, options) ?? ""; - imageFilter = JsonSerializer.Deserialize(filter, options); + string unescapedJsonString = JsonSerializer.Deserialize(filter, options) ?? ""; + imageFilter = JsonSerializer.Deserialize(unescapedJsonString, options); + //imageFilter = JsonSerializer.Deserialize(filter, options); } catch (Exception ex) { @@ -86,12 +89,9 @@ namespace Marco.Pms.Services.Controllers var uploadedByIds = imageFilter?.UploadedByIds; // Step 5: Fetch building > floor > area > work item hierarchy - // Step 3: Fetch building > floor > work area > work item hierarchy List? buildings = null; List? floors = null; List? workAreas = null; - //List? workItems = null; - //List? documents = null; if (buildingIds != null && buildingIds.Count > 0) { @@ -164,18 +164,35 @@ namespace Marco.Pms.Services.Controllers var documentIds = attachments.Select(ta => ta.DocumentId).ToList(); // Step 7: Fetch and filter documents + List documents = new List(); var docQuery = _context.Documents.Include(d => d.UploadedBy) .Where(d => documentIds.Contains(d.Id) && d.TenantId == tenantId); if (startDate != null && endDate != null) { docQuery = docQuery.Where(d => d.UploadedAt.Date >= startDate.Value.Date && d.UploadedAt.Date <= endDate.Value.Date); } - var documents = await docQuery.ToListAsync(); + if (pageNumber != null && pageSize != null) + { + documents = await docQuery + .GroupBy(d => d.BatchId) + .OrderBy(g => g.Key) + .Skip((pageNumber.Value - 1) * pageSize.Value) + .Take(pageSize.Value) + .Select(g => new DocumentBatchDto + { + BatchId = g.Key, + Documents = g.ToList() + }) + .ToListAsync(); + Console.Write("Pagenation Success"); + } + // Step 8: Build response var documentVM = documents.Select(d => { - var refId = attachments.FirstOrDefault(ta => ta.DocumentId == d.Id)?.ReferenceId; + var docIds = d.Documents?.Select(x => x.Id).ToList() ?? new List(); + var refId = attachments.FirstOrDefault(ta => docIds.Contains(ta.DocumentId))?.ReferenceId; var task = tasks.FirstOrDefault(t => t.Id == refId); var comment = comments.FirstOrDefault(c => c.Id == refId); @@ -194,12 +211,16 @@ namespace Marco.Pms.Services.Controllers return new { - Id = d.Id, + BatchId = d.BatchId, - thumbnailUrl = d.ThumbS3Key != null ? _s3Service.GeneratePreSignedUrlAsync(d.ThumbS3Key) : (d.S3Key != null ? _s3Service.GeneratePreSignedUrlAsync(d.S3Key) : null), - ImageUrl = d.S3Key != null ? _s3Service.GeneratePreSignedUrlAsync(d.S3Key) : null, - UploadedBy = d.UploadedBy?.ToBasicEmployeeVMFromEmployee() ?? uploadedBy?.ToBasicEmployeeVMFromEmployee(), - UploadedAt = d.UploadedAt, + Documents = d.Documents?.Select(x => new + { + Id = x.Id, + thumbnailUrl = x.ThumbS3Key != null ? _s3Service.GeneratePreSignedUrlAsync(x.ThumbS3Key) : (x.S3Key != null ? _s3Service.GeneratePreSignedUrlAsync(x.S3Key) : null), + Url = x.S3Key != null ? _s3Service.GeneratePreSignedUrlAsync(x.S3Key) : null, + UploadedBy = x.UploadedBy?.ToBasicEmployeeVMFromEmployee() ?? uploadedBy?.ToBasicEmployeeVMFromEmployee(), + UploadedAt = x.UploadedAt, + }).ToList(), Source = source, ProjectId = projectId, BuildingId = building?.Id, @@ -220,7 +241,7 @@ namespace Marco.Pms.Services.Controllers if (uploadedByIds?.Any() == true) { - documentVM = documentVM.Where(d => uploadedByIds.Contains(d.UploadedBy?.Id ?? Guid.Empty)).ToList(); + documentVM = documentVM.Where(d => d.Documents != null && d.Documents.Any(x => uploadedByIds.Contains(x.UploadedBy?.Id ?? Guid.Empty))).ToList(); } _logger.LogInfo("[GetImageList] Fetched {Count} documents for ProjectId: {ProjectId}", documentVM.Count, projectId); @@ -302,16 +323,18 @@ namespace Marco.Pms.Services.Controllers .FirstOrDefaultAsync(b => b.Id == buildingId); // Step 6: Construct the response - var response = documents.Select(d => new + var response = new { - Id = d.Id, - BatchId = d.BatchId, - thumbnailUrl = d.ThumbS3Key != null - ? _s3Service.GeneratePreSignedUrlAsync(d.ThumbS3Key) - : (d.S3Key != null ? _s3Service.GeneratePreSignedUrlAsync(d.S3Key) : null), - ImageUrl = d.S3Key != null ? _s3Service.GeneratePreSignedUrlAsync(d.S3Key) : null, - UploadedBy = d.UploadedBy?.ToBasicEmployeeVMFromEmployee() ?? uploadedBy?.ToBasicEmployeeVMFromEmployee(), - UploadedAt = d.UploadedAt, + + BatchId = batchId, + Documents = documents?.Select(x => new + { + Id = x.Id, + thumbnailUrl = x.ThumbS3Key != null ? _s3Service.GeneratePreSignedUrlAsync(x.ThumbS3Key) : (x.S3Key != null ? _s3Service.GeneratePreSignedUrlAsync(x.S3Key) : null), + Url = x.S3Key != null ? _s3Service.GeneratePreSignedUrlAsync(x.S3Key) : null, + UploadedBy = x.UploadedBy?.ToBasicEmployeeVMFromEmployee() ?? uploadedBy?.ToBasicEmployeeVMFromEmployee(), + UploadedAt = x.UploadedAt, + }).ToList(), Source = source, ProjectId = building?.ProjectId, BuildingId = building?.Id, @@ -327,9 +350,9 @@ namespace Marco.Pms.Services.Controllers WorkCategoryName = workItem?.WorkCategoryMaster?.Name, CommentId = comment?.Id, Comment = comment?.Comment - }).ToList(); + }; - _logger.LogInfo("Fetched {Count} image(s) for BatchId: {BatchId}", response.Count, batchId); + _logger.LogInfo("Fetched {Count} image(s) for BatchId: {BatchId}", response.Documents?.Count ?? 0, batchId); return Ok(ApiResponse.SuccessResponse(response, "Images for provided batchId fetched successfully", 200)); } From 800db99fd9928941ffe521354c02d4b7b3743ca1 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Sat, 5 Jul 2025 12:33:10 +0530 Subject: [PATCH 017/124] Removed double Deserialization --- Marco.Pms.Services/Controllers/ImageController.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Marco.Pms.Services/Controllers/ImageController.cs b/Marco.Pms.Services/Controllers/ImageController.cs index 5c3cdc5..6c8698a 100644 --- a/Marco.Pms.Services/Controllers/ImageController.cs +++ b/Marco.Pms.Services/Controllers/ImageController.cs @@ -68,9 +68,9 @@ namespace Marco.Pms.Services.Controllers try { var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; - string unescapedJsonString = JsonSerializer.Deserialize(filter, options) ?? ""; - imageFilter = JsonSerializer.Deserialize(unescapedJsonString, options); - //imageFilter = JsonSerializer.Deserialize(filter, options); + //string unescapedJsonString = JsonSerializer.Deserialize(filter, options) ?? ""; + //imageFilter = JsonSerializer.Deserialize(unescapedJsonString, options); + imageFilter = JsonSerializer.Deserialize(filter, options); } catch (Exception ex) { From f9ab7bb3c87131c3516262c09ab4419bcd9218f3 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Sat, 5 Jul 2025 13:19:06 +0530 Subject: [PATCH 018/124] Ordered by Uploaded at --- Marco.Pms.Services/Controllers/ImageController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Marco.Pms.Services/Controllers/ImageController.cs b/Marco.Pms.Services/Controllers/ImageController.cs index 6c8698a..7c54e9c 100644 --- a/Marco.Pms.Services/Controllers/ImageController.cs +++ b/Marco.Pms.Services/Controllers/ImageController.cs @@ -175,7 +175,7 @@ namespace Marco.Pms.Services.Controllers { documents = await docQuery .GroupBy(d => d.BatchId) - .OrderBy(g => g.Key) + .OrderByDescending(g => g.Max(d => d.UploadedAt)) .Skip((pageNumber.Value - 1) * pageSize.Value) .Take(pageSize.Value) .Select(g => new DocumentBatchDto From 00526922831dea6d032b4d542fd81214c7352691 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Sat, 5 Jul 2025 15:25:01 +0530 Subject: [PATCH 019/124] Added error handling in cache helper --- .../Helpers/CacheUpdateHelper.cs | 170 +++++++++++++++--- 1 file changed, 143 insertions(+), 27 deletions(-) diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 1c3ee70..75b51b5 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -1,6 +1,7 @@ using Marco.Pms.CacheHelper; using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.Projects; +using MarcoBMS.Services.Service; using Project = Marco.Pms.Model.Projects.Project; namespace Marco.Pms.Services.Helpers @@ -9,90 +10,205 @@ namespace Marco.Pms.Services.Helpers { private readonly ProjectCache _projectCache; private readonly EmployeeCache _employeeCache; + private readonly ILoggingService _logger; - public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache) + public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache, ILoggingService logger) { _projectCache = projectCache; _employeeCache = employeeCache; + _logger = logger; } // ------------------------------------ Project Details and Infrastructure Cache --------------------------------------- public async Task AddProjectDetails(Project project) { - await _projectCache.AddProjectDetailsToCache(project); + try + { + await _projectCache.AddProjectDetailsToCache(project); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while adding project to Cache: {Error}", ex.Message); + } } public async Task UpdateProjectDetailsOnly(Project project) { - bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project); - return response; + try + { + bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while updating project to Cache: {Error}", ex.Message); + return false; + } } public async Task GetProjectDetails(Guid projectId) { - var response = await _projectCache.GetProjectDetailsFromCache(projectId); - return response; + try + { + var response = await _projectCache.GetProjectDetailsFromCache(projectId); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while getting project to Cache: {Error}", ex.Message); + return null; + } } + //public async Task?> GetProjectDetailsList(List projectIds) + //{ + // var response = await _projectCache.GetProjectDetailsListFromCache(projectIds); + // return response; + //} public async Task AddBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) { - await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + try + { + await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while adding project infra to Cache: {Error}", ex.Message); + } } public async Task UpdateBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) { - var response = await _projectCache.UpdateBuildngInfraToCache(projectId, building, floor, workArea, buildingId); - if (!response) + try { - await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + var response = await _projectCache.UpdateBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + if (!response) + { + await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + } + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while updating project infra to Cache: {Error}", ex.Message); } } public async Task?> GetBuildingInfra(Guid projectId) { - var response = await _projectCache.GetBuildingInfraFromCache(projectId); - return response; + try + { + var response = await _projectCache.GetBuildingInfraFromCache(projectId); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while getting project infra Cache: {Error}", ex.Message); + return null; + } } // ------------------------------------ Employee Profile Cache --------------------------------------- public async Task AddApplicationRole(Guid employeeId, List roleIds) { - var response = await _employeeCache.AddApplicationRoleToCache(employeeId, roleIds); + try + { + var response = await _employeeCache.AddApplicationRoleToCache(employeeId, roleIds); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while adding Application roleIds to Cache to employee {Employee}: {Error}", employeeId, ex.Message); + } } public async Task AddProjects(Guid employeeId, List projectIds) { - var response = await _employeeCache.AddProjectsToCache(employeeId, projectIds); - return response; + try + { + var response = await _employeeCache.AddProjectsToCache(employeeId, projectIds); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while adding projectIds to Cache: {Error}", ex.Message); + return false; + } } public async Task?> GetProjects(Guid employeeId) { - var response = await _employeeCache.GetProjectsFromCache(employeeId); - if (response.Count > 0) + try { - return response; + var response = await _employeeCache.GetProjectsFromCache(employeeId); + if (response.Count > 0) + { + return response; + } + return null; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while getting projectIDs to Cache: {Error}", ex.Message); + return null; } - return null; } public async Task?> GetPermissions(Guid employeeId) { - var response = await _employeeCache.GetPermissionsFromCache(employeeId); - if (response.Count > 0) + try { - return response; + var response = await _employeeCache.GetPermissionsFromCache(employeeId); + if (response.Count > 0) + { + return response; + } + return null; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while getting permissionIds to Cache: {Error}", ex.Message); + return null; } - return null; } public async Task ClearAllProjectIds(Guid employeeId) { - var response = await _employeeCache.ClearAllProjectIdsFromCache(employeeId); + try + { + var response = await _employeeCache.ClearAllProjectIdsFromCache(employeeId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting projectIds from Cache for employee {EmployeeId}: {Error}", employeeId, ex.Message); + } } + //public async Task ClearAllProjectIdsByRoleId(Guid roleId) + //{ + // await _employeeCache.ClearAllProjectIdsByRoleIdFromCache(roleId); + //} public async Task ClearAllPermissionIdsByEmployeeID(Guid employeeId) { - var response = await _employeeCache.ClearAllPermissionIdsByEmployeeIDFromCache(employeeId); + try + { + var response = await _employeeCache.ClearAllPermissionIdsByEmployeeIDFromCache(employeeId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting permissionIds from to Cache: {Error}", ex.Message); + } } public async Task ClearAllPermissionIdsByRoleId(Guid roleId) { - var response = await _employeeCache.ClearAllPermissionIdsByRoleIdFromCache(roleId); + try + { + var response = await _employeeCache.ClearAllPermissionIdsByRoleIdFromCache(roleId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting permissionIds from to Cache: {Error}", ex.Message); + } } public async Task RemoveRoleId(Guid employeeId, Guid roleId) { - var response = await _employeeCache.RemoveRoleIdFromCache(employeeId, roleId); + try + { + var response = await _employeeCache.RemoveRoleIdFromCache(employeeId, roleId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting Application roleIds from to Cache: {Error}", ex.Message); + } } } } From 6373da3144b78b9472971d8235509b3c92fb1c8e Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Sat, 5 Jul 2025 17:11:59 +0530 Subject: [PATCH 020/124] Added SignalR Integration for Reporting, Commenting, and Approving Tasks --- .../Controllers/TaskController.cs | 61 ++++++++++++------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/Marco.Pms.Services/Controllers/TaskController.cs b/Marco.Pms.Services/Controllers/TaskController.cs index 5a35baf..c0ec5ff 100644 --- a/Marco.Pms.Services/Controllers/TaskController.cs +++ b/Marco.Pms.Services/Controllers/TaskController.cs @@ -6,11 +6,13 @@ using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Activities; +using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Service; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; using Microsoft.CodeAnalysis; using Microsoft.EntityFrameworkCore; using Document = Marco.Pms.Model.DocumentManager.Document; @@ -27,16 +29,19 @@ namespace MarcoBMS.Services.Controllers private readonly UserHelper _userHelper; private readonly S3UploadService _s3Service; private readonly ILoggingService _logger; + private readonly IHubContext _signalR; private readonly PermissionServices _permissionServices; private readonly Guid Approve_Task; private readonly Guid Assign_Report_Task; - public TaskController(ApplicationDbContext context, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permissionServices) + public TaskController(ApplicationDbContext context, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permissionServices, + IHubContext signalR) { _context = context; _userHelper = userHelper; _s3Service = s3Service; _logger = logger; + _signalR = signalR; _permissionServices = permissionServices; Approve_Task = Guid.Parse("db4e40c5-2ba9-4b6d-b8a6-a16a250ff99c"); Assign_Report_Task = Guid.Parse("6a32379b-8b3f-49a6-8c48-4b7ac1b55dc2"); @@ -194,17 +199,20 @@ namespace MarcoBMS.Services.Controllers var comment = reportTask.ToCommentFromReportTaskDto(tenantId, loggedInEmployee.Id); _context.TaskComments.Add(comment); + var workAreaId = taskAllocation.WorkItem?.WorkAreaId; + var workArea = await _context.WorkAreas.Include(a => a.Floor) + .FirstOrDefaultAsync(a => a.Id == workAreaId) ?? new WorkArea(); + + var buildingId = workArea.Floor?.BuildingId; + + var building = await _context.Buildings + .FirstOrDefaultAsync(b => b.Id == buildingId); + var batchId = Guid.NewGuid(); + var projectId = building?.ProjectId; + if (reportTask.Images?.Any() == true) { - var workAreaId = taskAllocation.WorkItem?.WorkAreaId; - var workArea = await _context.WorkAreas.Include(a => a.Floor) - .FirstOrDefaultAsync(a => a.Id == workAreaId) ?? new WorkArea(); - var buildingId = workArea.Floor?.BuildingId; - - var building = await _context.Buildings - .FirstOrDefaultAsync(b => b.Id == buildingId); - var batchId = Guid.NewGuid(); foreach (var image in reportTask.Images) { @@ -220,7 +228,7 @@ namespace MarcoBMS.Services.Controllers var fileType = _s3Service.GetContentTypeFromBase64(base64); var fileName = _s3Service.GenerateFileName(fileType, tenantId, "task_report"); - var objectKey = $"tenant-{tenantId}/project-{building?.ProjectId}/Actitvity/{fileName}"; + var objectKey = $"tenant-{tenantId}/project-{projectId}/Actitvity/{fileName}"; await _s3Service.UploadFileAsync(base64, fileType, objectKey); @@ -257,6 +265,9 @@ namespace MarcoBMS.Services.Controllers response.Comments = comments.Select(c => c.ToCommentVMFromTaskComment()).ToList(); response.checkList = checkListVMs; + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Task_Report", ProjectId = projectId }; + await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + _logger.LogInfo("Task {TaskId} reported successfully by Employee {EmployeeId}", taskAllocation.Id, loggedInEmployee.Id); return Ok(ApiResponse.SuccessResponse(response, "Task reported successfully", 200)); @@ -288,6 +299,7 @@ namespace MarcoBMS.Services.Controllers var buildingId = workArea.Floor?.BuildingId ?? Guid.Empty; var building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == buildingId); + var projectId = building?.ProjectId; // Save comment var comment = createComment.ToCommentFromCommentDto(tenantId, loggedInEmployee.Id); @@ -316,7 +328,7 @@ namespace MarcoBMS.Services.Controllers var fileType = _s3Service.GetContentTypeFromBase64(base64); var fileName = _s3Service.GenerateFileName(fileType, tenantId, "task_comment"); - var objectKey = $"tenant-{tenantId}/project-{building?.ProjectId}/Activity/{fileName}"; + var objectKey = $"tenant-{tenantId}/project-{projectId}/Activity/{fileName}"; await _s3Service.UploadFileAsync(base64, fileType, objectKey); _logger.LogInfo("Image uploaded to S3 with key: {ObjectKey}", objectKey); @@ -353,6 +365,9 @@ namespace MarcoBMS.Services.Controllers var response = comment.ToCommentVMFromTaskComment(); _logger.LogInfo("Returning response for commentId: {CommentId}", comment.Id); + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Task_Report", ProjectId = projectId }; + await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + return Ok(ApiResponse.SuccessResponse(response, "Comment saved successfully", 200)); } @@ -725,18 +740,19 @@ namespace MarcoBMS.Services.Controllers }; _context.TaskComments.Add(comment); + var workAreaId = taskAllocation.WorkItem?.WorkAreaId; + var workArea = await _context.WorkAreas.Include(a => a.Floor) + .FirstOrDefaultAsync(a => a.Id == workAreaId) ?? new WorkArea(); + + var buildingId = workArea.Floor?.BuildingId; + + var building = await _context.Buildings + .FirstOrDefaultAsync(b => b.Id == buildingId); + var projectId = building?.ProjectId; + // Handle image attachments, if any if (approveTask.Images?.Count > 0) { - var workAreaId = taskAllocation.WorkItem?.WorkAreaId; - var workArea = await _context.WorkAreas.Include(a => a.Floor) - .FirstOrDefaultAsync(a => a.Id == workAreaId) ?? new WorkArea(); - - var buildingId = workArea.Floor?.BuildingId; - - var building = await _context.Buildings - .FirstOrDefaultAsync(b => b.Id == buildingId); - var batchId = Guid.NewGuid(); foreach (var image in approveTask.Images) @@ -751,7 +767,7 @@ namespace MarcoBMS.Services.Controllers var fileType = _s3Service.GetContentTypeFromBase64(base64); var fileName = _s3Service.GenerateFileName(fileType, tenantId, "task_comment"); - var objectKey = $"tenant-{tenantId}/project-{building?.ProjectId}/Activity/{fileName}"; + var objectKey = $"tenant-{tenantId}/project-{projectId}/Activity/{fileName}"; await _s3Service.UploadFileAsync(base64, fileType, objectKey); @@ -785,6 +801,9 @@ namespace MarcoBMS.Services.Controllers // Commit all changes to the database await _context.SaveChangesAsync(); + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Task_Report", ProjectId = projectId }; + await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + _logger.LogInfo("Task {TaskId} successfully approved by Employee {EmployeeId}", approveTask.Id, loggedInEmployee.Id); return Ok(ApiResponse.SuccessResponse("Task has been approved", "Task has been approved", 200)); From 558fd6bd5b0d4e1cb83ba8d598bdcda11090626d Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Sat, 5 Jul 2025 17:24:57 +0530 Subject: [PATCH 021/124] Corrected Key for signalR --- Marco.Pms.Services/Controllers/TaskController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Marco.Pms.Services/Controllers/TaskController.cs b/Marco.Pms.Services/Controllers/TaskController.cs index c0ec5ff..f4f2dfa 100644 --- a/Marco.Pms.Services/Controllers/TaskController.cs +++ b/Marco.Pms.Services/Controllers/TaskController.cs @@ -365,7 +365,7 @@ namespace MarcoBMS.Services.Controllers var response = comment.ToCommentVMFromTaskComment(); _logger.LogInfo("Returning response for commentId: {CommentId}", comment.Id); - var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Task_Report", ProjectId = projectId }; + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Task_Comment", ProjectId = projectId }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); return Ok(ApiResponse.SuccessResponse(response, "Comment saved successfully", 200)); @@ -801,7 +801,7 @@ namespace MarcoBMS.Services.Controllers // Commit all changes to the database await _context.SaveChangesAsync(); - var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Task_Report", ProjectId = projectId }; + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Task_Comment", ProjectId = projectId }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); _logger.LogInfo("Task {TaskId} successfully approved by Employee {EmployeeId}", approveTask.Id, loggedInEmployee.Id); From b8cba9f378de1610de912183552dcd75922f200d Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 7 Jul 2025 10:04:11 +0530 Subject: [PATCH 022/124] Implemented the methods for deleting permission am asigned project from caches for certien employee --- Marco.Pms.CacheHelper/EmployeeCache.cs | 60 ++++++++++++++----- Marco.Pms.CacheHelper/ProjectCache.cs | 55 ++++++++++------- .../EmployeePermissionMongoDB.cs | 2 +- .../MongoDBModels/ProjectMongoDB.cs | 2 +- .../MongoDBModels/WorkItemMongoDB.cs | 9 +-- .../Controllers/RolesController.cs | 4 ++ .../Helpers/CacheUpdateHelper.cs | 57 +++++++++++------- Marco.Pms.Services/Helpers/ProjectsHelper.cs | 21 ++++++- 8 files changed, 144 insertions(+), 66 deletions(-) diff --git a/Marco.Pms.CacheHelper/EmployeeCache.cs b/Marco.Pms.CacheHelper/EmployeeCache.cs index 7d75407..5c86e6f 100644 --- a/Marco.Pms.CacheHelper/EmployeeCache.cs +++ b/Marco.Pms.CacheHelper/EmployeeCache.cs @@ -22,31 +22,47 @@ namespace Marco.Pms.CacheHelper } public async Task AddApplicationRoleToCache(Guid employeeId, List roleIds) { - var newRoleIds = roleIds.Select(r => r.ToString()).ToList(); - var newPermissionIds = await _context.RolePermissionMappings + // 1. Guard Clause: Avoid unnecessary database work if there are no roles to add. + if (roleIds == null || !roleIds.Any()) + { + return false; // Nothing to add, so the operation did not result in a change. + } + + // 2. Perform database queries concurrently for better performance. + var employeeIdString = employeeId.ToString(); + + Task> getPermissionIdsTask = _context.RolePermissionMappings .Where(rp => roleIds.Contains(rp.ApplicationRoleId)) .Select(p => p.FeaturePermissionId.ToString()) .Distinct() .ToListAsync(); - var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + // 3. Prepare role IDs in parallel with the database query. + var newRoleIds = roleIds.Select(r => r.ToString()).ToList(); + + // 4. Await the database query result. + var newPermissionIds = await getPermissionIdsTask; + + // 5. Build a single, efficient update operation. + var filter = Builders.Filter.Eq(e => e.Id, employeeIdString); var update = Builders.Update .AddToSetEach(e => e.ApplicationRoleIds, newRoleIds) .AddToSetEach(e => e.PermissionIds, newPermissionIds); - var result = await _collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); - if (result.MatchedCount == 0) - { - return false; - } - return true; + var options = new UpdateOptions { IsUpsert = true }; + + var result = await _collection.UpdateOneAsync(filter, update, options); + + // 6. Return a more accurate result indicating success for both updates and upserts. + // The operation is successful if an existing document was modified OR a new one was created. + return result.IsAcknowledged && (result.ModifiedCount > 0 || result.UpsertedId != null); } public async Task AddProjectsToCache(Guid employeeId, List projectIds) { var newprojectIds = projectIds.Select(p => p.ToString()).ToList(); - var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + var filter = Builders.Filter.Eq(e => e.Id, employeeId.ToString()); var update = Builders.Update .AddToSetEach(e => e.ProjectIds, newprojectIds); @@ -60,7 +76,7 @@ namespace Marco.Pms.CacheHelper } public async Task> GetProjectsFromCache(Guid employeeId) { - var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + var filter = Builders.Filter.Eq(e => e.Id, employeeId.ToString()); var result = await _collection @@ -77,7 +93,7 @@ namespace Marco.Pms.CacheHelper } public async Task> GetPermissionsFromCache(Guid employeeId) { - var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + var filter = Builders.Filter.Eq(e => e.Id, employeeId.ToString()); var result = await _collection @@ -95,7 +111,21 @@ namespace Marco.Pms.CacheHelper public async Task ClearAllProjectIdsFromCache(Guid employeeId) { var filter = Builders.Filter - .Eq(e => e.EmployeeId, employeeId.ToString()); + .Eq(e => e.Id, employeeId.ToString()); + + var update = Builders.Update + .Set(e => e.ProjectIds, new List()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + return true; + } + public async Task ClearAllProjectIdsByRoleIdFromCache(Guid roleId) + { + var filter = Builders.Filter.AnyEq(e => e.ApplicationRoleIds, roleId.ToString()); var update = Builders.Update .Set(e => e.ProjectIds, new List()); @@ -110,7 +140,7 @@ namespace Marco.Pms.CacheHelper public async Task RemoveRoleIdFromCache(Guid employeeId, Guid roleId) { var filter = Builders.Filter - .Eq(e => e.EmployeeId, employeeId.ToString()); + .Eq(e => e.Id, employeeId.ToString()); var update = Builders.Update .Pull(e => e.ApplicationRoleIds, roleId.ToString()); @@ -128,7 +158,7 @@ namespace Marco.Pms.CacheHelper public async Task ClearAllPermissionIdsByEmployeeIDFromCache(Guid employeeId) { var filter = Builders.Filter - .Eq(e => e.EmployeeId, employeeId.ToString()); + .Eq(e => e.Id, employeeId.ToString()); var update = Builders.Update .Set(e => e.PermissionIds, new List()); diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index b667694..f60884f 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -11,19 +11,21 @@ namespace Marco.Pms.CacheHelper public class ProjectCache { private readonly ApplicationDbContext _context; - private readonly IMongoDatabase _mongoDB; - //private readonly ILoggingService _logger; + private readonly IMongoCollection _projetCollection; + private readonly IMongoCollection _taskCollection; public ProjectCache(ApplicationDbContext context, IConfiguration configuration) { var connectionString = configuration["MongoDB:ConnectionString"]; _context = context; var mongoUrl = new MongoUrl(connectionString); var client = new MongoClient(mongoUrl); // Your MongoDB connection string - _mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name + var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name + _projetCollection = mongoDB.GetCollection("ProjectDetails"); + _taskCollection = mongoDB.GetCollection("WorkItemDetails"); } public async Task AddProjectDetailsToCache(Project project) { - var projectCollection = _mongoDB.GetCollection("ProjectDetails"); + //_logger.LogInfo("[AddProjectDetails] Initiated for ProjectId: {ProjectId}", project.Id); @@ -145,7 +147,7 @@ namespace Marco.Pms.CacheHelper projectDetails.PlannedWork = totalPlannedWork; projectDetails.CompletedWork = totalCompletedWork; - await projectCollection.InsertOneAsync(projectDetails); + await _projetCollection.InsertOneAsync(projectDetails); //_logger.LogInfo("[AddProjectDetails] Project details inserted in MongoDB for ProjectId: {ProjectId}", project.Id); } public async Task UpdateProjectDetailsOnlyToCache(Project project) @@ -160,8 +162,6 @@ namespace Marco.Pms.CacheHelper //_logger.LogWarning("StatusMaster not found for ProjectStatusId: {StatusId}", project.ProjectStatusId); } - var projectCollection = _mongoDB.GetCollection("ProjectDetails"); - // Build the update definition var updates = Builders.Update.Combine( Builders.Update.Set(r => r.Name, project.Name), @@ -178,7 +178,7 @@ namespace Marco.Pms.CacheHelper ); // Perform the update - var result = await projectCollection.UpdateOneAsync( + var result = await _projetCollection.UpdateOneAsync( filter: r => r.Id == project.Id.ToString(), update: updates ); @@ -194,7 +194,6 @@ namespace Marco.Pms.CacheHelper } public async Task GetProjectDetailsFromCache(Guid projectId) { - var projectCollection = _mongoDB.GetCollection("ProjectDetails"); // Build filter and projection to exclude large 'Buildings' list var filter = Builders.Filter.Eq(p => p.Id, projectId.ToString()); @@ -203,7 +202,7 @@ namespace Marco.Pms.CacheHelper //_logger.LogInfo("Fetching project details for ProjectId: {ProjectId} from MongoDB", projectId); // Perform query - var project = await projectCollection + var project = await _projetCollection .Find(filter) .Project(projection) .FirstOrDefaultAsync(); @@ -214,16 +213,23 @@ namespace Marco.Pms.CacheHelper return null; } - //// Deserialize the result manually - //var project = BsonSerializer.Deserialize(result); - //_logger.LogInfo("Successfully fetched project details (excluding Buildings) for ProjectId: {ProjectId}", projectId); return project; } + public async Task?> GetProjectDetailsListFromCache(List projectIds) + { + List stringProjectIds = projectIds.Select(p => p.ToString()).ToList(); + var filter = Builders.Filter.In(p => p.Id, stringProjectIds); + var projection = Builders.Projection.Exclude(p => p.Buildings); + var projects = await _projetCollection + .Find(filter) + .Project(projection) + .ToListAsync(); + return projects; + } public async Task AddBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId) { var stringProjectId = projectId.ToString(); - var projectCollection = _mongoDB.GetCollection("ProjectDetails"); // Add Building if (building != null) @@ -241,7 +247,7 @@ namespace Marco.Pms.CacheHelper var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); var update = Builders.Update.Push("Buildings", buildingMongo); - var result = await projectCollection.UpdateOneAsync(filter, update); + var result = await _projetCollection.UpdateOneAsync(filter, update); if (result.MatchedCount == 0) { @@ -271,7 +277,7 @@ namespace Marco.Pms.CacheHelper ); var update = Builders.Update.Push("Buildings.$.Floors", floorMongo); - var result = await projectCollection.UpdateOneAsync(filter, update); + var result = await _projetCollection.UpdateOneAsync(filter, update); if (result.MatchedCount == 0) { @@ -305,7 +311,7 @@ namespace Marco.Pms.CacheHelper var update = Builders.Update.Push("Buildings.$[b].Floors.$[f].WorkAreas", workAreaMongo); var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; - var result = await projectCollection.UpdateOneAsync(filter, update, updateOptions); + var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); if (result.MatchedCount == 0) { @@ -323,7 +329,6 @@ namespace Marco.Pms.CacheHelper public async Task UpdateBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId) { var stringProjectId = projectId.ToString(); - var projectCollection = _mongoDB.GetCollection("ProjectDetails"); // Update Building if (building != null) @@ -338,7 +343,7 @@ namespace Marco.Pms.CacheHelper Builders.Update.Set("Buildings.$.Description", building.Description) ); - var result = await projectCollection.UpdateOneAsync(filter, update); + var result = await _projetCollection.UpdateOneAsync(filter, update); if (result.MatchedCount == 0) { @@ -363,7 +368,7 @@ namespace Marco.Pms.CacheHelper var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); - var result = await projectCollection.UpdateOneAsync(filter, update, updateOptions); + var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); if (result.MatchedCount == 0) { @@ -389,7 +394,7 @@ namespace Marco.Pms.CacheHelper var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); - var result = await projectCollection.UpdateOneAsync(filter, update, updateOptions); + var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); if (result.MatchedCount == 0) { @@ -408,13 +413,12 @@ namespace Marco.Pms.CacheHelper } public async Task?> GetBuildingInfraFromCache(Guid projectId) { - var projectCollection = _mongoDB.GetCollection("ProjectDetails"); // Filter by project ID var filter = Builders.Filter.Eq(p => p.Id, projectId.ToString()); // Project only the "Buildings" field from the document - var buildings = await projectCollection + var buildings = await _projetCollection .Find(filter) .Project(p => p.Buildings) .FirstOrDefaultAsync(); @@ -430,5 +434,10 @@ namespace Marco.Pms.CacheHelper return buildings; } + + + // ------------------------------------------------------- WorkItem ------------------------------------------------------- + + } } diff --git a/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs b/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs index f141798..49c514e 100644 --- a/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs @@ -5,7 +5,7 @@ namespace Marco.Pms.Model.MongoDBModels [BsonIgnoreExtraElements] public class EmployeePermissionMongoDB { - public string EmployeeId { get; set; } = string.Empty; + public string Id { get; set; } = string.Empty; // Employee ID public List ApplicationRoleIds { get; set; } = new List(); public List PermissionIds { get; set; } = new List(); public List ProjectIds { get; set; } = new List(); diff --git a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs index 8bf1c9a..8b1612c 100644 --- a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs @@ -2,7 +2,7 @@ { public class ProjectMongoDB { - public string? Id { get; set; } + public string Id { get; set; } = string.Empty; public string? Name { get; set; } public string? ShortName { get; set; } public string? ProjectAddress { get; set; } diff --git a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs index dc7fdb9..71638a3 100644 --- a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs @@ -2,13 +2,14 @@ { public class WorkItemMongoDB { - public string? Id { get; set; } - public string? WorkAreaId { get; set; } + public string Id { get; set; } = string.Empty; + public string WorkAreaId { get; set; } = string.Empty; public ActivityMasterMongoDB? ActivityMaster { get; set; } public WorkCategoryMasterMongoDB? WorkCategoryMaster { get; set; } public string? ParentTaskId { get; set; } - public double PlannedWork { get; set; } - public double CompletedWork { get; set; } + public double PlannedWork { get; set; } = 0; + public double TodaysAssigned { get; set; } = 0; + public double CompletedWork { get; set; } = 0; public string? Description { get; set; } public DateTime TaskDate { get; set; } } diff --git a/Marco.Pms.Services/Controllers/RolesController.cs b/Marco.Pms.Services/Controllers/RolesController.cs index 4c75b3e..a67ecaf 100644 --- a/Marco.Pms.Services/Controllers/RolesController.cs +++ b/Marco.Pms.Services/Controllers/RolesController.cs @@ -292,6 +292,10 @@ namespace MarcoBMS.Services.Controllers _context.RolePermissionMappings.Add(item); modified = true; } + if (item.FeaturePermissionId == Guid.Parse("172fc9b6-755b-4f62-ab26-55c34a330614")) + { + await _cache.ClearAllProjectIdsByRoleId(id); + } } if (modified) await _context.SaveChangesAsync(); diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 75b51b5..6ff9cfe 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -28,7 +28,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while adding project to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while adding project {ProjectId} to Cache : {Error}", project.Id, ex.Message); } } public async Task UpdateProjectDetailsOnly(Project project) @@ -40,7 +40,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while updating project to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while updating project {ProjectId} to Cache: {Error}", project.Id, ex.Message); return false; } } @@ -53,15 +53,23 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while getting project to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while getting project {ProjectId} to Cache: {Error}", ex.Message); + return null; + } + } + public async Task?> GetProjectDetailsList(List projectIds) + { + try + { + var response = await _projectCache.GetProjectDetailsListFromCache(projectIds); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while getting list od project details from to Cache: {Error}", ex.Message); return null; } } - //public async Task?> GetProjectDetailsList(List projectIds) - //{ - // var response = await _projectCache.GetProjectDetailsListFromCache(projectIds); - // return response; - //} public async Task AddBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) { try @@ -70,7 +78,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while adding project infra to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while adding project infra for project {ProjectId} to Cache: {Error}", projectId, ex.Message); } } public async Task UpdateBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) @@ -85,7 +93,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while updating project infra to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while updating project infra for project {ProjectId} to Cache: {Error}", projectId, ex.Message); } } public async Task?> GetBuildingInfra(Guid projectId) @@ -97,7 +105,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while getting project infra Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while getting project infra for project {ProjectId} form Cache: {Error}", projectId, ex.Message); return null; } } @@ -124,7 +132,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while adding projectIds to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while adding projectIds for employee {EmployeeId} to Cache: {Error}", employeeId, ex.Message); return false; } } @@ -141,7 +149,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while getting projectIDs to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while getting projectIds for employee {EmployeeId} from Cache: {Error}", employeeId, ex.Message); return null; } } @@ -158,7 +166,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while getting permissionIds to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while getting permissionIds for employee {EmployeeId} from Cache: {Error}", employeeId, ex.Message); return null; } } @@ -173,10 +181,17 @@ namespace Marco.Pms.Services.Helpers _logger.LogWarning("Error occured while deleting projectIds from Cache for employee {EmployeeId}: {Error}", employeeId, ex.Message); } } - //public async Task ClearAllProjectIdsByRoleId(Guid roleId) - //{ - // await _employeeCache.ClearAllProjectIdsByRoleIdFromCache(roleId); - //} + public async Task ClearAllProjectIdsByRoleId(Guid roleId) + { + try + { + await _employeeCache.ClearAllProjectIdsByRoleIdFromCache(roleId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting projectIds from Cache for Application Role {RoleId}: {Error}", roleId, ex.Message); + } + } public async Task ClearAllPermissionIdsByEmployeeID(Guid employeeId) { try @@ -185,7 +200,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while deleting permissionIds from to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while deleting permissionIds from Cache for employee {EmployeeId}: {Error}", employeeId, ex.Message); } } public async Task ClearAllPermissionIdsByRoleId(Guid roleId) @@ -196,7 +211,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while deleting permissionIds from to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while deleting permissionIds from Cache for Application role {RoleId}: {Error}", roleId, ex.Message); } } public async Task RemoveRoleId(Guid employeeId, Guid roleId) @@ -207,7 +222,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while deleting Application roleIds from to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while deleting Application role {RoleId} from Cache for employee {EmployeeId}: {Error}", roleId, employeeId, ex.Message); } } } diff --git a/Marco.Pms.Services/Helpers/ProjectsHelper.cs b/Marco.Pms.Services/Helpers/ProjectsHelper.cs index 3ccddba..85003ae 100644 --- a/Marco.Pms.Services/Helpers/ProjectsHelper.cs +++ b/Marco.Pms.Services/Helpers/ProjectsHelper.cs @@ -1,6 +1,7 @@ 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 Microsoft.EntityFrameworkCore; @@ -59,7 +60,25 @@ namespace MarcoBMS.Services.Helpers if (projectIds != null) { - projects = await _context.Projects.Where(p => projectIds.Contains(p.Id)).ToListAsync(); + + List projectdetails = await _cache.GetProjectDetailsList(projectIds) ?? new List(); + projects = projectdetails.Select(p => new Project + { + 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 { From 65f337652323421bcd06742e311b0f33c74beeb6 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 7 Jul 2025 11:21:31 +0530 Subject: [PATCH 023/124] Added new paremeter of NumberOfImages in signalR message object --- Marco.Pms.Services/Controllers/TaskController.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Marco.Pms.Services/Controllers/TaskController.cs b/Marco.Pms.Services/Controllers/TaskController.cs index f4f2dfa..4ad1f85 100644 --- a/Marco.Pms.Services/Controllers/TaskController.cs +++ b/Marco.Pms.Services/Controllers/TaskController.cs @@ -199,6 +199,8 @@ namespace MarcoBMS.Services.Controllers var comment = reportTask.ToCommentFromReportTaskDto(tenantId, loggedInEmployee.Id); _context.TaskComments.Add(comment); + int numberofImages = 0; + var workAreaId = taskAllocation.WorkItem?.WorkAreaId; var workArea = await _context.WorkAreas.Include(a => a.Floor) .FirstOrDefaultAsync(a => a.Id == workAreaId) ?? new WorkArea(); @@ -252,6 +254,7 @@ namespace MarcoBMS.Services.Controllers ReferenceId = reportTask.Id }; _context.TaskAttachments.Add(attachment); + numberofImages += 1; } } @@ -265,7 +268,7 @@ namespace MarcoBMS.Services.Controllers response.Comments = comments.Select(c => c.ToCommentVMFromTaskComment()).ToList(); response.checkList = checkListVMs; - var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Task_Report", ProjectId = projectId }; + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Task_Report", NumberOfImages = numberofImages, ProjectId = projectId }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); _logger.LogInfo("Task {TaskId} reported successfully by Employee {EmployeeId}", taskAllocation.Id, loggedInEmployee.Id); @@ -310,6 +313,7 @@ namespace MarcoBMS.Services.Controllers // Process image uploads var images = createComment.Images; var batchId = Guid.NewGuid(); + int numberofImages = 0; if (images != null && images.Any()) { @@ -355,6 +359,7 @@ namespace MarcoBMS.Services.Controllers }; _context.TaskAttachments.Add(attachment); + numberofImages += 1; } await _context.SaveChangesAsync(); @@ -365,7 +370,7 @@ namespace MarcoBMS.Services.Controllers var response = comment.ToCommentVMFromTaskComment(); _logger.LogInfo("Returning response for commentId: {CommentId}", comment.Id); - var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Task_Comment", ProjectId = projectId }; + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Task_Comment", NumberOfImages = numberofImages, ProjectId = projectId }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); return Ok(ApiResponse.SuccessResponse(response, "Comment saved successfully", 200)); @@ -749,6 +754,7 @@ namespace MarcoBMS.Services.Controllers var building = await _context.Buildings .FirstOrDefaultAsync(b => b.Id == buildingId); var projectId = building?.ProjectId; + int numberofImages = 0; // Handle image attachments, if any if (approveTask.Images?.Count > 0) @@ -795,13 +801,14 @@ namespace MarcoBMS.Services.Controllers _context.TaskAttachments.Add(attachment); _logger.LogInfo("Attachment uploaded for Task {TaskId}: {FileName}", approveTask.Id, fileName); + numberofImages += 1; } } // Commit all changes to the database await _context.SaveChangesAsync(); - var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Task_Comment", ProjectId = projectId }; + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Task_Report", NumberOfImages = numberofImages, ProjectId = projectId }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); _logger.LogInfo("Task {TaskId} successfully approved by Employee {EmployeeId}", approveTask.Id, loggedInEmployee.Id); From 43e2aeb097a55f9123831169edff5d455de9876c Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 7 Jul 2025 13:15:27 +0530 Subject: [PATCH 024/124] Showing the comment added when task is reported --- Marco.Pms.Services/Controllers/ImageController.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Marco.Pms.Services/Controllers/ImageController.cs b/Marco.Pms.Services/Controllers/ImageController.cs index 7c54e9c..48fbc3b 100644 --- a/Marco.Pms.Services/Controllers/ImageController.cs +++ b/Marco.Pms.Services/Controllers/ImageController.cs @@ -203,6 +203,10 @@ namespace Marco.Pms.Services.Controllers { task = tasks.FirstOrDefault(t => t.Id == comment.TaskAllocationId); } + if (task != null) + { + comment = comments.OrderBy(c => c.CommentDate).FirstOrDefault(c => c.TaskAllocationId == task.Id); + } var workItem = workItems.FirstOrDefault(w => w.Id == task?.WorkItemId); var workArea = workAreas.FirstOrDefault(wa => wa.Id == workItem?.WorkAreaId); From ec1e2dc59f724940701e12e52afd2cb5882479ac Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 7 Jul 2025 17:44:58 +0530 Subject: [PATCH 025/124] Storing workItem in cache and changing planned work and completed work for respective project, building, floor, and workarea --- Marco.Pms.CacheHelper/ProjectCache.cs | 120 ++++++++++++++++++ .../MongoDBModels/ActivityMasterMongoDB.cs | 2 +- .../MongoDBModels/BuildingMongoDB.cs | 2 +- Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs | 2 +- .../MongoDBModels/ProjectMongoDB.cs | 2 +- .../WorkCategoryMasterMongoDB.cs | 2 +- .../MongoDBModels/WorkItemMongoDB.cs | 2 +- .../Controllers/ProjectController.cs | 73 +++++++++-- .../Helpers/CacheUpdateHelper.cs | 65 ++++++++++ .../appsettings.Development.json | 2 +- 10 files changed, 256 insertions(+), 16 deletions(-) diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index f60884f..6f5a3d3 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -1,4 +1,5 @@ using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.Master; using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.Projects; using Microsoft.EntityFrameworkCore; @@ -434,10 +435,129 @@ namespace Marco.Pms.CacheHelper return buildings; } + public async Task UpdatePlannedAndCompleteWorksInBuildingFromCache(Guid workAreaId, double plannedWork, double completedWork) + { + var filter = Builders.Filter.Eq("Buildings.Floors.WorkAreas._id", workAreaId.ToString()); + var project = await _projetCollection.Find(filter).FirstOrDefaultAsync(); + + string? selectedBuildingId = null; + string? selectedFloorId = null; + string? selectedWorkAreaId = null; + + foreach (var building in project.Buildings) + { + foreach (var floor in building.Floors) + { + foreach (var area in floor.WorkAreas) + { + if (area.Id == workAreaId.ToString()) + { + selectedWorkAreaId = area.Id; + selectedFloorId = floor.Id; + selectedBuildingId = building.Id; + } + } + } + } + + var arrayFilters = new List + { + new JsonArrayFilterDefinition("{ 'b._id': '" + selectedBuildingId + "' }"), + new JsonArrayFilterDefinition("{ 'f._id': '" + selectedFloorId + "' }"), + new JsonArrayFilterDefinition("{ 'a._id': '" + selectedWorkAreaId + "' }") + }; + var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; + var update = Builders.Update + .Inc("Buildings.$[b].Floors.$[f].WorkAreas.$[a].PlannedWork", plannedWork) + .Inc("Buildings.$[b].Floors.$[f].WorkAreas.$[a].CompletedWork", completedWork) + .Inc("Buildings.$[b].Floors.$[f].PlannedWork", plannedWork) + .Inc("Buildings.$[b].Floors.$[f].CompletedWork", completedWork) + .Inc("Buildings.$[b].PlannedWork", plannedWork) + .Inc("Buildings.$[b].CompletedWork", completedWork) + .Inc("PlannedWork", plannedWork) + .Inc("CompletedWork", completedWork); + var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); + + } // ------------------------------------------------------- WorkItem ------------------------------------------------------- + public async Task ManageWorkItemDetailsToCache(List workItems) + { + var activityIds = workItems.Select(wi => wi.ActivityId).ToList(); + var workCategoryIds = workItems.Select(wi => wi.WorkCategoryId).ToList(); + // fetching Activity master + var activities = await _context.ActivityMasters.Where(a => activityIds.Contains(a.Id)).ToListAsync() ?? new List(); + // Fetching Work Category + var workCategories = await _context.WorkCategoryMasters.Where(wc => workCategoryIds.Contains(wc.Id)).ToListAsync() ?? new List(); + + foreach (WorkItem workItem in workItems) + { + var activity = activities.FirstOrDefault(a => a.Id == workItem.ActivityId) ?? new ActivityMaster(); + var workCategory = workCategories.FirstOrDefault(a => a.Id == workItem.WorkCategoryId) ?? new WorkCategoryMaster(); + + var filter = Builders.Filter.Eq(p => p.Id, workItem.Id.ToString()); + var updates = Builders.Update.Combine( + Builders.Update.Set(r => r.WorkAreaId, workItem.WorkAreaId.ToString()), + Builders.Update.Set(r => r.ParentTaskId, (workItem.ParentTaskId != null ? workItem.ParentTaskId.ToString() : null)), + Builders.Update.Set(r => r.PlannedWork, workItem.PlannedWork), + Builders.Update.Set(r => r.TodaysAssigned, 0), + Builders.Update.Set(r => r.CompletedWork, workItem.CompletedWork), + Builders.Update.Set(r => r.Description, workItem.Description), + Builders.Update.Set(r => r.TaskDate, workItem.TaskDate), + Builders.Update.Set(r => r.ActivityMaster, new ActivityMasterMongoDB + { + Id = activity.Id.ToString(), + ActivityName = activity.ActivityName, + UnitOfMeasurement = activity.UnitOfMeasurement + }), + Builders.Update.Set(r => r.WorkCategoryMaster, new WorkCategoryMasterMongoDB + { + Id = workCategory.Id.ToString(), + Name = workCategory.Name, + Description = workCategory.Description, + }) + ); + var options = new UpdateOptions { IsUpsert = true }; + var result = await _taskCollection.UpdateOneAsync(filter, updates, options); + } + } + public async Task> GetWorkItemDetailsByWorkAreaFromCache(Guid workAreaId) + { + var filter = Builders.Filter.Eq(p => p.WorkAreaId, workAreaId.ToString()); + + var options = new UpdateOptions { IsUpsert = true }; + var workItems = await _taskCollection + .Find(filter) + .ToListAsync(); + return workItems; + } + public async Task GetWorkItemDetailsByIdFromCache(Guid id) + { + var filter = Builders.Filter.Eq(p => p.Id, id.ToString()); + + var options = new UpdateOptions { IsUpsert = true }; + var workItem = await _taskCollection + .Find(filter) + .FirstOrDefaultAsync(); + return workItem; + } + public async Task UpdatePlannedAndCompleteWorksInWorkItem(Guid id, double plannedWork = 0, double completedWork = 0, double todaysAssigned = 0) + { + var filter = Builders.Filter.Eq(p => p.Id, id.ToString()); + var updates = Builders.Update + .Inc("PlannedWork", plannedWork) + .Inc("CompletedWork", completedWork) + .Inc("TodaysAssigned", todaysAssigned); + + var result = await _taskCollection.UpdateOneAsync(filter, updates); + if (result.ModifiedCount > 0) + { + return true; + } + return false; + } } } diff --git a/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs index 37218b7..cc77d96 100644 --- a/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs @@ -2,7 +2,7 @@ { public class ActivityMasterMongoDB { - public string? Id { get; set; } + public string Id { get; set; } = string.Empty; public string? ActivityName { get; set; } public string? UnitOfMeasurement { get; set; } } diff --git a/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs b/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs index 87ccb8d..64ccbce 100644 --- a/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs @@ -7,7 +7,7 @@ public string? Description { get; set; } public double PlannedWork { get; set; } public double CompletedWork { get; set; } - public List? Floors { get; set; } + public List Floors { get; set; } = new List(); } public class BuildingMongoDBVM { diff --git a/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs b/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs index ae3975f..57257a4 100644 --- a/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs @@ -6,7 +6,7 @@ public string? FloorName { get; set; } public double PlannedWork { get; set; } public double CompletedWork { get; set; } - public List? WorkAreas { get; set; } + public List WorkAreas { get; set; } = new List(); } public class FloorMongoDBVM diff --git a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs index 8b1612c..7f3a557 100644 --- a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs @@ -7,7 +7,7 @@ public string? ShortName { get; set; } public string? ProjectAddress { get; set; } public string? ContactPerson { get; set; } - public List? Buildings { get; set; } + public List Buildings { get; set; } = new List(); public DateTime? StartDate { get; set; } public DateTime? EndDate { get; set; } public StatusMasterMongoDB? ProjectStatus { get; set; } diff --git a/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs index aef0ada..4ea4682 100644 --- a/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs @@ -2,7 +2,7 @@ { public class WorkCategoryMasterMongoDB { - public string? Id { get; set; } + public string Id { get; set; } = string.Empty; public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; } diff --git a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs index 71638a3..850300d 100644 --- a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs @@ -6,7 +6,7 @@ public string WorkAreaId { get; set; } = string.Empty; public ActivityMasterMongoDB? ActivityMaster { get; set; } public WorkCategoryMasterMongoDB? WorkCategoryMaster { get; set; } - public string? ParentTaskId { get; set; } + public string? ParentTaskId { get; set; } = null; public double PlannedWork { get; set; } = 0; public double TodaysAssigned { get; set; } = 0; public double CompletedWork { get; set; } = 0; diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index a440c21..3ae76ed 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -734,16 +734,45 @@ namespace MarcoBMS.Services.Controllers } // Step 4: Fetch WorkItems with related Activity and Work Category data - var workItems = await _context.WorkItems - .Include(wi => wi.ActivityMaster) - .Include(wi => wi.WorkCategoryMaster) - .Where(wi => wi.WorkAreaId == workAreaId) - .ToListAsync(); + var workItemVMs = await _cache.GetWorkItemDetailsByWorkArea(workAreaId); + if (workItemVMs == null) + { + var workItems = await _context.WorkItems + .Include(wi => wi.ActivityMaster) + .Include(wi => wi.WorkCategoryMaster) + .Where(wi => wi.WorkAreaId == workAreaId) + .ToListAsync(); - _logger.LogInfo("{Count} work items fetched successfully for WorkAreaId: {WorkAreaId}", workItems.Count, workAreaId); + workItemVMs = workItems.Select(wi => new WorkItemMongoDB + { + Id = wi.Id.ToString(), + WorkAreaId = wi.WorkAreaId.ToString(), + ParentTaskId = wi.ParentTaskId.ToString(), + ActivityMaster = new ActivityMasterMongoDB + { + Id = wi.ActivityId.ToString(), + ActivityName = wi.ActivityMaster != null ? wi.ActivityMaster.ActivityName : null, + UnitOfMeasurement = wi.ActivityMaster != null ? wi.ActivityMaster.UnitOfMeasurement : null + }, + WorkCategoryMaster = new WorkCategoryMasterMongoDB + { + Id = wi.ActivityId.ToString(), + Name = wi.WorkCategoryMaster != null ? wi.WorkCategoryMaster.Name : "", + Description = wi.WorkCategoryMaster != null ? wi.WorkCategoryMaster.Description : "" + }, + PlannedWork = wi.PlannedWork, + CompletedWork = wi.CompletedWork, + Description = wi.Description, + TaskDate = wi.TaskDate, + }).ToList(); + + await _cache.ManageWorkItemDetails(workItems); + } + + _logger.LogInfo("{Count} work items fetched successfully for WorkAreaId: {WorkAreaId}", workItemVMs.Count, workAreaId); // Step 5: Return result - return Ok(ApiResponse.SuccessResponse(workItems, $"{workItems.Count} records of tasks fetched successfully", 200)); + return Ok(ApiResponse.SuccessResponse(workItemVMs, $"{workItemVMs.Count} records of tasks fetched successfully", 200)); } [HttpPost("task")] @@ -765,6 +794,8 @@ namespace MarcoBMS.Services.Controllers var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); string message = ""; List projectIds = new List(); + var workItemIds = workItemDtos.Where(wi => wi.Id != null && wi.Id != Guid.Empty).Select(wi => wi.Id).ToList(); + var workItems = await _context.WorkItems.AsNoTracking().Where(wi => workItemIds.Contains(wi.Id)).ToListAsync(); foreach (var itemDto in workItemDtos) { @@ -778,6 +809,28 @@ namespace MarcoBMS.Services.Controllers // Update existing workItemsToUpdate.Add(workItem); message = $"Task Updated in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}"; + var existingWorkItem = workItems.FirstOrDefault(wi => wi.Id == workItem.Id); + double plannedWork = 0; + double completedWork = 0; + if (existingWorkItem != null) + { + if (existingWorkItem.PlannedWork != workItem.PlannedWork && existingWorkItem.CompletedWork != workItem.CompletedWork) + { + plannedWork = workItem.PlannedWork - existingWorkItem.PlannedWork; + completedWork = workItem.CompletedWork - existingWorkItem.CompletedWork; + } + else if (existingWorkItem.PlannedWork == workItem.PlannedWork && existingWorkItem.CompletedWork != workItem.CompletedWork) + { + plannedWork = 0; + completedWork = workItem.CompletedWork - existingWorkItem.CompletedWork; + } + else if (existingWorkItem.PlannedWork != workItem.PlannedWork && existingWorkItem.CompletedWork == workItem.CompletedWork) + { + plannedWork = workItem.PlannedWork - existingWorkItem.PlannedWork; + completedWork = 0; + } + await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, plannedWork, completedWork); + } } else { @@ -785,6 +838,7 @@ namespace MarcoBMS.Services.Controllers workItem.Id = Guid.NewGuid(); workItemsToCreate.Add(workItem); message = $"Task Added in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}"; + await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, workItem.PlannedWork, workItem.CompletedWork); } responseList.Add(new WorkItemVM @@ -793,6 +847,7 @@ namespace MarcoBMS.Services.Controllers WorkItem = workItem }); projectIds.Add(building.ProjectId); + } string responseMessage = ""; // Apply DB changes @@ -801,7 +856,7 @@ namespace MarcoBMS.Services.Controllers _logger.LogInfo("Adding {Count} new work items", workItemsToCreate.Count); await _context.WorkItems.AddRangeAsync(workItemsToCreate); responseMessage = "Task Added Successfully"; - + await _cache.ManageWorkItemDetails(workItemsToCreate); } if (workItemsToUpdate.Any()) @@ -809,7 +864,7 @@ namespace MarcoBMS.Services.Controllers _logger.LogInfo("Updating {Count} existing work items", workItemsToUpdate.Count); _context.WorkItems.UpdateRange(workItemsToUpdate); responseMessage = "Task Updated Successfully"; - + await _cache.ManageWorkItemDetails(workItemsToUpdate); } await _context.SaveChangesAsync(); diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 6ff9cfe..ecce8ab 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -109,6 +109,71 @@ namespace Marco.Pms.Services.Helpers return null; } } + public async Task UpdatePlannedAndCompleteWorksInBuilding(Guid workAreaId, double plannedWork = 0, double completedWork = 0) + { + try + { + await _projectCache.UpdatePlannedAndCompleteWorksInBuildingFromCache(workAreaId, plannedWork, completedWork); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while updating planned work and completed work in building infra form Cache: {Error}", ex.Message); + } + } + + // ------------------------------------------------------- WorkItem ------------------------------------------------------- + + public async Task ManageWorkItemDetails(List workItems) + { + try + { + await _projectCache.ManageWorkItemDetailsToCache(workItems); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while saving workItems form Cache: {Error}", ex.Message); + } + } + public async Task?> GetWorkItemDetailsByWorkArea(Guid workAreaId) + { + try + { + var workItems = await _projectCache.GetWorkItemDetailsByWorkAreaFromCache(workAreaId); + if (workItems.Count > 0) + { + return workItems; + } + else + { + return null; + } + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while fetching list of workItems form Cache: {Error}", ex.Message); + return null; + } + } + public async Task GetWorkItemDetailsById(Guid id) + { + try + { + var workItem = await _projectCache.GetWorkItemDetailsByIdFromCache(id); + if (workItem.Id != "") + { + return workItem; + } + else + { + return null; + } + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while fetching list of workItems form Cache: {Error}", ex.Message); + return null; + } + } // ------------------------------------ Employee Profile Cache --------------------------------------- diff --git a/Marco.Pms.Services/appsettings.Development.json b/Marco.Pms.Services/appsettings.Development.json index 5f5e19d..030c450 100644 --- a/Marco.Pms.Services/appsettings.Development.json +++ b/Marco.Pms.Services/appsettings.Development.json @@ -48,6 +48,6 @@ }, "MongoDB": { "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs", - "ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches" + "ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches?socketTimeoutMS=500&serverSelectionTimeoutMS=500&connectTimeoutMS=500" } } From c2354033b735c50237d3e0594d73308e9660e29f Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Fri, 4 Jul 2025 17:49:25 +0530 Subject: [PATCH 026/124] Saving project details with infrastructure, employee permissions and assigned project for that employee in mongodb --- Marco.Pms.CacheHelper/EmployeeCache.cs | 158 +++++++ .../Marco.Pms.CacheHelper.csproj | 18 + Marco.Pms.CacheHelper/ProjectCache.cs | 434 ++++++++++++++++++ Marco.Pms.Model/Marco.Pms.Model.csproj | 1 + .../MongoDBModels/ActivityMasterMongoDB.cs | 9 + .../MongoDBModels/BuildingMongoDB.cs | 18 + .../EmployeePermissionMongoDB.cs | 13 + Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs | 17 + .../MongoDBModels/ProjectMongoDB.cs | 18 + .../MongoDBModels/StatusMasterMongoDB.cs | 8 + .../MongoDBModels/WorkAreaMongoDB.cs | 15 + .../WorkCategoryMasterMongoDB.cs | 9 + .../MongoDBModels/WorkItemMongoDB.cs | 15 + .../Controllers/ProjectController.cs | 221 +++++++-- .../Controllers/RolesController.cs | 12 +- Marco.Pms.Services/Dockerfile | 1 + .../Helpers/CacheUpdateHelper.cs | 98 ++++ Marco.Pms.Services/Helpers/ProjectsHelper.cs | 69 +-- Marco.Pms.Services/Helpers/RolesHelper.cs | 7 +- Marco.Pms.Services/Marco.Pms.Services.csproj | 1 + Marco.Pms.Services/Program.cs | 6 +- .../Service/PermissionServices.cs | 18 +- .../appsettings.Development.json | 4 +- .../appsettings.Production.json | 5 +- marco.pms.api.sln | 6 + 25 files changed, 1090 insertions(+), 91 deletions(-) create mode 100644 Marco.Pms.CacheHelper/EmployeeCache.cs create mode 100644 Marco.Pms.CacheHelper/Marco.Pms.CacheHelper.csproj create mode 100644 Marco.Pms.CacheHelper/ProjectCache.cs create mode 100644 Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs create mode 100644 Marco.Pms.Services/Helpers/CacheUpdateHelper.cs diff --git a/Marco.Pms.CacheHelper/EmployeeCache.cs b/Marco.Pms.CacheHelper/EmployeeCache.cs new file mode 100644 index 0000000..7d75407 --- /dev/null +++ b/Marco.Pms.CacheHelper/EmployeeCache.cs @@ -0,0 +1,158 @@ +using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.MongoDBModels; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using MongoDB.Driver; + +namespace Marco.Pms.CacheHelper +{ + public class EmployeeCache + { + private readonly ApplicationDbContext _context; + //private readonly IMongoDatabase _mongoDB; + private readonly IMongoCollection _collection; + public EmployeeCache(ApplicationDbContext context, IConfiguration configuration) + { + var connectionString = configuration["MongoDB:ConnectionString"]; + _context = context; + var mongoUrl = new MongoUrl(connectionString); + var client = new MongoClient(mongoUrl); // Your MongoDB connection string + var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name + _collection = mongoDB.GetCollection("EmployeeProfile"); + } + public async Task AddApplicationRoleToCache(Guid employeeId, List roleIds) + { + var newRoleIds = roleIds.Select(r => r.ToString()).ToList(); + var newPermissionIds = await _context.RolePermissionMappings + .Where(rp => roleIds.Contains(rp.ApplicationRoleId)) + .Select(p => p.FeaturePermissionId.ToString()) + .Distinct() + .ToListAsync(); + + var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + + var update = Builders.Update + .AddToSetEach(e => e.ApplicationRoleIds, newRoleIds) + .AddToSetEach(e => e.PermissionIds, newPermissionIds); + + var result = await _collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); + if (result.MatchedCount == 0) + { + return false; + } + return true; + } + public async Task AddProjectsToCache(Guid employeeId, List projectIds) + { + var newprojectIds = projectIds.Select(p => p.ToString()).ToList(); + + var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + + var update = Builders.Update + .AddToSetEach(e => e.ProjectIds, newprojectIds); + + var result = await _collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); + if (result.MatchedCount == 0) + { + return false; + } + return true; + } + public async Task> GetProjectsFromCache(Guid employeeId) + { + var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + + + var result = await _collection + .Find(filter) + .FirstOrDefaultAsync(); + + var projectIds = new List(); + if (result != null) + { + projectIds = result.ProjectIds.Select(Guid.Parse).ToList(); + } + + return projectIds; + } + public async Task> GetPermissionsFromCache(Guid employeeId) + { + var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + + + var result = await _collection + .Find(filter) + .FirstOrDefaultAsync(); + + var permissionIds = new List(); + if (result != null) + { + permissionIds = result.PermissionIds.Select(Guid.Parse).ToList(); + } + + return permissionIds; + } + public async Task ClearAllProjectIdsFromCache(Guid employeeId) + { + var filter = Builders.Filter + .Eq(e => e.EmployeeId, employeeId.ToString()); + + var update = Builders.Update + .Set(e => e.ProjectIds, new List()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + return true; + } + public async Task RemoveRoleIdFromCache(Guid employeeId, Guid roleId) + { + var filter = Builders.Filter + .Eq(e => e.EmployeeId, employeeId.ToString()); + + var update = Builders.Update + .Pull(e => e.ApplicationRoleIds, roleId.ToString()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + if (result.ModifiedCount == 0) + return false; + + return true; + } + public async Task ClearAllPermissionIdsByEmployeeIDFromCache(Guid employeeId) + { + var filter = Builders.Filter + .Eq(e => e.EmployeeId, employeeId.ToString()); + + var update = Builders.Update + .Set(e => e.PermissionIds, new List()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + return true; + } + public async Task ClearAllPermissionIdsByRoleIdFromCache(Guid roleId) + { + var filter = Builders.Filter.AnyEq(e => e.ApplicationRoleIds, roleId.ToString()); + + var update = Builders.Update + .Set(e => e.PermissionIds, new List()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + return true; + } + } +} diff --git a/Marco.Pms.CacheHelper/Marco.Pms.CacheHelper.csproj b/Marco.Pms.CacheHelper/Marco.Pms.CacheHelper.csproj new file mode 100644 index 0000000..e12ac6c --- /dev/null +++ b/Marco.Pms.CacheHelper/Marco.Pms.CacheHelper.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs new file mode 100644 index 0000000..b667694 --- /dev/null +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -0,0 +1,434 @@ +using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.MongoDBModels; +using Marco.Pms.Model.Projects; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Marco.Pms.CacheHelper +{ + public class ProjectCache + { + private readonly ApplicationDbContext _context; + private readonly IMongoDatabase _mongoDB; + //private readonly ILoggingService _logger; + public ProjectCache(ApplicationDbContext context, IConfiguration configuration) + { + var connectionString = configuration["MongoDB:ConnectionString"]; + _context = context; + var mongoUrl = new MongoUrl(connectionString); + var client = new MongoClient(mongoUrl); // Your MongoDB connection string + _mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name + } + public async Task AddProjectDetailsToCache(Project project) + { + var projectCollection = _mongoDB.GetCollection("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(), + AreaName = wa.AreaName, + PlannedWork = waPlanned, + CompletedWork = waCompleted + }); + + floorPlanned += waPlanned; + floorCompleted += waCompleted; + } + + floorMongoList.Add(new FloorMongoDB + { + Id = floor.Id.ToString(), + FloorName = floor.FloorName, + PlannedWork = floorPlanned, + CompletedWork = floorCompleted, + WorkAreas = workAreaMongoList + }); + + buildingPlanned += floorPlanned; + buildingCompleted += floorCompleted; + } + + buildingMongoList.Add(new BuildingMongoDB + { + Id = building.Id.ToString(), + BuildingName = building.Name, + Description = building.Description, + PlannedWork = buildingPlanned, + CompletedWork = buildingCompleted, + Floors = floorMongoList + }); + + totalPlannedWork += buildingPlanned; + totalCompletedWork += buildingCompleted; + } + + projectDetails.Buildings = buildingMongoList; + projectDetails.PlannedWork = totalPlannedWork; + projectDetails.CompletedWork = totalCompletedWork; + + await projectCollection.InsertOneAsync(projectDetails); + //_logger.LogInfo("[AddProjectDetails] Project details inserted in MongoDB for ProjectId: {ProjectId}", project.Id); + } + public async Task UpdateProjectDetailsOnlyToCache(Project project) + { + //_logger.LogInfo("Starting update for project: {ProjectId}", project.Id); + + var projectStatus = await _context.StatusMasters + .FirstOrDefaultAsync(s => s.Id == project.ProjectStatusId); + + if (projectStatus == null) + { + //_logger.LogWarning("StatusMaster not found for ProjectStatusId: {StatusId}", project.ProjectStatusId); + } + + var projectCollection = _mongoDB.GetCollection("ProjectDetails"); + + // Build the update definition + var updates = Builders.Update.Combine( + Builders.Update.Set(r => r.Name, project.Name), + Builders.Update.Set(r => r.ProjectAddress, project.ProjectAddress), + Builders.Update.Set(r => r.ShortName, project.ShortName), + Builders.Update.Set(r => r.ProjectStatus, new StatusMasterMongoDB + { + Id = projectStatus?.Id.ToString(), + Status = projectStatus?.Status + }), + Builders.Update.Set(r => r.StartDate, project.StartDate), + Builders.Update.Set(r => r.EndDate, project.EndDate), + Builders.Update.Set(r => r.ContactPerson, project.ContactPerson) + ); + + // Perform the update + var result = await projectCollection.UpdateOneAsync( + filter: r => r.Id == project.Id.ToString(), + update: updates + ); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("No project matched in MongoDB for update. ProjectId: {ProjectId}", project.Id); + return false; + } + + //_logger.LogInfo("Project {ProjectId} successfully updated in MongoDB", project.Id); + return true; + } + public async Task GetProjectDetailsFromCache(Guid projectId) + { + var projectCollection = _mongoDB.GetCollection("ProjectDetails"); + + // Build filter and projection to exclude large 'Buildings' list + var filter = Builders.Filter.Eq(p => p.Id, projectId.ToString()); + var projection = Builders.Projection.Exclude(p => p.Buildings); + + //_logger.LogInfo("Fetching project details for ProjectId: {ProjectId} from MongoDB", projectId); + + // Perform query + var project = await projectCollection + .Find(filter) + .Project(projection) + .FirstOrDefaultAsync(); + + if (project == null) + { + //_logger.LogWarning("No project found in MongoDB for ProjectId: {ProjectId}", projectId); + return null; + } + + //// Deserialize the result manually + //var project = BsonSerializer.Deserialize(result); + + //_logger.LogInfo("Successfully fetched project details (excluding Buildings) for ProjectId: {ProjectId}", projectId); + return project; + } + public async Task AddBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId) + { + var stringProjectId = projectId.ToString(); + var projectCollection = _mongoDB.GetCollection("ProjectDetails"); + + // Add Building + if (building != null) + { + var buildingMongo = new BuildingMongoDB + { + Id = building.Id.ToString(), + BuildingName = building.Name, + Description = building.Description, + PlannedWork = 0, + CompletedWork = 0, + Floors = new List() + }; + + var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); + var update = Builders.Update.Push("Buildings", buildingMongo); + + var result = await projectCollection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("Project not found while adding building. ProjectId: {ProjectId}", projectId); + return; + } + + //_logger.LogInfo("Building {BuildingId} added to project {ProjectId}", building.Id, projectId); + return; + } + + // Add Floor + if (floor != null) + { + var floorMongo = new FloorMongoDB + { + Id = floor.Id.ToString(), + FloorName = floor.FloorName, + PlannedWork = 0, + CompletedWork = 0, + WorkAreas = new List() + }; + + var filter = Builders.Filter.And( + Builders.Filter.Eq(p => p.Id, stringProjectId), + Builders.Filter.Eq("Buildings._id", floor.BuildingId.ToString()) + ); + + var update = Builders.Update.Push("Buildings.$.Floors", floorMongo); + var result = await projectCollection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("Project or building not found while adding floor. ProjectId: {ProjectId}, BuildingId: {BuildingId}", projectId, floor.BuildingId); + return; + } + + //_logger.LogInfo("Floor {FloorId} added to building {BuildingId} in project {ProjectId}", floor.Id, floor.BuildingId, projectId); + return; + } + + // Add WorkArea + if (workArea != null && buildingId != null) + { + var workAreaMongo = new WorkAreaMongoDB + { + Id = workArea.Id.ToString(), + AreaName = workArea.AreaName, + PlannedWork = 0, + CompletedWork = 0 + }; + + var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); + + var arrayFilters = new List + { + new JsonArrayFilterDefinition("{ 'b._id': '" + buildingId + "' }"), + new JsonArrayFilterDefinition("{ 'f._id': '" + workArea.FloorId + "' }") + }; + + var update = Builders.Update.Push("Buildings.$[b].Floors.$[f].WorkAreas", workAreaMongo); + var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; + + var result = await projectCollection.UpdateOneAsync(filter, update, updateOptions); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("Project or nested structure not found while adding work area. ProjectId: {ProjectId}, BuildingId: {BuildingId}, FloorId: {FloorId}", projectId, buildingId, workArea.FloorId); + return; + } + + //_logger.LogInfo("WorkArea {WorkAreaId} added to floor {FloorId} in building {BuildingId}, ProjectId: {ProjectId}", workArea.Id, workArea.FloorId, buildingId, projectId); + return; + } + + // Fallback case when no valid data was passed + //_logger.LogWarning("No valid infra data provided to add for ProjectId: {ProjectId}", projectId); + } + public async Task UpdateBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId) + { + var stringProjectId = projectId.ToString(); + var projectCollection = _mongoDB.GetCollection("ProjectDetails"); + + // Update Building + if (building != null) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(p => p.Id, stringProjectId), + Builders.Filter.Eq("Buildings._id", building.Id.ToString()) + ); + + var update = Builders.Update.Combine( + Builders.Update.Set("Buildings.$.BuildingName", building.Name), + Builders.Update.Set("Buildings.$.Description", building.Description) + ); + + var result = await projectCollection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("Update failed: Project or Building not found. ProjectId: {ProjectId}, BuildingId: {BuildingId}", projectId, building.Id); + return false; + } + + //_logger.LogInfo("Building {BuildingId} updated successfully in project {ProjectId}", building.Id, projectId); + return true; + } + + // Update Floor + if (floor != null) + { + var arrayFilters = new List + { + new JsonArrayFilterDefinition("{ 'b._id': '" + floor.BuildingId + "' }"), + new JsonArrayFilterDefinition("{ 'f._id': '" + floor.Id + "' }") + }; + + var update = Builders.Update.Set("Buildings.$[b].Floors.$[f].FloorName", floor.FloorName); + var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; + var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); + + var result = await projectCollection.UpdateOneAsync(filter, update, updateOptions); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("Update failed: Project or Floor not found. ProjectId: {ProjectId}, BuildingId: {BuildingId}, FloorId: {FloorId}", projectId, floor.BuildingId, floor.Id); + return false; + } + + //_logger.LogInfo("Floor {FloorId} updated successfully in Building {BuildingId}, ProjectId: {ProjectId}", floor.Id, floor.BuildingId, projectId); + return true; + } + + // Update WorkArea + if (workArea != null && buildingId != null) + { + var arrayFilters = new List + { + new JsonArrayFilterDefinition("{ 'b._id': '" + buildingId + "' }"), + new JsonArrayFilterDefinition("{ 'f._id': '" + workArea.FloorId + "' }"), + new JsonArrayFilterDefinition("{ 'a._id': '" + workArea.Id + "' }") + }; + + var update = Builders.Update.Set("Buildings.$[b].Floors.$[f].WorkAreas.$[a].AreaName", workArea.AreaName); + var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; + var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); + + var result = await projectCollection.UpdateOneAsync(filter, update, updateOptions); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("Update failed: Project or WorkArea not found. ProjectId: {ProjectId}, BuildingId: {BuildingId}, FloorId: {FloorId}, WorkAreaId: {WorkAreaId}", + //projectId, buildingId, workArea.FloorId, workArea.Id); + return false; + } + + //_logger.LogInfo("WorkArea {WorkAreaId} updated successfully in Floor {FloorId}, Building {BuildingId}, ProjectId: {ProjectId}", + //workArea.Id, workArea.FloorId, buildingId, projectId); + return true; + } + + //_logger.LogWarning("No update performed. Missing or invalid data for ProjectId: {ProjectId}", projectId); + return false; + } + public async Task?> GetBuildingInfraFromCache(Guid projectId) + { + var projectCollection = _mongoDB.GetCollection("ProjectDetails"); + + // Filter by project ID + var filter = Builders.Filter.Eq(p => p.Id, projectId.ToString()); + + // Project only the "Buildings" field from the document + var buildings = await projectCollection + .Find(filter) + .Project(p => p.Buildings) + .FirstOrDefaultAsync(); + + //if (buildings == null) + //{ + // _logger.LogWarning("No building infrastructure found for ProjectId: {ProjectId}", projectId); + //} + //else + //{ + // _logger.LogInfo("Fetched {Count} buildings for ProjectId: {ProjectId}", buildings.Count, projectId); + //} + + return buildings; + } + } +} diff --git a/Marco.Pms.Model/Marco.Pms.Model.csproj b/Marco.Pms.Model/Marco.Pms.Model.csproj index d5927ce..a1a21a5 100644 --- a/Marco.Pms.Model/Marco.Pms.Model.csproj +++ b/Marco.Pms.Model/Marco.Pms.Model.csproj @@ -10,6 +10,7 @@ + diff --git a/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs new file mode 100644 index 0000000..37218b7 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs @@ -0,0 +1,9 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class ActivityMasterMongoDB + { + public string? Id { get; set; } + public string? ActivityName { get; set; } + public string? UnitOfMeasurement { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs b/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs new file mode 100644 index 0000000..87ccb8d --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs @@ -0,0 +1,18 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class BuildingMongoDB + { + public string Id { get; set; } = string.Empty; + public string? BuildingName { get; set; } + public string? Description { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } + public List? Floors { get; set; } + } + public class BuildingMongoDBVM + { + public string Id { get; set; } = string.Empty; + public string? Name { get; set; } + public string? Description { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs b/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs new file mode 100644 index 0000000..f141798 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs @@ -0,0 +1,13 @@ +using MongoDB.Bson.Serialization.Attributes; + +namespace Marco.Pms.Model.MongoDBModels +{ + [BsonIgnoreExtraElements] + public class EmployeePermissionMongoDB + { + public string EmployeeId { get; set; } = string.Empty; + public List ApplicationRoleIds { get; set; } = new List(); + public List PermissionIds { get; set; } = new List(); + public List ProjectIds { get; set; } = new List(); + } +} diff --git a/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs b/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs new file mode 100644 index 0000000..ae3975f --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs @@ -0,0 +1,17 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class FloorMongoDB + { + public string Id { get; set; } = string.Empty; + public string? FloorName { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } + public List? WorkAreas { get; set; } + } + + public class FloorMongoDBVM + { + public string Id { get; set; } = string.Empty; + public string? FloorName { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs new file mode 100644 index 0000000..8bf1c9a --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs @@ -0,0 +1,18 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class ProjectMongoDB + { + public string? Id { get; set; } + public string? Name { get; set; } + public string? ShortName { get; set; } + public string? ProjectAddress { get; set; } + public string? ContactPerson { get; set; } + public List? Buildings { get; set; } + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + public StatusMasterMongoDB? ProjectStatus { get; set; } + public int TeamSize { get; set; } + public double CompletedWork { get; set; } + public double PlannedWork { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs new file mode 100644 index 0000000..01a0552 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs @@ -0,0 +1,8 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class StatusMasterMongoDB + { + public string? Id { get; set; } + public string? Status { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs new file mode 100644 index 0000000..d17f52c --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs @@ -0,0 +1,15 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class WorkAreaMongoDB + { + public string Id { get; set; } = string.Empty; + public string? AreaName { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } + } + public class WorkAreaMongoDBVM + { + public string Id { get; set; } = string.Empty; + public string? AreaName { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs new file mode 100644 index 0000000..aef0ada --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs @@ -0,0 +1,9 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class WorkCategoryMasterMongoDB + { + public string? Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + } +} diff --git a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs new file mode 100644 index 0000000..dc7fdb9 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs @@ -0,0 +1,15 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class WorkItemMongoDB + { + public string? Id { get; set; } + public string? WorkAreaId { get; set; } + public ActivityMasterMongoDB? ActivityMaster { get; set; } + public WorkCategoryMasterMongoDB? WorkCategoryMaster { get; set; } + public string? ParentTaskId { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } + public string? Description { get; set; } + public DateTime TaskDate { get; set; } + } +} diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 6490c54..a440c21 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -2,10 +2,13 @@ using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Employees; 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; using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Model.ViewModels.Projects; +using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Service; using MarcoBMS.Services.Helpers; @@ -14,6 +17,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; +using MongoDB.Driver; namespace MarcoBMS.Services.Controllers { @@ -29,6 +33,7 @@ namespace MarcoBMS.Services.Controllers private readonly ProjectsHelper _projectsHelper; private readonly IHubContext _signalR; private readonly PermissionServices _permission; + private readonly CacheUpdateHelper _cache; private readonly Guid ViewProjects; private readonly Guid ManageProject; private readonly Guid ViewInfra; @@ -37,7 +42,7 @@ namespace MarcoBMS.Services.Controllers public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, ProjectsHelper projectHelper, - IHubContext signalR, PermissionServices permission) + IHubContext signalR, PermissionServices permission, CacheUpdateHelper cache) { _context = context; _userHelper = userHelper; @@ -45,13 +50,13 @@ namespace MarcoBMS.Services.Controllers _rolesHelper = rolesHelper; _projectsHelper = projectHelper; _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"); tenantId = _userHelper.GetTenantId(); - } [HttpGet("list/basic")] @@ -222,24 +227,54 @@ namespace MarcoBMS.Services.Controllers } // Step 5: Fetch project with status - var project = await _context.Projects + var projectDetails = await _cache.GetProjectDetails(id); + ProjectVM? projectVM = null; + if (projectDetails == null) + { + var project = await _context.Projects .Include(c => c.ProjectStatus) .FirstOrDefaultAsync(c => c.TenantId == tenantId && c.Id == id); + projectVM = GetProjectViewModel(project); + } + else + { + 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 (project == null) + if (projectVM == null) { _logger.LogWarning("Project not found. ProjectId: {ProjectId}", id); return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); } - // Step 6: Map and return result - var projectVM = GetProjectViewModel(project); + // Step 6: Return result + _logger.LogInfo("Project details fetched successfully. ProjectId: {ProjectId}", id); return Ok(ApiResponse.SuccessResponse(projectVM, "Project details fetched successfully", 200)); } - private ProjectVM GetProjectViewModel(Project project) + private ProjectVM? GetProjectViewModel(Project? project) { + if (project == null) + { + return null; + } return new ProjectVM { Id = project.Id, @@ -280,6 +315,9 @@ namespace MarcoBMS.Services.Controllers _context.Projects.Add(project); await _context.SaveChangesAsync(); + + await _cache.AddProjectDetails(project); + var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Create_Project", Response = project.ToProjectDto() }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); @@ -310,6 +348,13 @@ namespace MarcoBMS.Services.Controllers await _context.SaveChangesAsync(); + // Cache functions + bool isUpdated = await _cache.UpdateProjectDetailsOnly(project); + if (!isUpdated) + { + await _cache.AddProjectDetails(project); + } + var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Update_Project", Response = project.ToProjectDto() }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); @@ -524,6 +569,7 @@ namespace MarcoBMS.Services.Controllers employeeIds.Add(projectAllocation.EmployeeId); projectIds.Add(projectAllocation.ProjectId); } + await _cache.ClearAllProjectIds(item.EmpID); } catch (Exception ex) @@ -565,53 +611,102 @@ namespace MarcoBMS.Services.Controllers _logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have access to view infra", 403)); } - - // Step 4: Fetch buildings for the project - var buildings = await _context.Buildings - .Where(b => b.ProjectId == projectId) - .ToListAsync(); - - var buildingIds = buildings.Select(b => b.Id).ToList(); - - // Step 5: Fetch floors associated with the buildings - var floors = await _context.Floor - .Where(f => buildingIds.Contains(f.BuildingId)) - .ToListAsync(); - - var floorIds = floors.Select(f => f.Id).ToList(); - - // Step 6: Fetch work areas associated with the floors - var workAreas = await _context.WorkAreas - .Where(wa => floorIds.Contains(wa.FloorId)) - .ToListAsync(); - - // Step 7: Build the infra hierarchy (Building > Floors > Work Areas) - var infraVM = buildings.Select(b => + var result = await _cache.GetBuildingInfra(projectId); + if (result == null) { - var selectedFloors = floors - .Where(f => f.BuildingId == b.Id) - .Select(f => new - { - Id = f.Id, - FloorName = f.FloorName, - WorkAreas = workAreas - .Where(wa => wa.FloorId == f.Id) - .Select(wa => new { wa.Id, wa.AreaName }) - .ToList() - }).ToList(); - return new + // Step 4: Fetch buildings for the project + var buildings = await _context.Buildings + .Where(b => b.ProjectId == projectId) + .ToListAsync(); + + var buildingIds = buildings.Select(b => b.Id).ToList(); + + // Step 5: Fetch floors associated with the buildings + var floors = await _context.Floor + .Where(f => buildingIds.Contains(f.BuildingId)) + .ToListAsync(); + + var floorIds = floors.Select(f => f.Id).ToList(); + + // Step 6: Fetch work areas associated with the floors + var workAreas = await _context.WorkAreas + .Where(wa => floorIds.Contains(wa.FloorId)) + .ToListAsync(); + var workAreaIds = workAreas.Select(wa => wa.Id).ToList(); + + // Step 7: Fetch work items associated with the work area + var workItems = await _context.WorkItems + .Where(wi => workAreaIds.Contains(wi.WorkAreaId)) + .ToListAsync(); + + // Step 8: Build the infra hierarchy (Building > Floors > Work Areas) + List Buildings = new List(); + foreach (var building in buildings) { - Id = b.Id, - BuildingName = b.Name, - Floors = selectedFloors - }; - }).ToList(); + double buildingPlannedWorks = 0; + double buildingCompletedWorks = 0; + + var selectedFloors = floors.Where(f => f.BuildingId == building.Id).ToList(); + List Floors = new List(); + foreach (var floor in selectedFloors) + { + double floorPlannedWorks = 0; + double floorCompletedWorks = 0; + var selectedWorkAreas = workAreas.Where(wa => wa.FloorId == floor.Id).ToList(); + List WorkAreas = new List(); + foreach (var workArea in selectedWorkAreas) + { + double workAreaPlannedWorks = 0; + double workAreaCompletedWorks = 0; + var selectedWorkItems = workItems.Where(wi => wi.WorkAreaId == workArea.Id).ToList(); + foreach (var workItem in selectedWorkItems) + { + workAreaPlannedWorks += workItem.PlannedWork; + workAreaCompletedWorks += workItem.CompletedWork; + } + WorkAreaMongoDB workAreaMongo = new WorkAreaMongoDB + { + Id = workArea.Id.ToString(), + AreaName = workArea.AreaName, + PlannedWork = workAreaPlannedWorks, + CompletedWork = workAreaCompletedWorks + }; + WorkAreas.Add(workAreaMongo); + floorPlannedWorks += workAreaPlannedWorks; + floorCompletedWorks += workAreaCompletedWorks; + } + FloorMongoDB floorMongoDB = new FloorMongoDB + { + Id = floor.Id.ToString(), + FloorName = floor.FloorName, + PlannedWork = floorPlannedWorks, + CompletedWork = floorCompletedWorks, + WorkAreas = WorkAreas + }; + Floors.Add(floorMongoDB); + buildingPlannedWorks += floorPlannedWorks; + buildingCompletedWorks += floorCompletedWorks; + } + + var buildingMongo = new BuildingMongoDB + { + Id = building.Id.ToString(), + BuildingName = building.Name, + Description = building.Description, + PlannedWork = buildingPlannedWorks, + CompletedWork = buildingCompletedWorks, + Floors = Floors + }; + Buildings.Add(buildingMongo); + } + result = Buildings; + } _logger.LogInfo("Infra details fetched successfully for ProjectId: {ProjectId}, EmployeeId: {EmployeeId}, Buildings: {Count}", - projectId, loggedInEmployee.Id, infraVM.Count); + projectId, loggedInEmployee.Id, result.Count); - return Ok(ApiResponse.SuccessResponse(infraVM, "Infra details fetched successfully", 200)); + return Ok(ApiResponse.SuccessResponse(result, "Infra details fetched successfully", 200)); } [HttpGet("tasks/{workAreaId}")] @@ -807,6 +902,7 @@ namespace MarcoBMS.Services.Controllers responseData.building = building; responseMessage = "Buliding Added Successfully"; message = "Building Added"; + await _cache.AddBuildngInfra(building.ProjectId, building); } else { @@ -816,7 +912,7 @@ namespace MarcoBMS.Services.Controllers responseData.building = building; responseMessage = "Buliding Updated Successfully"; message = "Building Updated"; - + await _cache.UpdateBuildngInfra(building.ProjectId, building); } projectIds.Add(building.ProjectId); } @@ -824,6 +920,7 @@ namespace MarcoBMS.Services.Controllers { Floor floor = item.Floor.ToFloorFromFloorDto(tenantId); floor.TenantId = GetTenantId(); + bool isCreated = false; if (item.Floor.Id == null) { @@ -833,6 +930,7 @@ namespace MarcoBMS.Services.Controllers responseData.floor = floor; responseMessage = "Floor Added Successfully"; message = "Floor Added"; + isCreated = true; } else { @@ -844,13 +942,23 @@ namespace MarcoBMS.Services.Controllers message = "Floor Updated"; } Building? building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == floor.BuildingId); - projectIds.Add(building?.ProjectId ?? Guid.Empty); + var projectId = building?.ProjectId ?? Guid.Empty; + projectIds.Add(projectId); message = $"{message} in Building: {building?.Name}"; + if (isCreated) + { + await _cache.AddBuildngInfra(projectId, floor: floor); + } + else + { + await _cache.UpdateBuildngInfra(projectId, floor: floor); + } } if (item.WorkArea != null) { WorkArea workArea = item.WorkArea.ToWorkAreaFromWorkAreaDto(tenantId); workArea.TenantId = GetTenantId(); + bool isCreated = false; if (item.WorkArea.Id == null) { @@ -860,6 +968,7 @@ namespace MarcoBMS.Services.Controllers responseData.workArea = workArea; responseMessage = "Work Area Added Successfully"; message = "Work Area Added"; + isCreated = true; } else { @@ -871,8 +980,17 @@ namespace MarcoBMS.Services.Controllers message = "Work Area Updated"; } Floor? floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == workArea.FloorId); - projectIds.Add(floor?.Building?.ProjectId ?? Guid.Empty); + var projectId = floor?.Building?.ProjectId ?? Guid.Empty; + projectIds.Add(projectId); message = $"{message} in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}"; + if (isCreated) + { + await _cache.AddBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); + } + else + { + await _cache.UpdateBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); + } } } message = $"{message} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}"; @@ -996,6 +1114,7 @@ namespace MarcoBMS.Services.Controllers return Ok(ApiResponse.ErrorResponse(ex.Message, ex, 400)); } } + await _cache.ClearAllProjectIds(employeeId); var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeId = employeeId }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); diff --git a/Marco.Pms.Services/Controllers/RolesController.cs b/Marco.Pms.Services/Controllers/RolesController.cs index 2ac2b07..4c75b3e 100644 --- a/Marco.Pms.Services/Controllers/RolesController.cs +++ b/Marco.Pms.Services/Controllers/RolesController.cs @@ -10,6 +10,7 @@ using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels; using Marco.Pms.Model.ViewModels.Master; using Marco.Pms.Model.ViewModels.Roles; +using Marco.Pms.Services.Helpers; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; @@ -29,14 +30,17 @@ namespace MarcoBMS.Services.Controllers private readonly UserHelper _userHelper; private readonly UserManager _userManager; private readonly ILoggingService _logger; + private readonly CacheUpdateHelper _cache; - public RolesController(UserManager userManager, ApplicationDbContext context, RolesHelper rolesHelper, UserHelper userHelper, ILoggingService logger) + public RolesController(UserManager userManager, ApplicationDbContext context, RolesHelper rolesHelper, UserHelper userHelper, ILoggingService logger, + CacheUpdateHelper cache) { _context = context; _userManager = userManager; _rolesHelper = rolesHelper; _userHelper = userHelper; _logger = logger; + _cache = cache; } private Guid GetTenantId() @@ -292,6 +296,8 @@ namespace MarcoBMS.Services.Controllers if (modified) await _context.SaveChangesAsync(); + await _cache.ClearAllPermissionIdsByRoleId(id); + ApplicationRolesVM response = role.ToRoleVMFromApplicationRole(); List permissions = await _rolesHelper.GetFeaturePermissionByRoleID(response.Id); response.FeaturePermission = permissions.Select(c => c.ToFeaturePermissionVMFromFeaturePermission()).ToList(); @@ -424,12 +430,16 @@ namespace MarcoBMS.Services.Controllers if (role.IsEnabled == true) { _context.EmployeeRoleMappings.Add(mapping); + await _cache.AddApplicationRole(role.EmployeeId, [mapping.RoleId]); } } else if (role.IsEnabled == false) { _context.EmployeeRoleMappings.Remove(existingItem); + await _cache.RemoveRoleId(existingItem.EmployeeId, existingItem.RoleId); + await _cache.ClearAllPermissionIdsByEmployeeID(existingItem.EmployeeId); } + await _cache.ClearAllProjectIds(role.EmployeeId); } await _context.SaveChangesAsync(); diff --git a/Marco.Pms.Services/Dockerfile b/Marco.Pms.Services/Dockerfile index 5444e56..77311ee 100644 --- a/Marco.Pms.Services/Dockerfile +++ b/Marco.Pms.Services/Dockerfile @@ -19,6 +19,7 @@ COPY ["Marco.Pms.Services/Marco.Pms.Services.csproj", "Marco.Pms.Services/"] COPY ["Marco.Pms.DataAccess/Marco.Pms.DataAccess.csproj", "Marco.Pms.DataAccess/"] COPY ["Marco.Pms.Model/Marco.Pms.Model.csproj", "Marco.Pms.Model/"] COPY ["Marco.Pms.Utility/Marco.Pms.Utility.csproj", "Marco.Pms.Utility/"] +COPY ["Marco.Pms.Utility/Marco.Pms.CacheHelper.csproj", "Marco.Pms.CacheHelper/"] RUN dotnet restore "./Marco.Pms.Services/Marco.Pms.Services.csproj" COPY . . WORKDIR "/src/Marco.Pms.Services" diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs new file mode 100644 index 0000000..1c3ee70 --- /dev/null +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -0,0 +1,98 @@ +using Marco.Pms.CacheHelper; +using Marco.Pms.Model.MongoDBModels; +using Marco.Pms.Model.Projects; +using Project = Marco.Pms.Model.Projects.Project; + +namespace Marco.Pms.Services.Helpers +{ + public class CacheUpdateHelper + { + private readonly ProjectCache _projectCache; + private readonly EmployeeCache _employeeCache; + + public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache) + { + _projectCache = projectCache; + _employeeCache = employeeCache; + } + + // ------------------------------------ Project Details and Infrastructure Cache --------------------------------------- + public async Task AddProjectDetails(Project project) + { + await _projectCache.AddProjectDetailsToCache(project); + } + public async Task UpdateProjectDetailsOnly(Project project) + { + bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project); + return response; + } + public async Task GetProjectDetails(Guid projectId) + { + var response = await _projectCache.GetProjectDetailsFromCache(projectId); + return response; + } + public async Task AddBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) + { + await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + } + public async Task UpdateBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) + { + var response = await _projectCache.UpdateBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + if (!response) + { + await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + } + } + public async Task?> GetBuildingInfra(Guid projectId) + { + var response = await _projectCache.GetBuildingInfraFromCache(projectId); + return response; + } + + + // ------------------------------------ Employee Profile Cache --------------------------------------- + public async Task AddApplicationRole(Guid employeeId, List roleIds) + { + var response = await _employeeCache.AddApplicationRoleToCache(employeeId, roleIds); + } + public async Task AddProjects(Guid employeeId, List projectIds) + { + var response = await _employeeCache.AddProjectsToCache(employeeId, projectIds); + return response; + } + public async Task?> GetProjects(Guid employeeId) + { + var response = await _employeeCache.GetProjectsFromCache(employeeId); + if (response.Count > 0) + { + return response; + } + return null; + } + public async Task?> GetPermissions(Guid employeeId) + { + var response = await _employeeCache.GetPermissionsFromCache(employeeId); + if (response.Count > 0) + { + return response; + } + return null; + } + public async Task ClearAllProjectIds(Guid employeeId) + { + var response = await _employeeCache.ClearAllProjectIdsFromCache(employeeId); + } + public async Task ClearAllPermissionIdsByEmployeeID(Guid employeeId) + { + var response = await _employeeCache.ClearAllPermissionIdsByEmployeeIDFromCache(employeeId); + } + public async Task ClearAllPermissionIdsByRoleId(Guid roleId) + { + var response = await _employeeCache.ClearAllPermissionIdsByRoleIdFromCache(roleId); + } + public async Task RemoveRoleId(Guid employeeId, Guid roleId) + { + var response = await _employeeCache.RemoveRoleIdFromCache(employeeId, roleId); + } + } +} diff --git a/Marco.Pms.Services/Helpers/ProjectsHelper.cs b/Marco.Pms.Services/Helpers/ProjectsHelper.cs index 8ccbc85..3ccddba 100644 --- a/Marco.Pms.Services/Helpers/ProjectsHelper.cs +++ b/Marco.Pms.Services/Helpers/ProjectsHelper.cs @@ -2,11 +2,8 @@ using Marco.Pms.Model.Employees; using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Projects; -using Marco.Pms.Model.Utilities; -using Marco.Pms.Model.ViewModels.Projects; -using Microsoft.AspNetCore.Mvc; +using Marco.Pms.Services.Helpers; using Microsoft.EntityFrameworkCore; -using ModelServices.Helpers; namespace MarcoBMS.Services.Helpers { @@ -14,12 +11,14 @@ namespace MarcoBMS.Services.Helpers { private readonly ApplicationDbContext _context; private readonly RolesHelper _rolesHelper; + private readonly CacheUpdateHelper _cache; - public ProjectsHelper(ApplicationDbContext context, RolesHelper rolesHelper) + public ProjectsHelper(ApplicationDbContext context, RolesHelper rolesHelper, CacheUpdateHelper cache) { _context = context; _rolesHelper = rolesHelper; + _cache = cache; } public async Task> GetAllProjectByTanentID(Guid tanentID) @@ -53,40 +52,56 @@ namespace MarcoBMS.Services.Helpers public async Task> GetMyProjects(Guid tenantId, Employee LoggedInEmployee) { - List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(LoggedInEmployee.Id); - string[] projectsId = []; List projects = new List(); - // Define a common queryable base for projects - IQueryable projectQuery = _context.Projects.Where(c => c.TenantId == tenantId); + var projectIds = await _cache.GetProjects(LoggedInEmployee.Id); - // 2. Optimized Project Retrieval Logic - // 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) { - // 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 + projects = await _context.Projects.Where(p => projectIds.Contains(p.Id)).ToListAsync(); } 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()) + var featurePermissionIds = await _cache.GetPermissions(LoggedInEmployee.Id); + if (featurePermissionIds == null) { - return new List(); + 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); - // Use LINQ's Contains for efficient filtering by ProjectId - var projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); // Get distinct Guids + // 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 + } + else + { + // 3. Efficiently get project allocations and then filter projects + // Load allocations only once + var allocation = await GetProjectByEmployeeID(LoggedInEmployee.Id); - // Filter projects based on the retrieved ProjectIds - projects = await projectQuery.Where(c => projectIds.Contains(c.Id)).ToListAsync(); + // If there are no allocations, return an empty list early + if (allocation == null || !allocation.Any()) + { + return new List(); + } + + // 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(); + + } + projectIds = projects.Select(p => p.Id).ToList(); + await _cache.AddProjects(LoggedInEmployee.Id, projectIds); } return projects; diff --git a/Marco.Pms.Services/Helpers/RolesHelper.cs b/Marco.Pms.Services/Helpers/RolesHelper.cs index b571d03..15bf0b1 100644 --- a/Marco.Pms.Services/Helpers/RolesHelper.cs +++ b/Marco.Pms.Services/Helpers/RolesHelper.cs @@ -2,6 +2,7 @@ using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Entitlements; +using Marco.Pms.Services.Helpers; using Microsoft.EntityFrameworkCore; namespace MarcoBMS.Services.Helpers @@ -9,15 +10,19 @@ namespace MarcoBMS.Services.Helpers public class RolesHelper { private readonly ApplicationDbContext _context; - public RolesHelper(ApplicationDbContext context) + private readonly CacheUpdateHelper _cache; + public RolesHelper(ApplicationDbContext context, CacheUpdateHelper cache) { _context = context; + _cache = cache; } public async Task> GetFeaturePermissionByEmployeeID(Guid EmployeeID) { List roleMappings = await _context.EmployeeRoleMappings.Where(c => c.EmployeeId == EmployeeID && c.IsEnabled == true).Select(c => c.RoleId).ToListAsync(); + await _cache.AddApplicationRole(EmployeeID, roleMappings); + // _context.RolePermissionMappings var result = await (from rpm in _context.RolePermissionMappings diff --git a/Marco.Pms.Services/Marco.Pms.Services.csproj b/Marco.Pms.Services/Marco.Pms.Services.csproj index 7bef32f..a235e6a 100644 --- a/Marco.Pms.Services/Marco.Pms.Services.csproj +++ b/Marco.Pms.Services/Marco.Pms.Services.csproj @@ -44,6 +44,7 @@ + diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index 17eb5c7..1d9b4b3 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -1,4 +1,5 @@ using System.Text; +using Marco.Pms.CacheHelper; using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Authentication; using Marco.Pms.Model.Entitlements; @@ -136,6 +137,9 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddSingleton(); @@ -225,7 +229,7 @@ app.UseStaticFiles(); // Enables serving static files app.UseHttpsRedirection(); - +app.UseAuthentication(); app.UseAuthorization(); app.MapHub("/hubs/marco"); app.MapControllers(); diff --git a/Marco.Pms.Services/Service/PermissionServices.cs b/Marco.Pms.Services/Service/PermissionServices.cs index f3ddb58..ce7476b 100644 --- a/Marco.Pms.Services/Service/PermissionServices.cs +++ b/Marco.Pms.Services/Service/PermissionServices.cs @@ -2,6 +2,7 @@ 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,21 +13,24 @@ namespace Marco.Pms.Services.Service private readonly ApplicationDbContext _context; private readonly RolesHelper _rolesHelper; private readonly ProjectsHelper _projectsHelper; - public PermissionServices(ApplicationDbContext context, RolesHelper rolesHelper, ProjectsHelper projectsHelper) + private readonly CacheUpdateHelper _cache; + public PermissionServices(ApplicationDbContext context, RolesHelper rolesHelper, ProjectsHelper projectsHelper, CacheUpdateHelper cache) { _context = context; _rolesHelper = rolesHelper; _projectsHelper = projectsHelper; + _cache = cache; } public async Task HasPermission(Guid featurePermissionId, Guid employeeId) { - var hasPermission = await _context.EmployeeRoleMappings - .Where(er => er.EmployeeId == employeeId) - .Select(er => er.RoleId) - .Distinct() - .AnyAsync(roleId => _context.RolePermissionMappings - .Any(rp => rp.FeaturePermissionId == featurePermissionId && rp.ApplicationRoleId == roleId)); + var featurePermissionIds = await _cache.GetPermissions(employeeId); + if (featurePermissionIds == null) + { + List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(employeeId); + featurePermissionIds = featurePermission.Select(fp => fp.Id).ToList(); + } + var hasPermission = featurePermissionIds.Contains(featurePermissionId); return hasPermission; } public async Task HasProjectPermission(Employee emp, string projectId) diff --git a/Marco.Pms.Services/appsettings.Development.json b/Marco.Pms.Services/appsettings.Development.json index 1565018..ce80dc0 100644 --- a/Marco.Pms.Services/appsettings.Development.json +++ b/Marco.Pms.Services/appsettings.Development.json @@ -47,6 +47,8 @@ "BucketName": "testenv-marco-pms-documents" }, "MongoDB": { - "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs" + "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs", + "ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches" + //"DatabaseName": "" } } diff --git a/Marco.Pms.Services/appsettings.Production.json b/Marco.Pms.Services/appsettings.Production.json index 81aa998..0abe3f1 100644 --- a/Marco.Pms.Services/appsettings.Production.json +++ b/Marco.Pms.Services/appsettings.Production.json @@ -6,7 +6,7 @@ }, "Environment": { "Name": "Production", - "Title": "" + "Title": "" }, "ConnectionStrings": { "DefaultConnectionString": "Server=147.93.98.152;User ID=devuser;Password=AppUser@123$;Database=MarcoBMS1" @@ -40,6 +40,7 @@ "BucketName": "testenv-marco-pms-documents" }, "MongoDB": { - "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs" + "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs", + "ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches" } } \ No newline at end of file diff --git a/marco.pms.api.sln b/marco.pms.api.sln index 49d3e8c..424b709 100644 --- a/marco.pms.api.sln +++ b/marco.pms.api.sln @@ -11,6 +11,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Marco.Pms.Utility", "Marco. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Marco.Pms.Services", "Marco.Pms.Services\Marco.Pms.Services.csproj", "{27A83653-5B7F-4135-9886-01594D54AFAE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Marco.Pms.CacheHelper", "Marco.Pms.CacheHelper\Marco.Pms.CacheHelper.csproj", "{1A105C22-4ED7-4F54-8834-6923DDD96852}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,6 +35,10 @@ Global {27A83653-5B7F-4135-9886-01594D54AFAE}.Debug|Any CPU.Build.0 = Debug|Any CPU {27A83653-5B7F-4135-9886-01594D54AFAE}.Release|Any CPU.ActiveCfg = Release|Any CPU {27A83653-5B7F-4135-9886-01594D54AFAE}.Release|Any CPU.Build.0 = Release|Any CPU + {1A105C22-4ED7-4F54-8834-6923DDD96852}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A105C22-4ED7-4F54-8834-6923DDD96852}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A105C22-4ED7-4F54-8834-6923DDD96852}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A105C22-4ED7-4F54-8834-6923DDD96852}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 8e67e801a302579b9062f725a0837defae84700b Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Fri, 4 Jul 2025 17:50:27 +0530 Subject: [PATCH 027/124] removed comented code from appsetting file --- Marco.Pms.Services/appsettings.Development.json | 1 - 1 file changed, 1 deletion(-) diff --git a/Marco.Pms.Services/appsettings.Development.json b/Marco.Pms.Services/appsettings.Development.json index ce80dc0..5f5e19d 100644 --- a/Marco.Pms.Services/appsettings.Development.json +++ b/Marco.Pms.Services/appsettings.Development.json @@ -49,6 +49,5 @@ "MongoDB": { "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs", "ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches" - //"DatabaseName": "" } } From 0654bca655fed5b84e10fda64893c8bee1f7a06d Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Sat, 5 Jul 2025 15:25:01 +0530 Subject: [PATCH 028/124] Added error handling in cache helper --- .../Helpers/CacheUpdateHelper.cs | 170 +++++++++++++++--- 1 file changed, 143 insertions(+), 27 deletions(-) diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 1c3ee70..75b51b5 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -1,6 +1,7 @@ using Marco.Pms.CacheHelper; using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.Projects; +using MarcoBMS.Services.Service; using Project = Marco.Pms.Model.Projects.Project; namespace Marco.Pms.Services.Helpers @@ -9,90 +10,205 @@ namespace Marco.Pms.Services.Helpers { private readonly ProjectCache _projectCache; private readonly EmployeeCache _employeeCache; + private readonly ILoggingService _logger; - public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache) + public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache, ILoggingService logger) { _projectCache = projectCache; _employeeCache = employeeCache; + _logger = logger; } // ------------------------------------ Project Details and Infrastructure Cache --------------------------------------- public async Task AddProjectDetails(Project project) { - await _projectCache.AddProjectDetailsToCache(project); + try + { + await _projectCache.AddProjectDetailsToCache(project); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while adding project to Cache: {Error}", ex.Message); + } } public async Task UpdateProjectDetailsOnly(Project project) { - bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project); - return response; + try + { + bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while updating project to Cache: {Error}", ex.Message); + return false; + } } public async Task GetProjectDetails(Guid projectId) { - var response = await _projectCache.GetProjectDetailsFromCache(projectId); - return response; + try + { + var response = await _projectCache.GetProjectDetailsFromCache(projectId); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while getting project to Cache: {Error}", ex.Message); + return null; + } } + //public async Task?> GetProjectDetailsList(List projectIds) + //{ + // var response = await _projectCache.GetProjectDetailsListFromCache(projectIds); + // return response; + //} public async Task AddBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) { - await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + try + { + await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while adding project infra to Cache: {Error}", ex.Message); + } } public async Task UpdateBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) { - var response = await _projectCache.UpdateBuildngInfraToCache(projectId, building, floor, workArea, buildingId); - if (!response) + try { - await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + var response = await _projectCache.UpdateBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + if (!response) + { + await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + } + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while updating project infra to Cache: {Error}", ex.Message); } } public async Task?> GetBuildingInfra(Guid projectId) { - var response = await _projectCache.GetBuildingInfraFromCache(projectId); - return response; + try + { + var response = await _projectCache.GetBuildingInfraFromCache(projectId); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while getting project infra Cache: {Error}", ex.Message); + return null; + } } // ------------------------------------ Employee Profile Cache --------------------------------------- public async Task AddApplicationRole(Guid employeeId, List roleIds) { - var response = await _employeeCache.AddApplicationRoleToCache(employeeId, roleIds); + try + { + var response = await _employeeCache.AddApplicationRoleToCache(employeeId, roleIds); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while adding Application roleIds to Cache to employee {Employee}: {Error}", employeeId, ex.Message); + } } public async Task AddProjects(Guid employeeId, List projectIds) { - var response = await _employeeCache.AddProjectsToCache(employeeId, projectIds); - return response; + try + { + var response = await _employeeCache.AddProjectsToCache(employeeId, projectIds); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while adding projectIds to Cache: {Error}", ex.Message); + return false; + } } public async Task?> GetProjects(Guid employeeId) { - var response = await _employeeCache.GetProjectsFromCache(employeeId); - if (response.Count > 0) + try { - return response; + var response = await _employeeCache.GetProjectsFromCache(employeeId); + if (response.Count > 0) + { + return response; + } + return null; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while getting projectIDs to Cache: {Error}", ex.Message); + return null; } - return null; } public async Task?> GetPermissions(Guid employeeId) { - var response = await _employeeCache.GetPermissionsFromCache(employeeId); - if (response.Count > 0) + try { - return response; + var response = await _employeeCache.GetPermissionsFromCache(employeeId); + if (response.Count > 0) + { + return response; + } + return null; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while getting permissionIds to Cache: {Error}", ex.Message); + return null; } - return null; } public async Task ClearAllProjectIds(Guid employeeId) { - var response = await _employeeCache.ClearAllProjectIdsFromCache(employeeId); + try + { + var response = await _employeeCache.ClearAllProjectIdsFromCache(employeeId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting projectIds from Cache for employee {EmployeeId}: {Error}", employeeId, ex.Message); + } } + //public async Task ClearAllProjectIdsByRoleId(Guid roleId) + //{ + // await _employeeCache.ClearAllProjectIdsByRoleIdFromCache(roleId); + //} public async Task ClearAllPermissionIdsByEmployeeID(Guid employeeId) { - var response = await _employeeCache.ClearAllPermissionIdsByEmployeeIDFromCache(employeeId); + try + { + var response = await _employeeCache.ClearAllPermissionIdsByEmployeeIDFromCache(employeeId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting permissionIds from to Cache: {Error}", ex.Message); + } } public async Task ClearAllPermissionIdsByRoleId(Guid roleId) { - var response = await _employeeCache.ClearAllPermissionIdsByRoleIdFromCache(roleId); + try + { + var response = await _employeeCache.ClearAllPermissionIdsByRoleIdFromCache(roleId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting permissionIds from to Cache: {Error}", ex.Message); + } } public async Task RemoveRoleId(Guid employeeId, Guid roleId) { - var response = await _employeeCache.RemoveRoleIdFromCache(employeeId, roleId); + try + { + var response = await _employeeCache.RemoveRoleIdFromCache(employeeId, roleId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting Application roleIds from to Cache: {Error}", ex.Message); + } } } } From 11b54debc6eaa55b302ab39578ca03dbf97db228 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 7 Jul 2025 10:04:11 +0530 Subject: [PATCH 029/124] Implemented the methods for deleting permission am asigned project from caches for certien employee --- Marco.Pms.CacheHelper/EmployeeCache.cs | 60 ++++++++++++++----- Marco.Pms.CacheHelper/ProjectCache.cs | 55 ++++++++++------- .../EmployeePermissionMongoDB.cs | 2 +- .../MongoDBModels/ProjectMongoDB.cs | 2 +- .../MongoDBModels/WorkItemMongoDB.cs | 9 +-- .../Controllers/RolesController.cs | 4 ++ .../Helpers/CacheUpdateHelper.cs | 57 +++++++++++------- Marco.Pms.Services/Helpers/ProjectsHelper.cs | 21 ++++++- 8 files changed, 144 insertions(+), 66 deletions(-) diff --git a/Marco.Pms.CacheHelper/EmployeeCache.cs b/Marco.Pms.CacheHelper/EmployeeCache.cs index 7d75407..5c86e6f 100644 --- a/Marco.Pms.CacheHelper/EmployeeCache.cs +++ b/Marco.Pms.CacheHelper/EmployeeCache.cs @@ -22,31 +22,47 @@ namespace Marco.Pms.CacheHelper } public async Task AddApplicationRoleToCache(Guid employeeId, List roleIds) { - var newRoleIds = roleIds.Select(r => r.ToString()).ToList(); - var newPermissionIds = await _context.RolePermissionMappings + // 1. Guard Clause: Avoid unnecessary database work if there are no roles to add. + if (roleIds == null || !roleIds.Any()) + { + return false; // Nothing to add, so the operation did not result in a change. + } + + // 2. Perform database queries concurrently for better performance. + var employeeIdString = employeeId.ToString(); + + Task> getPermissionIdsTask = _context.RolePermissionMappings .Where(rp => roleIds.Contains(rp.ApplicationRoleId)) .Select(p => p.FeaturePermissionId.ToString()) .Distinct() .ToListAsync(); - var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + // 3. Prepare role IDs in parallel with the database query. + var newRoleIds = roleIds.Select(r => r.ToString()).ToList(); + + // 4. Await the database query result. + var newPermissionIds = await getPermissionIdsTask; + + // 5. Build a single, efficient update operation. + var filter = Builders.Filter.Eq(e => e.Id, employeeIdString); var update = Builders.Update .AddToSetEach(e => e.ApplicationRoleIds, newRoleIds) .AddToSetEach(e => e.PermissionIds, newPermissionIds); - var result = await _collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); - if (result.MatchedCount == 0) - { - return false; - } - return true; + var options = new UpdateOptions { IsUpsert = true }; + + var result = await _collection.UpdateOneAsync(filter, update, options); + + // 6. Return a more accurate result indicating success for both updates and upserts. + // The operation is successful if an existing document was modified OR a new one was created. + return result.IsAcknowledged && (result.ModifiedCount > 0 || result.UpsertedId != null); } public async Task AddProjectsToCache(Guid employeeId, List projectIds) { var newprojectIds = projectIds.Select(p => p.ToString()).ToList(); - var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + var filter = Builders.Filter.Eq(e => e.Id, employeeId.ToString()); var update = Builders.Update .AddToSetEach(e => e.ProjectIds, newprojectIds); @@ -60,7 +76,7 @@ namespace Marco.Pms.CacheHelper } public async Task> GetProjectsFromCache(Guid employeeId) { - var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + var filter = Builders.Filter.Eq(e => e.Id, employeeId.ToString()); var result = await _collection @@ -77,7 +93,7 @@ namespace Marco.Pms.CacheHelper } public async Task> GetPermissionsFromCache(Guid employeeId) { - var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + var filter = Builders.Filter.Eq(e => e.Id, employeeId.ToString()); var result = await _collection @@ -95,7 +111,21 @@ namespace Marco.Pms.CacheHelper public async Task ClearAllProjectIdsFromCache(Guid employeeId) { var filter = Builders.Filter - .Eq(e => e.EmployeeId, employeeId.ToString()); + .Eq(e => e.Id, employeeId.ToString()); + + var update = Builders.Update + .Set(e => e.ProjectIds, new List()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + return true; + } + public async Task ClearAllProjectIdsByRoleIdFromCache(Guid roleId) + { + var filter = Builders.Filter.AnyEq(e => e.ApplicationRoleIds, roleId.ToString()); var update = Builders.Update .Set(e => e.ProjectIds, new List()); @@ -110,7 +140,7 @@ namespace Marco.Pms.CacheHelper public async Task RemoveRoleIdFromCache(Guid employeeId, Guid roleId) { var filter = Builders.Filter - .Eq(e => e.EmployeeId, employeeId.ToString()); + .Eq(e => e.Id, employeeId.ToString()); var update = Builders.Update .Pull(e => e.ApplicationRoleIds, roleId.ToString()); @@ -128,7 +158,7 @@ namespace Marco.Pms.CacheHelper public async Task ClearAllPermissionIdsByEmployeeIDFromCache(Guid employeeId) { var filter = Builders.Filter - .Eq(e => e.EmployeeId, employeeId.ToString()); + .Eq(e => e.Id, employeeId.ToString()); var update = Builders.Update .Set(e => e.PermissionIds, new List()); diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index b667694..f60884f 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -11,19 +11,21 @@ namespace Marco.Pms.CacheHelper public class ProjectCache { private readonly ApplicationDbContext _context; - private readonly IMongoDatabase _mongoDB; - //private readonly ILoggingService _logger; + private readonly IMongoCollection _projetCollection; + private readonly IMongoCollection _taskCollection; public ProjectCache(ApplicationDbContext context, IConfiguration configuration) { var connectionString = configuration["MongoDB:ConnectionString"]; _context = context; var mongoUrl = new MongoUrl(connectionString); var client = new MongoClient(mongoUrl); // Your MongoDB connection string - _mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name + var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name + _projetCollection = mongoDB.GetCollection("ProjectDetails"); + _taskCollection = mongoDB.GetCollection("WorkItemDetails"); } public async Task AddProjectDetailsToCache(Project project) { - var projectCollection = _mongoDB.GetCollection("ProjectDetails"); + //_logger.LogInfo("[AddProjectDetails] Initiated for ProjectId: {ProjectId}", project.Id); @@ -145,7 +147,7 @@ namespace Marco.Pms.CacheHelper projectDetails.PlannedWork = totalPlannedWork; projectDetails.CompletedWork = totalCompletedWork; - await projectCollection.InsertOneAsync(projectDetails); + await _projetCollection.InsertOneAsync(projectDetails); //_logger.LogInfo("[AddProjectDetails] Project details inserted in MongoDB for ProjectId: {ProjectId}", project.Id); } public async Task UpdateProjectDetailsOnlyToCache(Project project) @@ -160,8 +162,6 @@ namespace Marco.Pms.CacheHelper //_logger.LogWarning("StatusMaster not found for ProjectStatusId: {StatusId}", project.ProjectStatusId); } - var projectCollection = _mongoDB.GetCollection("ProjectDetails"); - // Build the update definition var updates = Builders.Update.Combine( Builders.Update.Set(r => r.Name, project.Name), @@ -178,7 +178,7 @@ namespace Marco.Pms.CacheHelper ); // Perform the update - var result = await projectCollection.UpdateOneAsync( + var result = await _projetCollection.UpdateOneAsync( filter: r => r.Id == project.Id.ToString(), update: updates ); @@ -194,7 +194,6 @@ namespace Marco.Pms.CacheHelper } public async Task GetProjectDetailsFromCache(Guid projectId) { - var projectCollection = _mongoDB.GetCollection("ProjectDetails"); // Build filter and projection to exclude large 'Buildings' list var filter = Builders.Filter.Eq(p => p.Id, projectId.ToString()); @@ -203,7 +202,7 @@ namespace Marco.Pms.CacheHelper //_logger.LogInfo("Fetching project details for ProjectId: {ProjectId} from MongoDB", projectId); // Perform query - var project = await projectCollection + var project = await _projetCollection .Find(filter) .Project(projection) .FirstOrDefaultAsync(); @@ -214,16 +213,23 @@ namespace Marco.Pms.CacheHelper return null; } - //// Deserialize the result manually - //var project = BsonSerializer.Deserialize(result); - //_logger.LogInfo("Successfully fetched project details (excluding Buildings) for ProjectId: {ProjectId}", projectId); return project; } + public async Task?> GetProjectDetailsListFromCache(List projectIds) + { + List stringProjectIds = projectIds.Select(p => p.ToString()).ToList(); + var filter = Builders.Filter.In(p => p.Id, stringProjectIds); + var projection = Builders.Projection.Exclude(p => p.Buildings); + var projects = await _projetCollection + .Find(filter) + .Project(projection) + .ToListAsync(); + return projects; + } public async Task AddBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId) { var stringProjectId = projectId.ToString(); - var projectCollection = _mongoDB.GetCollection("ProjectDetails"); // Add Building if (building != null) @@ -241,7 +247,7 @@ namespace Marco.Pms.CacheHelper var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); var update = Builders.Update.Push("Buildings", buildingMongo); - var result = await projectCollection.UpdateOneAsync(filter, update); + var result = await _projetCollection.UpdateOneAsync(filter, update); if (result.MatchedCount == 0) { @@ -271,7 +277,7 @@ namespace Marco.Pms.CacheHelper ); var update = Builders.Update.Push("Buildings.$.Floors", floorMongo); - var result = await projectCollection.UpdateOneAsync(filter, update); + var result = await _projetCollection.UpdateOneAsync(filter, update); if (result.MatchedCount == 0) { @@ -305,7 +311,7 @@ namespace Marco.Pms.CacheHelper var update = Builders.Update.Push("Buildings.$[b].Floors.$[f].WorkAreas", workAreaMongo); var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; - var result = await projectCollection.UpdateOneAsync(filter, update, updateOptions); + var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); if (result.MatchedCount == 0) { @@ -323,7 +329,6 @@ namespace Marco.Pms.CacheHelper public async Task UpdateBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId) { var stringProjectId = projectId.ToString(); - var projectCollection = _mongoDB.GetCollection("ProjectDetails"); // Update Building if (building != null) @@ -338,7 +343,7 @@ namespace Marco.Pms.CacheHelper Builders.Update.Set("Buildings.$.Description", building.Description) ); - var result = await projectCollection.UpdateOneAsync(filter, update); + var result = await _projetCollection.UpdateOneAsync(filter, update); if (result.MatchedCount == 0) { @@ -363,7 +368,7 @@ namespace Marco.Pms.CacheHelper var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); - var result = await projectCollection.UpdateOneAsync(filter, update, updateOptions); + var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); if (result.MatchedCount == 0) { @@ -389,7 +394,7 @@ namespace Marco.Pms.CacheHelper var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); - var result = await projectCollection.UpdateOneAsync(filter, update, updateOptions); + var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); if (result.MatchedCount == 0) { @@ -408,13 +413,12 @@ namespace Marco.Pms.CacheHelper } public async Task?> GetBuildingInfraFromCache(Guid projectId) { - var projectCollection = _mongoDB.GetCollection("ProjectDetails"); // Filter by project ID var filter = Builders.Filter.Eq(p => p.Id, projectId.ToString()); // Project only the "Buildings" field from the document - var buildings = await projectCollection + var buildings = await _projetCollection .Find(filter) .Project(p => p.Buildings) .FirstOrDefaultAsync(); @@ -430,5 +434,10 @@ namespace Marco.Pms.CacheHelper return buildings; } + + + // ------------------------------------------------------- WorkItem ------------------------------------------------------- + + } } diff --git a/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs b/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs index f141798..49c514e 100644 --- a/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs @@ -5,7 +5,7 @@ namespace Marco.Pms.Model.MongoDBModels [BsonIgnoreExtraElements] public class EmployeePermissionMongoDB { - public string EmployeeId { get; set; } = string.Empty; + public string Id { get; set; } = string.Empty; // Employee ID public List ApplicationRoleIds { get; set; } = new List(); public List PermissionIds { get; set; } = new List(); public List ProjectIds { get; set; } = new List(); diff --git a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs index 8bf1c9a..8b1612c 100644 --- a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs @@ -2,7 +2,7 @@ { public class ProjectMongoDB { - public string? Id { get; set; } + public string Id { get; set; } = string.Empty; public string? Name { get; set; } public string? ShortName { get; set; } public string? ProjectAddress { get; set; } diff --git a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs index dc7fdb9..71638a3 100644 --- a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs @@ -2,13 +2,14 @@ { public class WorkItemMongoDB { - public string? Id { get; set; } - public string? WorkAreaId { get; set; } + public string Id { get; set; } = string.Empty; + public string WorkAreaId { get; set; } = string.Empty; public ActivityMasterMongoDB? ActivityMaster { get; set; } public WorkCategoryMasterMongoDB? WorkCategoryMaster { get; set; } public string? ParentTaskId { get; set; } - public double PlannedWork { get; set; } - public double CompletedWork { get; set; } + public double PlannedWork { get; set; } = 0; + public double TodaysAssigned { get; set; } = 0; + public double CompletedWork { get; set; } = 0; public string? Description { get; set; } public DateTime TaskDate { get; set; } } diff --git a/Marco.Pms.Services/Controllers/RolesController.cs b/Marco.Pms.Services/Controllers/RolesController.cs index 4c75b3e..a67ecaf 100644 --- a/Marco.Pms.Services/Controllers/RolesController.cs +++ b/Marco.Pms.Services/Controllers/RolesController.cs @@ -292,6 +292,10 @@ namespace MarcoBMS.Services.Controllers _context.RolePermissionMappings.Add(item); modified = true; } + if (item.FeaturePermissionId == Guid.Parse("172fc9b6-755b-4f62-ab26-55c34a330614")) + { + await _cache.ClearAllProjectIdsByRoleId(id); + } } if (modified) await _context.SaveChangesAsync(); diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 75b51b5..6ff9cfe 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -28,7 +28,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while adding project to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while adding project {ProjectId} to Cache : {Error}", project.Id, ex.Message); } } public async Task UpdateProjectDetailsOnly(Project project) @@ -40,7 +40,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while updating project to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while updating project {ProjectId} to Cache: {Error}", project.Id, ex.Message); return false; } } @@ -53,15 +53,23 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while getting project to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while getting project {ProjectId} to Cache: {Error}", ex.Message); + return null; + } + } + public async Task?> GetProjectDetailsList(List projectIds) + { + try + { + var response = await _projectCache.GetProjectDetailsListFromCache(projectIds); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while getting list od project details from to Cache: {Error}", ex.Message); return null; } } - //public async Task?> GetProjectDetailsList(List projectIds) - //{ - // var response = await _projectCache.GetProjectDetailsListFromCache(projectIds); - // return response; - //} public async Task AddBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) { try @@ -70,7 +78,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while adding project infra to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while adding project infra for project {ProjectId} to Cache: {Error}", projectId, ex.Message); } } public async Task UpdateBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) @@ -85,7 +93,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while updating project infra to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while updating project infra for project {ProjectId} to Cache: {Error}", projectId, ex.Message); } } public async Task?> GetBuildingInfra(Guid projectId) @@ -97,7 +105,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while getting project infra Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while getting project infra for project {ProjectId} form Cache: {Error}", projectId, ex.Message); return null; } } @@ -124,7 +132,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while adding projectIds to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while adding projectIds for employee {EmployeeId} to Cache: {Error}", employeeId, ex.Message); return false; } } @@ -141,7 +149,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while getting projectIDs to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while getting projectIds for employee {EmployeeId} from Cache: {Error}", employeeId, ex.Message); return null; } } @@ -158,7 +166,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while getting permissionIds to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while getting permissionIds for employee {EmployeeId} from Cache: {Error}", employeeId, ex.Message); return null; } } @@ -173,10 +181,17 @@ namespace Marco.Pms.Services.Helpers _logger.LogWarning("Error occured while deleting projectIds from Cache for employee {EmployeeId}: {Error}", employeeId, ex.Message); } } - //public async Task ClearAllProjectIdsByRoleId(Guid roleId) - //{ - // await _employeeCache.ClearAllProjectIdsByRoleIdFromCache(roleId); - //} + public async Task ClearAllProjectIdsByRoleId(Guid roleId) + { + try + { + await _employeeCache.ClearAllProjectIdsByRoleIdFromCache(roleId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting projectIds from Cache for Application Role {RoleId}: {Error}", roleId, ex.Message); + } + } public async Task ClearAllPermissionIdsByEmployeeID(Guid employeeId) { try @@ -185,7 +200,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while deleting permissionIds from to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while deleting permissionIds from Cache for employee {EmployeeId}: {Error}", employeeId, ex.Message); } } public async Task ClearAllPermissionIdsByRoleId(Guid roleId) @@ -196,7 +211,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while deleting permissionIds from to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while deleting permissionIds from Cache for Application role {RoleId}: {Error}", roleId, ex.Message); } } public async Task RemoveRoleId(Guid employeeId, Guid roleId) @@ -207,7 +222,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while deleting Application roleIds from to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while deleting Application role {RoleId} from Cache for employee {EmployeeId}: {Error}", roleId, employeeId, ex.Message); } } } diff --git a/Marco.Pms.Services/Helpers/ProjectsHelper.cs b/Marco.Pms.Services/Helpers/ProjectsHelper.cs index 3ccddba..85003ae 100644 --- a/Marco.Pms.Services/Helpers/ProjectsHelper.cs +++ b/Marco.Pms.Services/Helpers/ProjectsHelper.cs @@ -1,6 +1,7 @@ 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 Microsoft.EntityFrameworkCore; @@ -59,7 +60,25 @@ namespace MarcoBMS.Services.Helpers if (projectIds != null) { - projects = await _context.Projects.Where(p => projectIds.Contains(p.Id)).ToListAsync(); + + List projectdetails = await _cache.GetProjectDetailsList(projectIds) ?? new List(); + projects = projectdetails.Select(p => new Project + { + 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 { From 80a197b408ffeb3f5c1a55c8416b8b06e728f40a Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 7 Jul 2025 17:44:58 +0530 Subject: [PATCH 030/124] Storing workItem in cache and changing planned work and completed work for respective project, building, floor, and workarea --- Marco.Pms.CacheHelper/ProjectCache.cs | 120 ++++++++++++++++++ .../MongoDBModels/ActivityMasterMongoDB.cs | 2 +- .../MongoDBModels/BuildingMongoDB.cs | 2 +- Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs | 2 +- .../MongoDBModels/ProjectMongoDB.cs | 2 +- .../WorkCategoryMasterMongoDB.cs | 2 +- .../MongoDBModels/WorkItemMongoDB.cs | 2 +- .../Controllers/ProjectController.cs | 73 +++++++++-- .../Helpers/CacheUpdateHelper.cs | 65 ++++++++++ .../appsettings.Development.json | 2 +- 10 files changed, 256 insertions(+), 16 deletions(-) diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index f60884f..6f5a3d3 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -1,4 +1,5 @@ using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.Master; using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.Projects; using Microsoft.EntityFrameworkCore; @@ -434,10 +435,129 @@ namespace Marco.Pms.CacheHelper return buildings; } + public async Task UpdatePlannedAndCompleteWorksInBuildingFromCache(Guid workAreaId, double plannedWork, double completedWork) + { + var filter = Builders.Filter.Eq("Buildings.Floors.WorkAreas._id", workAreaId.ToString()); + var project = await _projetCollection.Find(filter).FirstOrDefaultAsync(); + + string? selectedBuildingId = null; + string? selectedFloorId = null; + string? selectedWorkAreaId = null; + + foreach (var building in project.Buildings) + { + foreach (var floor in building.Floors) + { + foreach (var area in floor.WorkAreas) + { + if (area.Id == workAreaId.ToString()) + { + selectedWorkAreaId = area.Id; + selectedFloorId = floor.Id; + selectedBuildingId = building.Id; + } + } + } + } + + var arrayFilters = new List + { + new JsonArrayFilterDefinition("{ 'b._id': '" + selectedBuildingId + "' }"), + new JsonArrayFilterDefinition("{ 'f._id': '" + selectedFloorId + "' }"), + new JsonArrayFilterDefinition("{ 'a._id': '" + selectedWorkAreaId + "' }") + }; + var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; + var update = Builders.Update + .Inc("Buildings.$[b].Floors.$[f].WorkAreas.$[a].PlannedWork", plannedWork) + .Inc("Buildings.$[b].Floors.$[f].WorkAreas.$[a].CompletedWork", completedWork) + .Inc("Buildings.$[b].Floors.$[f].PlannedWork", plannedWork) + .Inc("Buildings.$[b].Floors.$[f].CompletedWork", completedWork) + .Inc("Buildings.$[b].PlannedWork", plannedWork) + .Inc("Buildings.$[b].CompletedWork", completedWork) + .Inc("PlannedWork", plannedWork) + .Inc("CompletedWork", completedWork); + var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); + + } // ------------------------------------------------------- WorkItem ------------------------------------------------------- + public async Task ManageWorkItemDetailsToCache(List workItems) + { + var activityIds = workItems.Select(wi => wi.ActivityId).ToList(); + var workCategoryIds = workItems.Select(wi => wi.WorkCategoryId).ToList(); + // fetching Activity master + var activities = await _context.ActivityMasters.Where(a => activityIds.Contains(a.Id)).ToListAsync() ?? new List(); + // Fetching Work Category + var workCategories = await _context.WorkCategoryMasters.Where(wc => workCategoryIds.Contains(wc.Id)).ToListAsync() ?? new List(); + + foreach (WorkItem workItem in workItems) + { + var activity = activities.FirstOrDefault(a => a.Id == workItem.ActivityId) ?? new ActivityMaster(); + var workCategory = workCategories.FirstOrDefault(a => a.Id == workItem.WorkCategoryId) ?? new WorkCategoryMaster(); + + var filter = Builders.Filter.Eq(p => p.Id, workItem.Id.ToString()); + var updates = Builders.Update.Combine( + Builders.Update.Set(r => r.WorkAreaId, workItem.WorkAreaId.ToString()), + Builders.Update.Set(r => r.ParentTaskId, (workItem.ParentTaskId != null ? workItem.ParentTaskId.ToString() : null)), + Builders.Update.Set(r => r.PlannedWork, workItem.PlannedWork), + Builders.Update.Set(r => r.TodaysAssigned, 0), + Builders.Update.Set(r => r.CompletedWork, workItem.CompletedWork), + Builders.Update.Set(r => r.Description, workItem.Description), + Builders.Update.Set(r => r.TaskDate, workItem.TaskDate), + Builders.Update.Set(r => r.ActivityMaster, new ActivityMasterMongoDB + { + Id = activity.Id.ToString(), + ActivityName = activity.ActivityName, + UnitOfMeasurement = activity.UnitOfMeasurement + }), + Builders.Update.Set(r => r.WorkCategoryMaster, new WorkCategoryMasterMongoDB + { + Id = workCategory.Id.ToString(), + Name = workCategory.Name, + Description = workCategory.Description, + }) + ); + var options = new UpdateOptions { IsUpsert = true }; + var result = await _taskCollection.UpdateOneAsync(filter, updates, options); + } + } + public async Task> GetWorkItemDetailsByWorkAreaFromCache(Guid workAreaId) + { + var filter = Builders.Filter.Eq(p => p.WorkAreaId, workAreaId.ToString()); + + var options = new UpdateOptions { IsUpsert = true }; + var workItems = await _taskCollection + .Find(filter) + .ToListAsync(); + return workItems; + } + public async Task GetWorkItemDetailsByIdFromCache(Guid id) + { + var filter = Builders.Filter.Eq(p => p.Id, id.ToString()); + + var options = new UpdateOptions { IsUpsert = true }; + var workItem = await _taskCollection + .Find(filter) + .FirstOrDefaultAsync(); + return workItem; + } + public async Task UpdatePlannedAndCompleteWorksInWorkItem(Guid id, double plannedWork = 0, double completedWork = 0, double todaysAssigned = 0) + { + var filter = Builders.Filter.Eq(p => p.Id, id.ToString()); + var updates = Builders.Update + .Inc("PlannedWork", plannedWork) + .Inc("CompletedWork", completedWork) + .Inc("TodaysAssigned", todaysAssigned); + + var result = await _taskCollection.UpdateOneAsync(filter, updates); + if (result.ModifiedCount > 0) + { + return true; + } + return false; + } } } diff --git a/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs index 37218b7..cc77d96 100644 --- a/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs @@ -2,7 +2,7 @@ { public class ActivityMasterMongoDB { - public string? Id { get; set; } + public string Id { get; set; } = string.Empty; public string? ActivityName { get; set; } public string? UnitOfMeasurement { get; set; } } diff --git a/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs b/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs index 87ccb8d..64ccbce 100644 --- a/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs @@ -7,7 +7,7 @@ public string? Description { get; set; } public double PlannedWork { get; set; } public double CompletedWork { get; set; } - public List? Floors { get; set; } + public List Floors { get; set; } = new List(); } public class BuildingMongoDBVM { diff --git a/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs b/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs index ae3975f..57257a4 100644 --- a/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs @@ -6,7 +6,7 @@ public string? FloorName { get; set; } public double PlannedWork { get; set; } public double CompletedWork { get; set; } - public List? WorkAreas { get; set; } + public List WorkAreas { get; set; } = new List(); } public class FloorMongoDBVM diff --git a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs index 8b1612c..7f3a557 100644 --- a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs @@ -7,7 +7,7 @@ public string? ShortName { get; set; } public string? ProjectAddress { get; set; } public string? ContactPerson { get; set; } - public List? Buildings { get; set; } + public List Buildings { get; set; } = new List(); public DateTime? StartDate { get; set; } public DateTime? EndDate { get; set; } public StatusMasterMongoDB? ProjectStatus { get; set; } diff --git a/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs index aef0ada..4ea4682 100644 --- a/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs @@ -2,7 +2,7 @@ { public class WorkCategoryMasterMongoDB { - public string? Id { get; set; } + public string Id { get; set; } = string.Empty; public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; } diff --git a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs index 71638a3..850300d 100644 --- a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs @@ -6,7 +6,7 @@ public string WorkAreaId { get; set; } = string.Empty; public ActivityMasterMongoDB? ActivityMaster { get; set; } public WorkCategoryMasterMongoDB? WorkCategoryMaster { get; set; } - public string? ParentTaskId { get; set; } + public string? ParentTaskId { get; set; } = null; public double PlannedWork { get; set; } = 0; public double TodaysAssigned { get; set; } = 0; public double CompletedWork { get; set; } = 0; diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index a440c21..3ae76ed 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -734,16 +734,45 @@ namespace MarcoBMS.Services.Controllers } // Step 4: Fetch WorkItems with related Activity and Work Category data - var workItems = await _context.WorkItems - .Include(wi => wi.ActivityMaster) - .Include(wi => wi.WorkCategoryMaster) - .Where(wi => wi.WorkAreaId == workAreaId) - .ToListAsync(); + var workItemVMs = await _cache.GetWorkItemDetailsByWorkArea(workAreaId); + if (workItemVMs == null) + { + var workItems = await _context.WorkItems + .Include(wi => wi.ActivityMaster) + .Include(wi => wi.WorkCategoryMaster) + .Where(wi => wi.WorkAreaId == workAreaId) + .ToListAsync(); - _logger.LogInfo("{Count} work items fetched successfully for WorkAreaId: {WorkAreaId}", workItems.Count, workAreaId); + workItemVMs = workItems.Select(wi => new WorkItemMongoDB + { + Id = wi.Id.ToString(), + WorkAreaId = wi.WorkAreaId.ToString(), + ParentTaskId = wi.ParentTaskId.ToString(), + ActivityMaster = new ActivityMasterMongoDB + { + Id = wi.ActivityId.ToString(), + ActivityName = wi.ActivityMaster != null ? wi.ActivityMaster.ActivityName : null, + UnitOfMeasurement = wi.ActivityMaster != null ? wi.ActivityMaster.UnitOfMeasurement : null + }, + WorkCategoryMaster = new WorkCategoryMasterMongoDB + { + Id = wi.ActivityId.ToString(), + Name = wi.WorkCategoryMaster != null ? wi.WorkCategoryMaster.Name : "", + Description = wi.WorkCategoryMaster != null ? wi.WorkCategoryMaster.Description : "" + }, + PlannedWork = wi.PlannedWork, + CompletedWork = wi.CompletedWork, + Description = wi.Description, + TaskDate = wi.TaskDate, + }).ToList(); + + await _cache.ManageWorkItemDetails(workItems); + } + + _logger.LogInfo("{Count} work items fetched successfully for WorkAreaId: {WorkAreaId}", workItemVMs.Count, workAreaId); // Step 5: Return result - return Ok(ApiResponse.SuccessResponse(workItems, $"{workItems.Count} records of tasks fetched successfully", 200)); + return Ok(ApiResponse.SuccessResponse(workItemVMs, $"{workItemVMs.Count} records of tasks fetched successfully", 200)); } [HttpPost("task")] @@ -765,6 +794,8 @@ namespace MarcoBMS.Services.Controllers var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); string message = ""; List projectIds = new List(); + var workItemIds = workItemDtos.Where(wi => wi.Id != null && wi.Id != Guid.Empty).Select(wi => wi.Id).ToList(); + var workItems = await _context.WorkItems.AsNoTracking().Where(wi => workItemIds.Contains(wi.Id)).ToListAsync(); foreach (var itemDto in workItemDtos) { @@ -778,6 +809,28 @@ namespace MarcoBMS.Services.Controllers // Update existing workItemsToUpdate.Add(workItem); message = $"Task Updated in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}"; + var existingWorkItem = workItems.FirstOrDefault(wi => wi.Id == workItem.Id); + double plannedWork = 0; + double completedWork = 0; + if (existingWorkItem != null) + { + if (existingWorkItem.PlannedWork != workItem.PlannedWork && existingWorkItem.CompletedWork != workItem.CompletedWork) + { + plannedWork = workItem.PlannedWork - existingWorkItem.PlannedWork; + completedWork = workItem.CompletedWork - existingWorkItem.CompletedWork; + } + else if (existingWorkItem.PlannedWork == workItem.PlannedWork && existingWorkItem.CompletedWork != workItem.CompletedWork) + { + plannedWork = 0; + completedWork = workItem.CompletedWork - existingWorkItem.CompletedWork; + } + else if (existingWorkItem.PlannedWork != workItem.PlannedWork && existingWorkItem.CompletedWork == workItem.CompletedWork) + { + plannedWork = workItem.PlannedWork - existingWorkItem.PlannedWork; + completedWork = 0; + } + await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, plannedWork, completedWork); + } } else { @@ -785,6 +838,7 @@ namespace MarcoBMS.Services.Controllers workItem.Id = Guid.NewGuid(); workItemsToCreate.Add(workItem); message = $"Task Added in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}"; + await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, workItem.PlannedWork, workItem.CompletedWork); } responseList.Add(new WorkItemVM @@ -793,6 +847,7 @@ namespace MarcoBMS.Services.Controllers WorkItem = workItem }); projectIds.Add(building.ProjectId); + } string responseMessage = ""; // Apply DB changes @@ -801,7 +856,7 @@ namespace MarcoBMS.Services.Controllers _logger.LogInfo("Adding {Count} new work items", workItemsToCreate.Count); await _context.WorkItems.AddRangeAsync(workItemsToCreate); responseMessage = "Task Added Successfully"; - + await _cache.ManageWorkItemDetails(workItemsToCreate); } if (workItemsToUpdate.Any()) @@ -809,7 +864,7 @@ namespace MarcoBMS.Services.Controllers _logger.LogInfo("Updating {Count} existing work items", workItemsToUpdate.Count); _context.WorkItems.UpdateRange(workItemsToUpdate); responseMessage = "Task Updated Successfully"; - + await _cache.ManageWorkItemDetails(workItemsToUpdate); } await _context.SaveChangesAsync(); diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 6ff9cfe..ecce8ab 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -109,6 +109,71 @@ namespace Marco.Pms.Services.Helpers return null; } } + public async Task UpdatePlannedAndCompleteWorksInBuilding(Guid workAreaId, double plannedWork = 0, double completedWork = 0) + { + try + { + await _projectCache.UpdatePlannedAndCompleteWorksInBuildingFromCache(workAreaId, plannedWork, completedWork); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while updating planned work and completed work in building infra form Cache: {Error}", ex.Message); + } + } + + // ------------------------------------------------------- WorkItem ------------------------------------------------------- + + public async Task ManageWorkItemDetails(List workItems) + { + try + { + await _projectCache.ManageWorkItemDetailsToCache(workItems); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while saving workItems form Cache: {Error}", ex.Message); + } + } + public async Task?> GetWorkItemDetailsByWorkArea(Guid workAreaId) + { + try + { + var workItems = await _projectCache.GetWorkItemDetailsByWorkAreaFromCache(workAreaId); + if (workItems.Count > 0) + { + return workItems; + } + else + { + return null; + } + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while fetching list of workItems form Cache: {Error}", ex.Message); + return null; + } + } + public async Task GetWorkItemDetailsById(Guid id) + { + try + { + var workItem = await _projectCache.GetWorkItemDetailsByIdFromCache(id); + if (workItem.Id != "") + { + return workItem; + } + else + { + return null; + } + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while fetching list of workItems form Cache: {Error}", ex.Message); + return null; + } + } // ------------------------------------ Employee Profile Cache --------------------------------------- diff --git a/Marco.Pms.Services/appsettings.Development.json b/Marco.Pms.Services/appsettings.Development.json index 5f5e19d..030c450 100644 --- a/Marco.Pms.Services/appsettings.Development.json +++ b/Marco.Pms.Services/appsettings.Development.json @@ -48,6 +48,6 @@ }, "MongoDB": { "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs", - "ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches" + "ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches?socketTimeoutMS=500&serverSelectionTimeoutMS=500&connectTimeoutMS=500" } } From 67c8bee2c2624414ebf285d85786bc198501cbaa Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 8 Jul 2025 12:20:54 +0530 Subject: [PATCH 031/124] Implemented the cache in task allocation --- Marco.Pms.CacheHelper/ProjectCache.cs | 4 +--- Marco.Pms.Services/Controllers/ProjectController.cs | 4 ++-- Marco.Pms.Services/Controllers/TaskController.cs | 12 +++++++++++- Marco.Pms.Services/Helpers/CacheUpdateHelper.cs | 11 +++++++++++ 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index 6f5a3d3..23df64c 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -26,8 +26,6 @@ namespace Marco.Pms.CacheHelper } public async Task AddProjectDetailsToCache(Project project) { - - //_logger.LogInfo("[AddProjectDetails] Initiated for ProjectId: {ProjectId}", project.Id); var projectDetails = new ProjectMongoDB @@ -544,7 +542,7 @@ namespace Marco.Pms.CacheHelper .FirstOrDefaultAsync(); return workItem; } - public async Task UpdatePlannedAndCompleteWorksInWorkItem(Guid id, double plannedWork = 0, double completedWork = 0, double todaysAssigned = 0) + public async Task UpdatePlannedAndCompleteWorksInWorkItemToCache(Guid id, double plannedWork, double completedWork, double todaysAssigned) { var filter = Builders.Filter.Eq(p => p.Id, id.ToString()); var updates = Builders.Update diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 3ae76ed..e12d2ad 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -29,7 +29,7 @@ namespace MarcoBMS.Services.Controllers private readonly ApplicationDbContext _context; private readonly UserHelper _userHelper; private readonly ILoggingService _logger; - private readonly RolesHelper _rolesHelper; + //private readonly RolesHelper _rolesHelper; private readonly ProjectsHelper _projectsHelper; private readonly IHubContext _signalR; private readonly PermissionServices _permission; @@ -47,7 +47,7 @@ namespace MarcoBMS.Services.Controllers _context = context; _userHelper = userHelper; _logger = logger; - _rolesHelper = rolesHelper; + //_rolesHelper = rolesHelper; _projectsHelper = projectHelper; _signalR = signalR; _cache = cache; diff --git a/Marco.Pms.Services/Controllers/TaskController.cs b/Marco.Pms.Services/Controllers/TaskController.cs index 6b55c3f..4a89e19 100644 --- a/Marco.Pms.Services/Controllers/TaskController.cs +++ b/Marco.Pms.Services/Controllers/TaskController.cs @@ -6,6 +6,7 @@ using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Activities; +using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Service; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; @@ -28,16 +29,18 @@ namespace MarcoBMS.Services.Controllers private readonly S3UploadService _s3Service; private readonly ILoggingService _logger; private readonly PermissionServices _permissionServices; + private readonly CacheUpdateHelper _cache; private readonly Guid Approve_Task; private readonly Guid Assign_Report_Task; - public TaskController(ApplicationDbContext context, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permissionServices) + public TaskController(ApplicationDbContext context, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permissionServices, CacheUpdateHelper cache) { _context = context; _userHelper = userHelper; _s3Service = s3Service; _logger = logger; _permissionServices = permissionServices; + _cache = cache; Approve_Task = Guid.Parse("db4e40c5-2ba9-4b6d-b8a6-a16a250ff99c"); Assign_Report_Task = Guid.Parse("6a32379b-8b3f-49a6-8c48-4b7ac1b55dc2"); } @@ -81,6 +84,8 @@ namespace MarcoBMS.Services.Controllers _context.TaskAllocations.Add(taskAllocation); await _context.SaveChangesAsync(); + await _cache.UpdatePlannedAndCompleteWorksInWorkItem(taskAllocation.WorkItemId, todaysAssigned: taskAllocation.PlannedTask); + _logger.LogInfo("Task {TaskId} assigned by Employee {EmployeeId}", taskAllocation.Id, employee.Id); var response = taskAllocation.ToAssignTaskVMFromTaskAllocation(); @@ -245,6 +250,10 @@ namespace MarcoBMS.Services.Controllers } await _context.SaveChangesAsync(); + var selectedWorkAreaId = taskAllocation.WorkItem?.WorkAreaId ?? Guid.Empty; + + await _cache.UpdatePlannedAndCompleteWorksInWorkItem(taskAllocation.WorkItemId, completedWork: taskAllocation.CompletedTask); + await _cache.UpdatePlannedAndCompleteWorksInBuilding(selectedWorkAreaId, completedWork: taskAllocation.CompletedTask); var response = taskAllocation.ToReportTaskVMFromTaskAllocation(); var comments = await _context.TaskComments @@ -653,6 +662,7 @@ namespace MarcoBMS.Services.Controllers /// /// DTO containing task approval details. /// IActionResult indicating success or failure. + [HttpPost("approve")] public async Task ApproveTask(ApproveTaskDto approveTask) { diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index ecce8ab..03fd397 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -174,6 +174,17 @@ namespace Marco.Pms.Services.Helpers return null; } } + public async Task UpdatePlannedAndCompleteWorksInWorkItem(Guid id, double plannedWork = 0, double completedWork = 0, double todaysAssigned = 0) + { + try + { + var response = await _projectCache.UpdatePlannedAndCompleteWorksInWorkItemToCache(id, plannedWork, completedWork, todaysAssigned); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while updating planned work, completed work, and today's assigned work in workItems in Cache: {Error}", ex.Message); + } + } // ------------------------------------ Employee Profile Cache --------------------------------------- From 3dd5e7f626bcad948e868ac35e3e546435e61275 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 1 Jul 2025 12:39:07 +0530 Subject: [PATCH 032/124] project details API is split into three APIs. --- .../ViewModels/Projects/ProjectVM.cs | 13 +- .../Controllers/ProjectController.cs | 281 +++++++++++------- 2 files changed, 179 insertions(+), 115 deletions(-) diff --git a/Marco.Pms.Model/ViewModels/Projects/ProjectVM.cs b/Marco.Pms.Model/ViewModels/Projects/ProjectVM.cs index cd349bb..240b35f 100644 --- a/Marco.Pms.Model/ViewModels/Projects/ProjectVM.cs +++ b/Marco.Pms.Model/ViewModels/Projects/ProjectVM.cs @@ -1,10 +1,17 @@ -using Marco.Pms.Model.Dtos.Project; +using Marco.Pms.Model.Master; namespace Marco.Pms.Model.ViewModels.Projects { - public class ProjectVM : ProjectDto + public class ProjectVM { - public List? Buildings { get; set; } + public Guid Id { get; set; } + public string? Name { get; set; } + public string? ShortName { get; set; } + public string? ProjectAddress { get; set; } + public string? ContactPerson { get; set; } + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + public StatusMaster? ProjectStatus { get; set; } } } diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 6b83a6c..6490c54 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -1,14 +1,13 @@ 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.Projects; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Model.ViewModels.Projects; using Marco.Pms.Services.Hubs; +using Marco.Pms.Services.Service; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; @@ -29,9 +28,16 @@ namespace MarcoBMS.Services.Controllers private readonly RolesHelper _rolesHelper; private readonly ProjectsHelper _projectsHelper; private readonly IHubContext _signalR; + private readonly PermissionServices _permission; + private readonly Guid ViewProjects; + private readonly Guid ManageProject; + private readonly Guid ViewInfra; + private readonly Guid ManageInfra; + private readonly Guid tenantId; - public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, ProjectsHelper projectHelper, IHubContext signalR) + public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, ProjectsHelper projectHelper, + IHubContext signalR, PermissionServices permission) { _context = context; _userHelper = userHelper; @@ -39,6 +45,12 @@ namespace MarcoBMS.Services.Controllers _rolesHelper = rolesHelper; _projectsHelper = projectHelper; _signalR = signalR; + _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"); + tenantId = _userHelper.GetTenantId(); } @@ -177,133 +189,68 @@ namespace MarcoBMS.Services.Controllers [HttpGet("details/{id}")] public async Task Details([FromRoute] Guid id) { - // ProjectDetailsVM vm = new ProjectDetailsVM(); - + // Step 1: Validate model state if (!ModelState.IsValid) { var errors = ModelState.Values .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); + _logger.LogWarning("Invalid model state in Details endpoint. Errors: {@Errors}", errors); + return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } - var project = await _context.Projects.Where(c => c.TenantId == _userHelper.GetTenantId() && c.Id == id).Include(c => c.ProjectStatus).SingleOrDefaultAsync(); // includeProperties: "ProjectStatus,Tenant"); //_context.Stock.FindAsync(id); + // Step 2: Get logged-in employee + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + _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); + if (!hasViewProjectPermission) + { + _logger.LogWarning("ViewProjects permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); + return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have permission to view projects", 403)); + } + + // Step 4: Check permission for this specific project + var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, id.ToString()); + if (!hasProjectPermission) + { + _logger.LogWarning("Project-specific access denied. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployee.Id, id); + return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have access to this project", 403)); + } + + // Step 5: Fetch project with status + var project = await _context.Projects + .Include(c => c.ProjectStatus) + .FirstOrDefaultAsync(c => c.TenantId == tenantId && c.Id == id); if (project == null) { + _logger.LogWarning("Project not found. ProjectId: {ProjectId}", id); return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); - - } - else - { - //var project = projects.Where(c => c.Id == id).SingleOrDefault(); - ProjectDetailsVM vm = await GetProjectViewModel(id, project); - - ProjectVM projectVM = new ProjectVM(); - if (vm.project != null) - { - projectVM.Id = vm.project.Id; - projectVM.Name = vm.project.Name; - projectVM.ShortName = vm.project.ShortName; - projectVM.ProjectAddress = vm.project.ProjectAddress; - projectVM.ContactPerson = vm.project.ContactPerson; - projectVM.StartDate = vm.project.StartDate; - projectVM.EndDate = vm.project.EndDate; - projectVM.ProjectStatusId = vm.project.ProjectStatusId; - } - projectVM.Buildings = new List(); - if (vm.buildings != null) - { - foreach (Building build in vm.buildings) - { - BuildingVM buildVM = new BuildingVM() { Id = build.Id, Description = build.Description, Name = build.Name }; - buildVM.Floors = new List(); - if (vm.floors != null) - { - foreach (Floor floorDto in vm.floors.Where(c => c.BuildingId == build.Id).ToList()) - { - FloorsVM floorVM = new FloorsVM() { FloorName = floorDto.FloorName, Id = floorDto.Id }; - floorVM.WorkAreas = new List(); - - if (vm.workAreas != null) - { - foreach (WorkArea workAreaDto in vm.workAreas.Where(c => c.FloorId == floorVM.Id).ToList()) - { - WorkAreaVM workAreaVM = new WorkAreaVM() { Id = workAreaDto.Id, AreaName = workAreaDto.AreaName, WorkItems = new List() }; - - if (vm.workItems != null) - { - foreach (WorkItem workItemDto in vm.workItems.Where(c => c.WorkAreaId == workAreaDto.Id).ToList()) - { - WorkItemVM workItemVM = new WorkItemVM() { WorkItemId = workItemDto.Id, WorkItem = workItemDto }; - - workItemVM.WorkItem.WorkArea = new WorkArea(); - - if (workItemVM.WorkItem.ActivityMaster != null) - { - workItemVM.WorkItem.ActivityMaster.Tenant = new Tenant(); - } - workItemVM.WorkItem.Tenant = new Tenant(); - - double todaysAssigned = 0; - if (vm.Tasks != null) - { - var tasks = vm.Tasks.Where(t => t.WorkItemId == workItemDto.Id).ToList(); - foreach (TaskAllocation task in tasks) - { - todaysAssigned += task.PlannedTask; - } - } - workItemVM.TodaysAssigned = todaysAssigned; - - workAreaVM.WorkItems.Add(workItemVM); - } - } - - floorVM.WorkAreas.Add(workAreaVM); - } - } - - buildVM.Floors.Add(floorVM); - } - } - projectVM.Buildings.Add(buildVM); - } - } - return Ok(ApiResponse.SuccessResponse(projectVM, "Success.", 200)); } - + // Step 6: Map and return result + var projectVM = GetProjectViewModel(project); + _logger.LogInfo("Project details fetched successfully. ProjectId: {ProjectId}", id); + return Ok(ApiResponse.SuccessResponse(projectVM, "Project details fetched successfully", 200)); } - private async Task GetProjectViewModel(Guid? id, Project project) + private ProjectVM GetProjectViewModel(Project project) { - ProjectDetailsVM vm = new ProjectDetailsVM(); - - // List buildings = _unitOfWork.Building.GetAll(c => c.ProjectId == id).ToList(); - List buildings = await _context.Buildings.Where(c => c.ProjectId == id).ToListAsync(); - List idList = buildings.Select(o => o.Id).ToList(); - // List floors = _unitOfWork.Floor.GetAll(c => idList.Contains(c.Id)).ToList(); - List floors = await _context.Floor.Where(c => idList.Contains(c.BuildingId)).ToListAsync(); - idList = floors.Select(o => o.Id).ToList(); - //List workAreas = _unitOfWork.WorkArea.GetAll(c => idList.Contains(c.Id), includeProperties: "WorkItems,WorkItems.ActivityMaster").ToList(); - - List workAreas = await _context.WorkAreas.Where(c => idList.Contains(c.FloorId)).ToListAsync(); - - idList = workAreas.Select(o => o.Id).ToList(); - List workItems = await _context.WorkItems.Include(c => c.WorkCategoryMaster).Where(c => idList.Contains(c.WorkAreaId)).Include(c => c.ActivityMaster).ToListAsync(); - // List workItems = _unitOfWork.WorkItem.GetAll(c => idList.Contains(c.WorkAreaId), includeProperties: "ActivityMaster").ToList(); - idList = workItems.Select(t => t.Id).ToList(); - List tasks = await _context.TaskAllocations.Where(t => idList.Contains(t.WorkItemId) && t.AssignmentDate.Date == DateTime.UtcNow.Date).ToListAsync(); - vm.project = project; - vm.buildings = buildings; - vm.floors = floors; - vm.workAreas = workAreas; - vm.workItems = workItems; - vm.Tasks = tasks; - return vm; + 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, + }; } private Guid GetTenantId() @@ -594,6 +541,116 @@ namespace MarcoBMS.Services.Controllers } + + [HttpGet("infra-details/{projectId}")] + public async Task GetInfraDetails(Guid projectId) + { + _logger.LogInfo("GetInfraDetails called for ProjectId: {ProjectId}", projectId); + + // Step 1: Get logged-in employee + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // Step 2: Check project-specific permission + var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.ToString()); + if (!hasProjectPermission) + { + _logger.LogWarning("Project access denied for EmployeeId: {EmployeeId} on ProjectId: {ProjectId}", loggedInEmployee.Id, projectId); + return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have access to this project", 403)); + } + + // Step 3: Check 'ViewInfra' permission + var hasViewInfraPermission = await _permission.HasPermission(ViewInfra, loggedInEmployee.Id); + if (!hasViewInfraPermission) + { + _logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); + return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have access to view infra", 403)); + } + + // Step 4: Fetch buildings for the project + var buildings = await _context.Buildings + .Where(b => b.ProjectId == projectId) + .ToListAsync(); + + var buildingIds = buildings.Select(b => b.Id).ToList(); + + // Step 5: Fetch floors associated with the buildings + var floors = await _context.Floor + .Where(f => buildingIds.Contains(f.BuildingId)) + .ToListAsync(); + + var floorIds = floors.Select(f => f.Id).ToList(); + + // Step 6: Fetch work areas associated with the floors + var workAreas = await _context.WorkAreas + .Where(wa => floorIds.Contains(wa.FloorId)) + .ToListAsync(); + + // Step 7: Build the infra hierarchy (Building > Floors > Work Areas) + var infraVM = buildings.Select(b => + { + var selectedFloors = floors + .Where(f => f.BuildingId == b.Id) + .Select(f => new + { + Id = f.Id, + FloorName = f.FloorName, + WorkAreas = workAreas + .Where(wa => wa.FloorId == f.Id) + .Select(wa => new { wa.Id, wa.AreaName }) + .ToList() + }).ToList(); + + return new + { + Id = b.Id, + BuildingName = b.Name, + Floors = selectedFloors + }; + }).ToList(); + + _logger.LogInfo("Infra details fetched successfully for ProjectId: {ProjectId}, EmployeeId: {EmployeeId}, Buildings: {Count}", + projectId, loggedInEmployee.Id, infraVM.Count); + + return Ok(ApiResponse.SuccessResponse(infraVM, "Infra details fetched successfully", 200)); + } + + [HttpGet("tasks/{workAreaId}")] + public async Task GetWorkItems(Guid workAreaId) + { + _logger.LogInfo("GetWorkItems called for WorkAreaId: {WorkAreaId}", workAreaId); + + // Step 1: Get the currently logged-in employee + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // Step 2: Check if the employee has ViewInfra permission + var hasViewInfraPermission = await _permission.HasPermission(ViewInfra, loggedInEmployee.Id); + if (!hasViewInfraPermission) + { + _logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); + return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have permission to view infrastructure", 403)); + } + + // Step 3: Check if the specified Work Area exists + var isWorkAreaExist = await _context.WorkAreas.AnyAsync(wa => wa.Id == workAreaId); + if (!isWorkAreaExist) + { + _logger.LogWarning("Work Area not found for WorkAreaId: {WorkAreaId}", workAreaId); + return NotFound(ApiResponse.ErrorResponse("Work Area not found", "Work Area not found in database", 404)); + } + + // Step 4: Fetch WorkItems with related Activity and Work Category data + var workItems = await _context.WorkItems + .Include(wi => wi.ActivityMaster) + .Include(wi => wi.WorkCategoryMaster) + .Where(wi => wi.WorkAreaId == workAreaId) + .ToListAsync(); + + _logger.LogInfo("{Count} work items fetched successfully for WorkAreaId: {WorkAreaId}", workItems.Count, workAreaId); + + // Step 5: Return result + return Ok(ApiResponse.SuccessResponse(workItems, $"{workItems.Count} records of tasks fetched successfully", 200)); + } + [HttpPost("task")] public async Task CreateProjectTask(List workItemDtos) { From 3ce9851a7f3914722d7b26863464a87bf9e6c98e Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Fri, 4 Jul 2025 17:49:25 +0530 Subject: [PATCH 033/124] Saving project details with infrastructure, employee permissions and assigned project for that employee in mongodb --- Marco.Pms.CacheHelper/EmployeeCache.cs | 158 +++++++ .../Marco.Pms.CacheHelper.csproj | 18 + Marco.Pms.CacheHelper/ProjectCache.cs | 434 ++++++++++++++++++ Marco.Pms.Model/Marco.Pms.Model.csproj | 1 + .../MongoDBModels/ActivityMasterMongoDB.cs | 9 + .../MongoDBModels/BuildingMongoDB.cs | 18 + .../EmployeePermissionMongoDB.cs | 13 + Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs | 17 + .../MongoDBModels/ProjectMongoDB.cs | 18 + .../MongoDBModels/StatusMasterMongoDB.cs | 8 + .../MongoDBModels/WorkAreaMongoDB.cs | 15 + .../WorkCategoryMasterMongoDB.cs | 9 + .../MongoDBModels/WorkItemMongoDB.cs | 15 + .../Controllers/ProjectController.cs | 221 +++++++-- .../Controllers/RolesController.cs | 12 +- Marco.Pms.Services/Dockerfile | 1 + .../Helpers/CacheUpdateHelper.cs | 98 ++++ Marco.Pms.Services/Helpers/ProjectsHelper.cs | 69 +-- Marco.Pms.Services/Helpers/RolesHelper.cs | 7 +- Marco.Pms.Services/Marco.Pms.Services.csproj | 1 + Marco.Pms.Services/Program.cs | 6 +- .../Service/PermissionServices.cs | 18 +- .../appsettings.Development.json | 4 +- .../appsettings.Production.json | 5 +- marco.pms.api.sln | 6 + 25 files changed, 1090 insertions(+), 91 deletions(-) create mode 100644 Marco.Pms.CacheHelper/EmployeeCache.cs create mode 100644 Marco.Pms.CacheHelper/Marco.Pms.CacheHelper.csproj create mode 100644 Marco.Pms.CacheHelper/ProjectCache.cs create mode 100644 Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs create mode 100644 Marco.Pms.Services/Helpers/CacheUpdateHelper.cs diff --git a/Marco.Pms.CacheHelper/EmployeeCache.cs b/Marco.Pms.CacheHelper/EmployeeCache.cs new file mode 100644 index 0000000..7d75407 --- /dev/null +++ b/Marco.Pms.CacheHelper/EmployeeCache.cs @@ -0,0 +1,158 @@ +using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.MongoDBModels; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using MongoDB.Driver; + +namespace Marco.Pms.CacheHelper +{ + public class EmployeeCache + { + private readonly ApplicationDbContext _context; + //private readonly IMongoDatabase _mongoDB; + private readonly IMongoCollection _collection; + public EmployeeCache(ApplicationDbContext context, IConfiguration configuration) + { + var connectionString = configuration["MongoDB:ConnectionString"]; + _context = context; + var mongoUrl = new MongoUrl(connectionString); + var client = new MongoClient(mongoUrl); // Your MongoDB connection string + var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name + _collection = mongoDB.GetCollection("EmployeeProfile"); + } + public async Task AddApplicationRoleToCache(Guid employeeId, List roleIds) + { + var newRoleIds = roleIds.Select(r => r.ToString()).ToList(); + var newPermissionIds = await _context.RolePermissionMappings + .Where(rp => roleIds.Contains(rp.ApplicationRoleId)) + .Select(p => p.FeaturePermissionId.ToString()) + .Distinct() + .ToListAsync(); + + var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + + var update = Builders.Update + .AddToSetEach(e => e.ApplicationRoleIds, newRoleIds) + .AddToSetEach(e => e.PermissionIds, newPermissionIds); + + var result = await _collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); + if (result.MatchedCount == 0) + { + return false; + } + return true; + } + public async Task AddProjectsToCache(Guid employeeId, List projectIds) + { + var newprojectIds = projectIds.Select(p => p.ToString()).ToList(); + + var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + + var update = Builders.Update + .AddToSetEach(e => e.ProjectIds, newprojectIds); + + var result = await _collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); + if (result.MatchedCount == 0) + { + return false; + } + return true; + } + public async Task> GetProjectsFromCache(Guid employeeId) + { + var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + + + var result = await _collection + .Find(filter) + .FirstOrDefaultAsync(); + + var projectIds = new List(); + if (result != null) + { + projectIds = result.ProjectIds.Select(Guid.Parse).ToList(); + } + + return projectIds; + } + public async Task> GetPermissionsFromCache(Guid employeeId) + { + var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + + + var result = await _collection + .Find(filter) + .FirstOrDefaultAsync(); + + var permissionIds = new List(); + if (result != null) + { + permissionIds = result.PermissionIds.Select(Guid.Parse).ToList(); + } + + return permissionIds; + } + public async Task ClearAllProjectIdsFromCache(Guid employeeId) + { + var filter = Builders.Filter + .Eq(e => e.EmployeeId, employeeId.ToString()); + + var update = Builders.Update + .Set(e => e.ProjectIds, new List()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + return true; + } + public async Task RemoveRoleIdFromCache(Guid employeeId, Guid roleId) + { + var filter = Builders.Filter + .Eq(e => e.EmployeeId, employeeId.ToString()); + + var update = Builders.Update + .Pull(e => e.ApplicationRoleIds, roleId.ToString()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + if (result.ModifiedCount == 0) + return false; + + return true; + } + public async Task ClearAllPermissionIdsByEmployeeIDFromCache(Guid employeeId) + { + var filter = Builders.Filter + .Eq(e => e.EmployeeId, employeeId.ToString()); + + var update = Builders.Update + .Set(e => e.PermissionIds, new List()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + return true; + } + public async Task ClearAllPermissionIdsByRoleIdFromCache(Guid roleId) + { + var filter = Builders.Filter.AnyEq(e => e.ApplicationRoleIds, roleId.ToString()); + + var update = Builders.Update + .Set(e => e.PermissionIds, new List()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + return true; + } + } +} diff --git a/Marco.Pms.CacheHelper/Marco.Pms.CacheHelper.csproj b/Marco.Pms.CacheHelper/Marco.Pms.CacheHelper.csproj new file mode 100644 index 0000000..e12ac6c --- /dev/null +++ b/Marco.Pms.CacheHelper/Marco.Pms.CacheHelper.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs new file mode 100644 index 0000000..b667694 --- /dev/null +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -0,0 +1,434 @@ +using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.MongoDBModels; +using Marco.Pms.Model.Projects; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Marco.Pms.CacheHelper +{ + public class ProjectCache + { + private readonly ApplicationDbContext _context; + private readonly IMongoDatabase _mongoDB; + //private readonly ILoggingService _logger; + public ProjectCache(ApplicationDbContext context, IConfiguration configuration) + { + var connectionString = configuration["MongoDB:ConnectionString"]; + _context = context; + var mongoUrl = new MongoUrl(connectionString); + var client = new MongoClient(mongoUrl); // Your MongoDB connection string + _mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name + } + public async Task AddProjectDetailsToCache(Project project) + { + var projectCollection = _mongoDB.GetCollection("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(), + AreaName = wa.AreaName, + PlannedWork = waPlanned, + CompletedWork = waCompleted + }); + + floorPlanned += waPlanned; + floorCompleted += waCompleted; + } + + floorMongoList.Add(new FloorMongoDB + { + Id = floor.Id.ToString(), + FloorName = floor.FloorName, + PlannedWork = floorPlanned, + CompletedWork = floorCompleted, + WorkAreas = workAreaMongoList + }); + + buildingPlanned += floorPlanned; + buildingCompleted += floorCompleted; + } + + buildingMongoList.Add(new BuildingMongoDB + { + Id = building.Id.ToString(), + BuildingName = building.Name, + Description = building.Description, + PlannedWork = buildingPlanned, + CompletedWork = buildingCompleted, + Floors = floorMongoList + }); + + totalPlannedWork += buildingPlanned; + totalCompletedWork += buildingCompleted; + } + + projectDetails.Buildings = buildingMongoList; + projectDetails.PlannedWork = totalPlannedWork; + projectDetails.CompletedWork = totalCompletedWork; + + await projectCollection.InsertOneAsync(projectDetails); + //_logger.LogInfo("[AddProjectDetails] Project details inserted in MongoDB for ProjectId: {ProjectId}", project.Id); + } + public async Task UpdateProjectDetailsOnlyToCache(Project project) + { + //_logger.LogInfo("Starting update for project: {ProjectId}", project.Id); + + var projectStatus = await _context.StatusMasters + .FirstOrDefaultAsync(s => s.Id == project.ProjectStatusId); + + if (projectStatus == null) + { + //_logger.LogWarning("StatusMaster not found for ProjectStatusId: {StatusId}", project.ProjectStatusId); + } + + var projectCollection = _mongoDB.GetCollection("ProjectDetails"); + + // Build the update definition + var updates = Builders.Update.Combine( + Builders.Update.Set(r => r.Name, project.Name), + Builders.Update.Set(r => r.ProjectAddress, project.ProjectAddress), + Builders.Update.Set(r => r.ShortName, project.ShortName), + Builders.Update.Set(r => r.ProjectStatus, new StatusMasterMongoDB + { + Id = projectStatus?.Id.ToString(), + Status = projectStatus?.Status + }), + Builders.Update.Set(r => r.StartDate, project.StartDate), + Builders.Update.Set(r => r.EndDate, project.EndDate), + Builders.Update.Set(r => r.ContactPerson, project.ContactPerson) + ); + + // Perform the update + var result = await projectCollection.UpdateOneAsync( + filter: r => r.Id == project.Id.ToString(), + update: updates + ); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("No project matched in MongoDB for update. ProjectId: {ProjectId}", project.Id); + return false; + } + + //_logger.LogInfo("Project {ProjectId} successfully updated in MongoDB", project.Id); + return true; + } + public async Task GetProjectDetailsFromCache(Guid projectId) + { + var projectCollection = _mongoDB.GetCollection("ProjectDetails"); + + // Build filter and projection to exclude large 'Buildings' list + var filter = Builders.Filter.Eq(p => p.Id, projectId.ToString()); + var projection = Builders.Projection.Exclude(p => p.Buildings); + + //_logger.LogInfo("Fetching project details for ProjectId: {ProjectId} from MongoDB", projectId); + + // Perform query + var project = await projectCollection + .Find(filter) + .Project(projection) + .FirstOrDefaultAsync(); + + if (project == null) + { + //_logger.LogWarning("No project found in MongoDB for ProjectId: {ProjectId}", projectId); + return null; + } + + //// Deserialize the result manually + //var project = BsonSerializer.Deserialize(result); + + //_logger.LogInfo("Successfully fetched project details (excluding Buildings) for ProjectId: {ProjectId}", projectId); + return project; + } + public async Task AddBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId) + { + var stringProjectId = projectId.ToString(); + var projectCollection = _mongoDB.GetCollection("ProjectDetails"); + + // Add Building + if (building != null) + { + var buildingMongo = new BuildingMongoDB + { + Id = building.Id.ToString(), + BuildingName = building.Name, + Description = building.Description, + PlannedWork = 0, + CompletedWork = 0, + Floors = new List() + }; + + var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); + var update = Builders.Update.Push("Buildings", buildingMongo); + + var result = await projectCollection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("Project not found while adding building. ProjectId: {ProjectId}", projectId); + return; + } + + //_logger.LogInfo("Building {BuildingId} added to project {ProjectId}", building.Id, projectId); + return; + } + + // Add Floor + if (floor != null) + { + var floorMongo = new FloorMongoDB + { + Id = floor.Id.ToString(), + FloorName = floor.FloorName, + PlannedWork = 0, + CompletedWork = 0, + WorkAreas = new List() + }; + + var filter = Builders.Filter.And( + Builders.Filter.Eq(p => p.Id, stringProjectId), + Builders.Filter.Eq("Buildings._id", floor.BuildingId.ToString()) + ); + + var update = Builders.Update.Push("Buildings.$.Floors", floorMongo); + var result = await projectCollection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("Project or building not found while adding floor. ProjectId: {ProjectId}, BuildingId: {BuildingId}", projectId, floor.BuildingId); + return; + } + + //_logger.LogInfo("Floor {FloorId} added to building {BuildingId} in project {ProjectId}", floor.Id, floor.BuildingId, projectId); + return; + } + + // Add WorkArea + if (workArea != null && buildingId != null) + { + var workAreaMongo = new WorkAreaMongoDB + { + Id = workArea.Id.ToString(), + AreaName = workArea.AreaName, + PlannedWork = 0, + CompletedWork = 0 + }; + + var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); + + var arrayFilters = new List + { + new JsonArrayFilterDefinition("{ 'b._id': '" + buildingId + "' }"), + new JsonArrayFilterDefinition("{ 'f._id': '" + workArea.FloorId + "' }") + }; + + var update = Builders.Update.Push("Buildings.$[b].Floors.$[f].WorkAreas", workAreaMongo); + var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; + + var result = await projectCollection.UpdateOneAsync(filter, update, updateOptions); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("Project or nested structure not found while adding work area. ProjectId: {ProjectId}, BuildingId: {BuildingId}, FloorId: {FloorId}", projectId, buildingId, workArea.FloorId); + return; + } + + //_logger.LogInfo("WorkArea {WorkAreaId} added to floor {FloorId} in building {BuildingId}, ProjectId: {ProjectId}", workArea.Id, workArea.FloorId, buildingId, projectId); + return; + } + + // Fallback case when no valid data was passed + //_logger.LogWarning("No valid infra data provided to add for ProjectId: {ProjectId}", projectId); + } + public async Task UpdateBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId) + { + var stringProjectId = projectId.ToString(); + var projectCollection = _mongoDB.GetCollection("ProjectDetails"); + + // Update Building + if (building != null) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(p => p.Id, stringProjectId), + Builders.Filter.Eq("Buildings._id", building.Id.ToString()) + ); + + var update = Builders.Update.Combine( + Builders.Update.Set("Buildings.$.BuildingName", building.Name), + Builders.Update.Set("Buildings.$.Description", building.Description) + ); + + var result = await projectCollection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("Update failed: Project or Building not found. ProjectId: {ProjectId}, BuildingId: {BuildingId}", projectId, building.Id); + return false; + } + + //_logger.LogInfo("Building {BuildingId} updated successfully in project {ProjectId}", building.Id, projectId); + return true; + } + + // Update Floor + if (floor != null) + { + var arrayFilters = new List + { + new JsonArrayFilterDefinition("{ 'b._id': '" + floor.BuildingId + "' }"), + new JsonArrayFilterDefinition("{ 'f._id': '" + floor.Id + "' }") + }; + + var update = Builders.Update.Set("Buildings.$[b].Floors.$[f].FloorName", floor.FloorName); + var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; + var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); + + var result = await projectCollection.UpdateOneAsync(filter, update, updateOptions); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("Update failed: Project or Floor not found. ProjectId: {ProjectId}, BuildingId: {BuildingId}, FloorId: {FloorId}", projectId, floor.BuildingId, floor.Id); + return false; + } + + //_logger.LogInfo("Floor {FloorId} updated successfully in Building {BuildingId}, ProjectId: {ProjectId}", floor.Id, floor.BuildingId, projectId); + return true; + } + + // Update WorkArea + if (workArea != null && buildingId != null) + { + var arrayFilters = new List + { + new JsonArrayFilterDefinition("{ 'b._id': '" + buildingId + "' }"), + new JsonArrayFilterDefinition("{ 'f._id': '" + workArea.FloorId + "' }"), + new JsonArrayFilterDefinition("{ 'a._id': '" + workArea.Id + "' }") + }; + + var update = Builders.Update.Set("Buildings.$[b].Floors.$[f].WorkAreas.$[a].AreaName", workArea.AreaName); + var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; + var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); + + var result = await projectCollection.UpdateOneAsync(filter, update, updateOptions); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("Update failed: Project or WorkArea not found. ProjectId: {ProjectId}, BuildingId: {BuildingId}, FloorId: {FloorId}, WorkAreaId: {WorkAreaId}", + //projectId, buildingId, workArea.FloorId, workArea.Id); + return false; + } + + //_logger.LogInfo("WorkArea {WorkAreaId} updated successfully in Floor {FloorId}, Building {BuildingId}, ProjectId: {ProjectId}", + //workArea.Id, workArea.FloorId, buildingId, projectId); + return true; + } + + //_logger.LogWarning("No update performed. Missing or invalid data for ProjectId: {ProjectId}", projectId); + return false; + } + public async Task?> GetBuildingInfraFromCache(Guid projectId) + { + var projectCollection = _mongoDB.GetCollection("ProjectDetails"); + + // Filter by project ID + var filter = Builders.Filter.Eq(p => p.Id, projectId.ToString()); + + // Project only the "Buildings" field from the document + var buildings = await projectCollection + .Find(filter) + .Project(p => p.Buildings) + .FirstOrDefaultAsync(); + + //if (buildings == null) + //{ + // _logger.LogWarning("No building infrastructure found for ProjectId: {ProjectId}", projectId); + //} + //else + //{ + // _logger.LogInfo("Fetched {Count} buildings for ProjectId: {ProjectId}", buildings.Count, projectId); + //} + + return buildings; + } + } +} diff --git a/Marco.Pms.Model/Marco.Pms.Model.csproj b/Marco.Pms.Model/Marco.Pms.Model.csproj index d5927ce..a1a21a5 100644 --- a/Marco.Pms.Model/Marco.Pms.Model.csproj +++ b/Marco.Pms.Model/Marco.Pms.Model.csproj @@ -10,6 +10,7 @@ + diff --git a/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs new file mode 100644 index 0000000..37218b7 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs @@ -0,0 +1,9 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class ActivityMasterMongoDB + { + public string? Id { get; set; } + public string? ActivityName { get; set; } + public string? UnitOfMeasurement { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs b/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs new file mode 100644 index 0000000..87ccb8d --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs @@ -0,0 +1,18 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class BuildingMongoDB + { + public string Id { get; set; } = string.Empty; + public string? BuildingName { get; set; } + public string? Description { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } + public List? Floors { get; set; } + } + public class BuildingMongoDBVM + { + public string Id { get; set; } = string.Empty; + public string? Name { get; set; } + public string? Description { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs b/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs new file mode 100644 index 0000000..f141798 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs @@ -0,0 +1,13 @@ +using MongoDB.Bson.Serialization.Attributes; + +namespace Marco.Pms.Model.MongoDBModels +{ + [BsonIgnoreExtraElements] + public class EmployeePermissionMongoDB + { + public string EmployeeId { get; set; } = string.Empty; + public List ApplicationRoleIds { get; set; } = new List(); + public List PermissionIds { get; set; } = new List(); + public List ProjectIds { get; set; } = new List(); + } +} diff --git a/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs b/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs new file mode 100644 index 0000000..ae3975f --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs @@ -0,0 +1,17 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class FloorMongoDB + { + public string Id { get; set; } = string.Empty; + public string? FloorName { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } + public List? WorkAreas { get; set; } + } + + public class FloorMongoDBVM + { + public string Id { get; set; } = string.Empty; + public string? FloorName { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs new file mode 100644 index 0000000..8bf1c9a --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs @@ -0,0 +1,18 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class ProjectMongoDB + { + public string? Id { get; set; } + public string? Name { get; set; } + public string? ShortName { get; set; } + public string? ProjectAddress { get; set; } + public string? ContactPerson { get; set; } + public List? Buildings { get; set; } + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + public StatusMasterMongoDB? ProjectStatus { get; set; } + public int TeamSize { get; set; } + public double CompletedWork { get; set; } + public double PlannedWork { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs new file mode 100644 index 0000000..01a0552 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs @@ -0,0 +1,8 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class StatusMasterMongoDB + { + public string? Id { get; set; } + public string? Status { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs new file mode 100644 index 0000000..d17f52c --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs @@ -0,0 +1,15 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class WorkAreaMongoDB + { + public string Id { get; set; } = string.Empty; + public string? AreaName { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } + } + public class WorkAreaMongoDBVM + { + public string Id { get; set; } = string.Empty; + public string? AreaName { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs new file mode 100644 index 0000000..aef0ada --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs @@ -0,0 +1,9 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class WorkCategoryMasterMongoDB + { + public string? Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + } +} diff --git a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs new file mode 100644 index 0000000..dc7fdb9 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs @@ -0,0 +1,15 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class WorkItemMongoDB + { + public string? Id { get; set; } + public string? WorkAreaId { get; set; } + public ActivityMasterMongoDB? ActivityMaster { get; set; } + public WorkCategoryMasterMongoDB? WorkCategoryMaster { get; set; } + public string? ParentTaskId { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } + public string? Description { get; set; } + public DateTime TaskDate { get; set; } + } +} diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 6490c54..a440c21 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -2,10 +2,13 @@ using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Employees; 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; using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Model.ViewModels.Projects; +using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Service; using MarcoBMS.Services.Helpers; @@ -14,6 +17,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; +using MongoDB.Driver; namespace MarcoBMS.Services.Controllers { @@ -29,6 +33,7 @@ namespace MarcoBMS.Services.Controllers private readonly ProjectsHelper _projectsHelper; private readonly IHubContext _signalR; private readonly PermissionServices _permission; + private readonly CacheUpdateHelper _cache; private readonly Guid ViewProjects; private readonly Guid ManageProject; private readonly Guid ViewInfra; @@ -37,7 +42,7 @@ namespace MarcoBMS.Services.Controllers public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, ProjectsHelper projectHelper, - IHubContext signalR, PermissionServices permission) + IHubContext signalR, PermissionServices permission, CacheUpdateHelper cache) { _context = context; _userHelper = userHelper; @@ -45,13 +50,13 @@ namespace MarcoBMS.Services.Controllers _rolesHelper = rolesHelper; _projectsHelper = projectHelper; _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"); tenantId = _userHelper.GetTenantId(); - } [HttpGet("list/basic")] @@ -222,24 +227,54 @@ namespace MarcoBMS.Services.Controllers } // Step 5: Fetch project with status - var project = await _context.Projects + var projectDetails = await _cache.GetProjectDetails(id); + ProjectVM? projectVM = null; + if (projectDetails == null) + { + var project = await _context.Projects .Include(c => c.ProjectStatus) .FirstOrDefaultAsync(c => c.TenantId == tenantId && c.Id == id); + projectVM = GetProjectViewModel(project); + } + else + { + 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 (project == null) + if (projectVM == null) { _logger.LogWarning("Project not found. ProjectId: {ProjectId}", id); return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); } - // Step 6: Map and return result - var projectVM = GetProjectViewModel(project); + // Step 6: Return result + _logger.LogInfo("Project details fetched successfully. ProjectId: {ProjectId}", id); return Ok(ApiResponse.SuccessResponse(projectVM, "Project details fetched successfully", 200)); } - private ProjectVM GetProjectViewModel(Project project) + private ProjectVM? GetProjectViewModel(Project? project) { + if (project == null) + { + return null; + } return new ProjectVM { Id = project.Id, @@ -280,6 +315,9 @@ namespace MarcoBMS.Services.Controllers _context.Projects.Add(project); await _context.SaveChangesAsync(); + + await _cache.AddProjectDetails(project); + var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Create_Project", Response = project.ToProjectDto() }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); @@ -310,6 +348,13 @@ namespace MarcoBMS.Services.Controllers await _context.SaveChangesAsync(); + // Cache functions + bool isUpdated = await _cache.UpdateProjectDetailsOnly(project); + if (!isUpdated) + { + await _cache.AddProjectDetails(project); + } + var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Update_Project", Response = project.ToProjectDto() }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); @@ -524,6 +569,7 @@ namespace MarcoBMS.Services.Controllers employeeIds.Add(projectAllocation.EmployeeId); projectIds.Add(projectAllocation.ProjectId); } + await _cache.ClearAllProjectIds(item.EmpID); } catch (Exception ex) @@ -565,53 +611,102 @@ namespace MarcoBMS.Services.Controllers _logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have access to view infra", 403)); } - - // Step 4: Fetch buildings for the project - var buildings = await _context.Buildings - .Where(b => b.ProjectId == projectId) - .ToListAsync(); - - var buildingIds = buildings.Select(b => b.Id).ToList(); - - // Step 5: Fetch floors associated with the buildings - var floors = await _context.Floor - .Where(f => buildingIds.Contains(f.BuildingId)) - .ToListAsync(); - - var floorIds = floors.Select(f => f.Id).ToList(); - - // Step 6: Fetch work areas associated with the floors - var workAreas = await _context.WorkAreas - .Where(wa => floorIds.Contains(wa.FloorId)) - .ToListAsync(); - - // Step 7: Build the infra hierarchy (Building > Floors > Work Areas) - var infraVM = buildings.Select(b => + var result = await _cache.GetBuildingInfra(projectId); + if (result == null) { - var selectedFloors = floors - .Where(f => f.BuildingId == b.Id) - .Select(f => new - { - Id = f.Id, - FloorName = f.FloorName, - WorkAreas = workAreas - .Where(wa => wa.FloorId == f.Id) - .Select(wa => new { wa.Id, wa.AreaName }) - .ToList() - }).ToList(); - return new + // Step 4: Fetch buildings for the project + var buildings = await _context.Buildings + .Where(b => b.ProjectId == projectId) + .ToListAsync(); + + var buildingIds = buildings.Select(b => b.Id).ToList(); + + // Step 5: Fetch floors associated with the buildings + var floors = await _context.Floor + .Where(f => buildingIds.Contains(f.BuildingId)) + .ToListAsync(); + + var floorIds = floors.Select(f => f.Id).ToList(); + + // Step 6: Fetch work areas associated with the floors + var workAreas = await _context.WorkAreas + .Where(wa => floorIds.Contains(wa.FloorId)) + .ToListAsync(); + var workAreaIds = workAreas.Select(wa => wa.Id).ToList(); + + // Step 7: Fetch work items associated with the work area + var workItems = await _context.WorkItems + .Where(wi => workAreaIds.Contains(wi.WorkAreaId)) + .ToListAsync(); + + // Step 8: Build the infra hierarchy (Building > Floors > Work Areas) + List Buildings = new List(); + foreach (var building in buildings) { - Id = b.Id, - BuildingName = b.Name, - Floors = selectedFloors - }; - }).ToList(); + double buildingPlannedWorks = 0; + double buildingCompletedWorks = 0; + + var selectedFloors = floors.Where(f => f.BuildingId == building.Id).ToList(); + List Floors = new List(); + foreach (var floor in selectedFloors) + { + double floorPlannedWorks = 0; + double floorCompletedWorks = 0; + var selectedWorkAreas = workAreas.Where(wa => wa.FloorId == floor.Id).ToList(); + List WorkAreas = new List(); + foreach (var workArea in selectedWorkAreas) + { + double workAreaPlannedWorks = 0; + double workAreaCompletedWorks = 0; + var selectedWorkItems = workItems.Where(wi => wi.WorkAreaId == workArea.Id).ToList(); + foreach (var workItem in selectedWorkItems) + { + workAreaPlannedWorks += workItem.PlannedWork; + workAreaCompletedWorks += workItem.CompletedWork; + } + WorkAreaMongoDB workAreaMongo = new WorkAreaMongoDB + { + Id = workArea.Id.ToString(), + AreaName = workArea.AreaName, + PlannedWork = workAreaPlannedWorks, + CompletedWork = workAreaCompletedWorks + }; + WorkAreas.Add(workAreaMongo); + floorPlannedWorks += workAreaPlannedWorks; + floorCompletedWorks += workAreaCompletedWorks; + } + FloorMongoDB floorMongoDB = new FloorMongoDB + { + Id = floor.Id.ToString(), + FloorName = floor.FloorName, + PlannedWork = floorPlannedWorks, + CompletedWork = floorCompletedWorks, + WorkAreas = WorkAreas + }; + Floors.Add(floorMongoDB); + buildingPlannedWorks += floorPlannedWorks; + buildingCompletedWorks += floorCompletedWorks; + } + + var buildingMongo = new BuildingMongoDB + { + Id = building.Id.ToString(), + BuildingName = building.Name, + Description = building.Description, + PlannedWork = buildingPlannedWorks, + CompletedWork = buildingCompletedWorks, + Floors = Floors + }; + Buildings.Add(buildingMongo); + } + result = Buildings; + } _logger.LogInfo("Infra details fetched successfully for ProjectId: {ProjectId}, EmployeeId: {EmployeeId}, Buildings: {Count}", - projectId, loggedInEmployee.Id, infraVM.Count); + projectId, loggedInEmployee.Id, result.Count); - return Ok(ApiResponse.SuccessResponse(infraVM, "Infra details fetched successfully", 200)); + return Ok(ApiResponse.SuccessResponse(result, "Infra details fetched successfully", 200)); } [HttpGet("tasks/{workAreaId}")] @@ -807,6 +902,7 @@ namespace MarcoBMS.Services.Controllers responseData.building = building; responseMessage = "Buliding Added Successfully"; message = "Building Added"; + await _cache.AddBuildngInfra(building.ProjectId, building); } else { @@ -816,7 +912,7 @@ namespace MarcoBMS.Services.Controllers responseData.building = building; responseMessage = "Buliding Updated Successfully"; message = "Building Updated"; - + await _cache.UpdateBuildngInfra(building.ProjectId, building); } projectIds.Add(building.ProjectId); } @@ -824,6 +920,7 @@ namespace MarcoBMS.Services.Controllers { Floor floor = item.Floor.ToFloorFromFloorDto(tenantId); floor.TenantId = GetTenantId(); + bool isCreated = false; if (item.Floor.Id == null) { @@ -833,6 +930,7 @@ namespace MarcoBMS.Services.Controllers responseData.floor = floor; responseMessage = "Floor Added Successfully"; message = "Floor Added"; + isCreated = true; } else { @@ -844,13 +942,23 @@ namespace MarcoBMS.Services.Controllers message = "Floor Updated"; } Building? building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == floor.BuildingId); - projectIds.Add(building?.ProjectId ?? Guid.Empty); + var projectId = building?.ProjectId ?? Guid.Empty; + projectIds.Add(projectId); message = $"{message} in Building: {building?.Name}"; + if (isCreated) + { + await _cache.AddBuildngInfra(projectId, floor: floor); + } + else + { + await _cache.UpdateBuildngInfra(projectId, floor: floor); + } } if (item.WorkArea != null) { WorkArea workArea = item.WorkArea.ToWorkAreaFromWorkAreaDto(tenantId); workArea.TenantId = GetTenantId(); + bool isCreated = false; if (item.WorkArea.Id == null) { @@ -860,6 +968,7 @@ namespace MarcoBMS.Services.Controllers responseData.workArea = workArea; responseMessage = "Work Area Added Successfully"; message = "Work Area Added"; + isCreated = true; } else { @@ -871,8 +980,17 @@ namespace MarcoBMS.Services.Controllers message = "Work Area Updated"; } Floor? floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == workArea.FloorId); - projectIds.Add(floor?.Building?.ProjectId ?? Guid.Empty); + var projectId = floor?.Building?.ProjectId ?? Guid.Empty; + projectIds.Add(projectId); message = $"{message} in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}"; + if (isCreated) + { + await _cache.AddBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); + } + else + { + await _cache.UpdateBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); + } } } message = $"{message} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}"; @@ -996,6 +1114,7 @@ namespace MarcoBMS.Services.Controllers return Ok(ApiResponse.ErrorResponse(ex.Message, ex, 400)); } } + await _cache.ClearAllProjectIds(employeeId); var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeId = employeeId }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); diff --git a/Marco.Pms.Services/Controllers/RolesController.cs b/Marco.Pms.Services/Controllers/RolesController.cs index 2ac2b07..4c75b3e 100644 --- a/Marco.Pms.Services/Controllers/RolesController.cs +++ b/Marco.Pms.Services/Controllers/RolesController.cs @@ -10,6 +10,7 @@ using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels; using Marco.Pms.Model.ViewModels.Master; using Marco.Pms.Model.ViewModels.Roles; +using Marco.Pms.Services.Helpers; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; @@ -29,14 +30,17 @@ namespace MarcoBMS.Services.Controllers private readonly UserHelper _userHelper; private readonly UserManager _userManager; private readonly ILoggingService _logger; + private readonly CacheUpdateHelper _cache; - public RolesController(UserManager userManager, ApplicationDbContext context, RolesHelper rolesHelper, UserHelper userHelper, ILoggingService logger) + public RolesController(UserManager userManager, ApplicationDbContext context, RolesHelper rolesHelper, UserHelper userHelper, ILoggingService logger, + CacheUpdateHelper cache) { _context = context; _userManager = userManager; _rolesHelper = rolesHelper; _userHelper = userHelper; _logger = logger; + _cache = cache; } private Guid GetTenantId() @@ -292,6 +296,8 @@ namespace MarcoBMS.Services.Controllers if (modified) await _context.SaveChangesAsync(); + await _cache.ClearAllPermissionIdsByRoleId(id); + ApplicationRolesVM response = role.ToRoleVMFromApplicationRole(); List permissions = await _rolesHelper.GetFeaturePermissionByRoleID(response.Id); response.FeaturePermission = permissions.Select(c => c.ToFeaturePermissionVMFromFeaturePermission()).ToList(); @@ -424,12 +430,16 @@ namespace MarcoBMS.Services.Controllers if (role.IsEnabled == true) { _context.EmployeeRoleMappings.Add(mapping); + await _cache.AddApplicationRole(role.EmployeeId, [mapping.RoleId]); } } else if (role.IsEnabled == false) { _context.EmployeeRoleMappings.Remove(existingItem); + await _cache.RemoveRoleId(existingItem.EmployeeId, existingItem.RoleId); + await _cache.ClearAllPermissionIdsByEmployeeID(existingItem.EmployeeId); } + await _cache.ClearAllProjectIds(role.EmployeeId); } await _context.SaveChangesAsync(); diff --git a/Marco.Pms.Services/Dockerfile b/Marco.Pms.Services/Dockerfile index 5444e56..77311ee 100644 --- a/Marco.Pms.Services/Dockerfile +++ b/Marco.Pms.Services/Dockerfile @@ -19,6 +19,7 @@ COPY ["Marco.Pms.Services/Marco.Pms.Services.csproj", "Marco.Pms.Services/"] COPY ["Marco.Pms.DataAccess/Marco.Pms.DataAccess.csproj", "Marco.Pms.DataAccess/"] COPY ["Marco.Pms.Model/Marco.Pms.Model.csproj", "Marco.Pms.Model/"] COPY ["Marco.Pms.Utility/Marco.Pms.Utility.csproj", "Marco.Pms.Utility/"] +COPY ["Marco.Pms.Utility/Marco.Pms.CacheHelper.csproj", "Marco.Pms.CacheHelper/"] RUN dotnet restore "./Marco.Pms.Services/Marco.Pms.Services.csproj" COPY . . WORKDIR "/src/Marco.Pms.Services" diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs new file mode 100644 index 0000000..1c3ee70 --- /dev/null +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -0,0 +1,98 @@ +using Marco.Pms.CacheHelper; +using Marco.Pms.Model.MongoDBModels; +using Marco.Pms.Model.Projects; +using Project = Marco.Pms.Model.Projects.Project; + +namespace Marco.Pms.Services.Helpers +{ + public class CacheUpdateHelper + { + private readonly ProjectCache _projectCache; + private readonly EmployeeCache _employeeCache; + + public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache) + { + _projectCache = projectCache; + _employeeCache = employeeCache; + } + + // ------------------------------------ Project Details and Infrastructure Cache --------------------------------------- + public async Task AddProjectDetails(Project project) + { + await _projectCache.AddProjectDetailsToCache(project); + } + public async Task UpdateProjectDetailsOnly(Project project) + { + bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project); + return response; + } + public async Task GetProjectDetails(Guid projectId) + { + var response = await _projectCache.GetProjectDetailsFromCache(projectId); + return response; + } + public async Task AddBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) + { + await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + } + public async Task UpdateBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) + { + var response = await _projectCache.UpdateBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + if (!response) + { + await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + } + } + public async Task?> GetBuildingInfra(Guid projectId) + { + var response = await _projectCache.GetBuildingInfraFromCache(projectId); + return response; + } + + + // ------------------------------------ Employee Profile Cache --------------------------------------- + public async Task AddApplicationRole(Guid employeeId, List roleIds) + { + var response = await _employeeCache.AddApplicationRoleToCache(employeeId, roleIds); + } + public async Task AddProjects(Guid employeeId, List projectIds) + { + var response = await _employeeCache.AddProjectsToCache(employeeId, projectIds); + return response; + } + public async Task?> GetProjects(Guid employeeId) + { + var response = await _employeeCache.GetProjectsFromCache(employeeId); + if (response.Count > 0) + { + return response; + } + return null; + } + public async Task?> GetPermissions(Guid employeeId) + { + var response = await _employeeCache.GetPermissionsFromCache(employeeId); + if (response.Count > 0) + { + return response; + } + return null; + } + public async Task ClearAllProjectIds(Guid employeeId) + { + var response = await _employeeCache.ClearAllProjectIdsFromCache(employeeId); + } + public async Task ClearAllPermissionIdsByEmployeeID(Guid employeeId) + { + var response = await _employeeCache.ClearAllPermissionIdsByEmployeeIDFromCache(employeeId); + } + public async Task ClearAllPermissionIdsByRoleId(Guid roleId) + { + var response = await _employeeCache.ClearAllPermissionIdsByRoleIdFromCache(roleId); + } + public async Task RemoveRoleId(Guid employeeId, Guid roleId) + { + var response = await _employeeCache.RemoveRoleIdFromCache(employeeId, roleId); + } + } +} diff --git a/Marco.Pms.Services/Helpers/ProjectsHelper.cs b/Marco.Pms.Services/Helpers/ProjectsHelper.cs index 8ccbc85..3ccddba 100644 --- a/Marco.Pms.Services/Helpers/ProjectsHelper.cs +++ b/Marco.Pms.Services/Helpers/ProjectsHelper.cs @@ -2,11 +2,8 @@ using Marco.Pms.Model.Employees; using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Projects; -using Marco.Pms.Model.Utilities; -using Marco.Pms.Model.ViewModels.Projects; -using Microsoft.AspNetCore.Mvc; +using Marco.Pms.Services.Helpers; using Microsoft.EntityFrameworkCore; -using ModelServices.Helpers; namespace MarcoBMS.Services.Helpers { @@ -14,12 +11,14 @@ namespace MarcoBMS.Services.Helpers { private readonly ApplicationDbContext _context; private readonly RolesHelper _rolesHelper; + private readonly CacheUpdateHelper _cache; - public ProjectsHelper(ApplicationDbContext context, RolesHelper rolesHelper) + public ProjectsHelper(ApplicationDbContext context, RolesHelper rolesHelper, CacheUpdateHelper cache) { _context = context; _rolesHelper = rolesHelper; + _cache = cache; } public async Task> GetAllProjectByTanentID(Guid tanentID) @@ -53,40 +52,56 @@ namespace MarcoBMS.Services.Helpers public async Task> GetMyProjects(Guid tenantId, Employee LoggedInEmployee) { - List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(LoggedInEmployee.Id); - string[] projectsId = []; List projects = new List(); - // Define a common queryable base for projects - IQueryable projectQuery = _context.Projects.Where(c => c.TenantId == tenantId); + var projectIds = await _cache.GetProjects(LoggedInEmployee.Id); - // 2. Optimized Project Retrieval Logic - // 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) { - // 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 + projects = await _context.Projects.Where(p => projectIds.Contains(p.Id)).ToListAsync(); } 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()) + var featurePermissionIds = await _cache.GetPermissions(LoggedInEmployee.Id); + if (featurePermissionIds == null) { - return new List(); + 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); - // Use LINQ's Contains for efficient filtering by ProjectId - var projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); // Get distinct Guids + // 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 + } + else + { + // 3. Efficiently get project allocations and then filter projects + // Load allocations only once + var allocation = await GetProjectByEmployeeID(LoggedInEmployee.Id); - // Filter projects based on the retrieved ProjectIds - projects = await projectQuery.Where(c => projectIds.Contains(c.Id)).ToListAsync(); + // If there are no allocations, return an empty list early + if (allocation == null || !allocation.Any()) + { + return new List(); + } + + // 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(); + + } + projectIds = projects.Select(p => p.Id).ToList(); + await _cache.AddProjects(LoggedInEmployee.Id, projectIds); } return projects; diff --git a/Marco.Pms.Services/Helpers/RolesHelper.cs b/Marco.Pms.Services/Helpers/RolesHelper.cs index b571d03..15bf0b1 100644 --- a/Marco.Pms.Services/Helpers/RolesHelper.cs +++ b/Marco.Pms.Services/Helpers/RolesHelper.cs @@ -2,6 +2,7 @@ using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Entitlements; +using Marco.Pms.Services.Helpers; using Microsoft.EntityFrameworkCore; namespace MarcoBMS.Services.Helpers @@ -9,15 +10,19 @@ namespace MarcoBMS.Services.Helpers public class RolesHelper { private readonly ApplicationDbContext _context; - public RolesHelper(ApplicationDbContext context) + private readonly CacheUpdateHelper _cache; + public RolesHelper(ApplicationDbContext context, CacheUpdateHelper cache) { _context = context; + _cache = cache; } public async Task> GetFeaturePermissionByEmployeeID(Guid EmployeeID) { List roleMappings = await _context.EmployeeRoleMappings.Where(c => c.EmployeeId == EmployeeID && c.IsEnabled == true).Select(c => c.RoleId).ToListAsync(); + await _cache.AddApplicationRole(EmployeeID, roleMappings); + // _context.RolePermissionMappings var result = await (from rpm in _context.RolePermissionMappings diff --git a/Marco.Pms.Services/Marco.Pms.Services.csproj b/Marco.Pms.Services/Marco.Pms.Services.csproj index 7bef32f..a235e6a 100644 --- a/Marco.Pms.Services/Marco.Pms.Services.csproj +++ b/Marco.Pms.Services/Marco.Pms.Services.csproj @@ -44,6 +44,7 @@ + diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index 17eb5c7..1d9b4b3 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -1,4 +1,5 @@ using System.Text; +using Marco.Pms.CacheHelper; using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Authentication; using Marco.Pms.Model.Entitlements; @@ -136,6 +137,9 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddSingleton(); @@ -225,7 +229,7 @@ app.UseStaticFiles(); // Enables serving static files app.UseHttpsRedirection(); - +app.UseAuthentication(); app.UseAuthorization(); app.MapHub("/hubs/marco"); app.MapControllers(); diff --git a/Marco.Pms.Services/Service/PermissionServices.cs b/Marco.Pms.Services/Service/PermissionServices.cs index f3ddb58..ce7476b 100644 --- a/Marco.Pms.Services/Service/PermissionServices.cs +++ b/Marco.Pms.Services/Service/PermissionServices.cs @@ -2,6 +2,7 @@ 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,21 +13,24 @@ namespace Marco.Pms.Services.Service private readonly ApplicationDbContext _context; private readonly RolesHelper _rolesHelper; private readonly ProjectsHelper _projectsHelper; - public PermissionServices(ApplicationDbContext context, RolesHelper rolesHelper, ProjectsHelper projectsHelper) + private readonly CacheUpdateHelper _cache; + public PermissionServices(ApplicationDbContext context, RolesHelper rolesHelper, ProjectsHelper projectsHelper, CacheUpdateHelper cache) { _context = context; _rolesHelper = rolesHelper; _projectsHelper = projectsHelper; + _cache = cache; } public async Task HasPermission(Guid featurePermissionId, Guid employeeId) { - var hasPermission = await _context.EmployeeRoleMappings - .Where(er => er.EmployeeId == employeeId) - .Select(er => er.RoleId) - .Distinct() - .AnyAsync(roleId => _context.RolePermissionMappings - .Any(rp => rp.FeaturePermissionId == featurePermissionId && rp.ApplicationRoleId == roleId)); + var featurePermissionIds = await _cache.GetPermissions(employeeId); + if (featurePermissionIds == null) + { + List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(employeeId); + featurePermissionIds = featurePermission.Select(fp => fp.Id).ToList(); + } + var hasPermission = featurePermissionIds.Contains(featurePermissionId); return hasPermission; } public async Task HasProjectPermission(Employee emp, string projectId) diff --git a/Marco.Pms.Services/appsettings.Development.json b/Marco.Pms.Services/appsettings.Development.json index 1565018..ce80dc0 100644 --- a/Marco.Pms.Services/appsettings.Development.json +++ b/Marco.Pms.Services/appsettings.Development.json @@ -47,6 +47,8 @@ "BucketName": "testenv-marco-pms-documents" }, "MongoDB": { - "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs" + "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs", + "ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches" + //"DatabaseName": "" } } diff --git a/Marco.Pms.Services/appsettings.Production.json b/Marco.Pms.Services/appsettings.Production.json index 81aa998..0abe3f1 100644 --- a/Marco.Pms.Services/appsettings.Production.json +++ b/Marco.Pms.Services/appsettings.Production.json @@ -6,7 +6,7 @@ }, "Environment": { "Name": "Production", - "Title": "" + "Title": "" }, "ConnectionStrings": { "DefaultConnectionString": "Server=147.93.98.152;User ID=devuser;Password=AppUser@123$;Database=MarcoBMS1" @@ -40,6 +40,7 @@ "BucketName": "testenv-marco-pms-documents" }, "MongoDB": { - "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs" + "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs", + "ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches" } } \ No newline at end of file diff --git a/marco.pms.api.sln b/marco.pms.api.sln index 49d3e8c..424b709 100644 --- a/marco.pms.api.sln +++ b/marco.pms.api.sln @@ -11,6 +11,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Marco.Pms.Utility", "Marco. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Marco.Pms.Services", "Marco.Pms.Services\Marco.Pms.Services.csproj", "{27A83653-5B7F-4135-9886-01594D54AFAE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Marco.Pms.CacheHelper", "Marco.Pms.CacheHelper\Marco.Pms.CacheHelper.csproj", "{1A105C22-4ED7-4F54-8834-6923DDD96852}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,6 +35,10 @@ Global {27A83653-5B7F-4135-9886-01594D54AFAE}.Debug|Any CPU.Build.0 = Debug|Any CPU {27A83653-5B7F-4135-9886-01594D54AFAE}.Release|Any CPU.ActiveCfg = Release|Any CPU {27A83653-5B7F-4135-9886-01594D54AFAE}.Release|Any CPU.Build.0 = Release|Any CPU + {1A105C22-4ED7-4F54-8834-6923DDD96852}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A105C22-4ED7-4F54-8834-6923DDD96852}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A105C22-4ED7-4F54-8834-6923DDD96852}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A105C22-4ED7-4F54-8834-6923DDD96852}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 8c85d92ba6a8845e6ffabaef44d02bd166417e13 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Fri, 4 Jul 2025 17:50:27 +0530 Subject: [PATCH 034/124] removed comented code from appsetting file --- Marco.Pms.Services/appsettings.Development.json | 1 - 1 file changed, 1 deletion(-) diff --git a/Marco.Pms.Services/appsettings.Development.json b/Marco.Pms.Services/appsettings.Development.json index ce80dc0..5f5e19d 100644 --- a/Marco.Pms.Services/appsettings.Development.json +++ b/Marco.Pms.Services/appsettings.Development.json @@ -49,6 +49,5 @@ "MongoDB": { "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs", "ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches" - //"DatabaseName": "" } } From 8521a68c3e356118ac7a3e82eee350bc0659ef5d Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Sat, 5 Jul 2025 15:25:01 +0530 Subject: [PATCH 035/124] Added error handling in cache helper --- .../Helpers/CacheUpdateHelper.cs | 170 +++++++++++++++--- 1 file changed, 143 insertions(+), 27 deletions(-) diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 1c3ee70..75b51b5 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -1,6 +1,7 @@ using Marco.Pms.CacheHelper; using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.Projects; +using MarcoBMS.Services.Service; using Project = Marco.Pms.Model.Projects.Project; namespace Marco.Pms.Services.Helpers @@ -9,90 +10,205 @@ namespace Marco.Pms.Services.Helpers { private readonly ProjectCache _projectCache; private readonly EmployeeCache _employeeCache; + private readonly ILoggingService _logger; - public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache) + public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache, ILoggingService logger) { _projectCache = projectCache; _employeeCache = employeeCache; + _logger = logger; } // ------------------------------------ Project Details and Infrastructure Cache --------------------------------------- public async Task AddProjectDetails(Project project) { - await _projectCache.AddProjectDetailsToCache(project); + try + { + await _projectCache.AddProjectDetailsToCache(project); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while adding project to Cache: {Error}", ex.Message); + } } public async Task UpdateProjectDetailsOnly(Project project) { - bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project); - return response; + try + { + bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while updating project to Cache: {Error}", ex.Message); + return false; + } } public async Task GetProjectDetails(Guid projectId) { - var response = await _projectCache.GetProjectDetailsFromCache(projectId); - return response; + try + { + var response = await _projectCache.GetProjectDetailsFromCache(projectId); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while getting project to Cache: {Error}", ex.Message); + return null; + } } + //public async Task?> GetProjectDetailsList(List projectIds) + //{ + // var response = await _projectCache.GetProjectDetailsListFromCache(projectIds); + // return response; + //} public async Task AddBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) { - await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + try + { + await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while adding project infra to Cache: {Error}", ex.Message); + } } public async Task UpdateBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) { - var response = await _projectCache.UpdateBuildngInfraToCache(projectId, building, floor, workArea, buildingId); - if (!response) + try { - await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + var response = await _projectCache.UpdateBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + if (!response) + { + await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + } + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while updating project infra to Cache: {Error}", ex.Message); } } public async Task?> GetBuildingInfra(Guid projectId) { - var response = await _projectCache.GetBuildingInfraFromCache(projectId); - return response; + try + { + var response = await _projectCache.GetBuildingInfraFromCache(projectId); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while getting project infra Cache: {Error}", ex.Message); + return null; + } } // ------------------------------------ Employee Profile Cache --------------------------------------- public async Task AddApplicationRole(Guid employeeId, List roleIds) { - var response = await _employeeCache.AddApplicationRoleToCache(employeeId, roleIds); + try + { + var response = await _employeeCache.AddApplicationRoleToCache(employeeId, roleIds); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while adding Application roleIds to Cache to employee {Employee}: {Error}", employeeId, ex.Message); + } } public async Task AddProjects(Guid employeeId, List projectIds) { - var response = await _employeeCache.AddProjectsToCache(employeeId, projectIds); - return response; + try + { + var response = await _employeeCache.AddProjectsToCache(employeeId, projectIds); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while adding projectIds to Cache: {Error}", ex.Message); + return false; + } } public async Task?> GetProjects(Guid employeeId) { - var response = await _employeeCache.GetProjectsFromCache(employeeId); - if (response.Count > 0) + try { - return response; + var response = await _employeeCache.GetProjectsFromCache(employeeId); + if (response.Count > 0) + { + return response; + } + return null; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while getting projectIDs to Cache: {Error}", ex.Message); + return null; } - return null; } public async Task?> GetPermissions(Guid employeeId) { - var response = await _employeeCache.GetPermissionsFromCache(employeeId); - if (response.Count > 0) + try { - return response; + var response = await _employeeCache.GetPermissionsFromCache(employeeId); + if (response.Count > 0) + { + return response; + } + return null; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while getting permissionIds to Cache: {Error}", ex.Message); + return null; } - return null; } public async Task ClearAllProjectIds(Guid employeeId) { - var response = await _employeeCache.ClearAllProjectIdsFromCache(employeeId); + try + { + var response = await _employeeCache.ClearAllProjectIdsFromCache(employeeId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting projectIds from Cache for employee {EmployeeId}: {Error}", employeeId, ex.Message); + } } + //public async Task ClearAllProjectIdsByRoleId(Guid roleId) + //{ + // await _employeeCache.ClearAllProjectIdsByRoleIdFromCache(roleId); + //} public async Task ClearAllPermissionIdsByEmployeeID(Guid employeeId) { - var response = await _employeeCache.ClearAllPermissionIdsByEmployeeIDFromCache(employeeId); + try + { + var response = await _employeeCache.ClearAllPermissionIdsByEmployeeIDFromCache(employeeId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting permissionIds from to Cache: {Error}", ex.Message); + } } public async Task ClearAllPermissionIdsByRoleId(Guid roleId) { - var response = await _employeeCache.ClearAllPermissionIdsByRoleIdFromCache(roleId); + try + { + var response = await _employeeCache.ClearAllPermissionIdsByRoleIdFromCache(roleId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting permissionIds from to Cache: {Error}", ex.Message); + } } public async Task RemoveRoleId(Guid employeeId, Guid roleId) { - var response = await _employeeCache.RemoveRoleIdFromCache(employeeId, roleId); + try + { + var response = await _employeeCache.RemoveRoleIdFromCache(employeeId, roleId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting Application roleIds from to Cache: {Error}", ex.Message); + } } } } From 1d318c75d83ca57e179deb07bbee602dca0e83d1 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 7 Jul 2025 10:04:11 +0530 Subject: [PATCH 036/124] Implemented the methods for deleting permission am asigned project from caches for certien employee --- Marco.Pms.CacheHelper/EmployeeCache.cs | 60 ++++++++++++++----- Marco.Pms.CacheHelper/ProjectCache.cs | 55 ++++++++++------- .../EmployeePermissionMongoDB.cs | 2 +- .../MongoDBModels/ProjectMongoDB.cs | 2 +- .../MongoDBModels/WorkItemMongoDB.cs | 9 +-- .../Controllers/RolesController.cs | 4 ++ .../Helpers/CacheUpdateHelper.cs | 57 +++++++++++------- Marco.Pms.Services/Helpers/ProjectsHelper.cs | 21 ++++++- 8 files changed, 144 insertions(+), 66 deletions(-) diff --git a/Marco.Pms.CacheHelper/EmployeeCache.cs b/Marco.Pms.CacheHelper/EmployeeCache.cs index 7d75407..5c86e6f 100644 --- a/Marco.Pms.CacheHelper/EmployeeCache.cs +++ b/Marco.Pms.CacheHelper/EmployeeCache.cs @@ -22,31 +22,47 @@ namespace Marco.Pms.CacheHelper } public async Task AddApplicationRoleToCache(Guid employeeId, List roleIds) { - var newRoleIds = roleIds.Select(r => r.ToString()).ToList(); - var newPermissionIds = await _context.RolePermissionMappings + // 1. Guard Clause: Avoid unnecessary database work if there are no roles to add. + if (roleIds == null || !roleIds.Any()) + { + return false; // Nothing to add, so the operation did not result in a change. + } + + // 2. Perform database queries concurrently for better performance. + var employeeIdString = employeeId.ToString(); + + Task> getPermissionIdsTask = _context.RolePermissionMappings .Where(rp => roleIds.Contains(rp.ApplicationRoleId)) .Select(p => p.FeaturePermissionId.ToString()) .Distinct() .ToListAsync(); - var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + // 3. Prepare role IDs in parallel with the database query. + var newRoleIds = roleIds.Select(r => r.ToString()).ToList(); + + // 4. Await the database query result. + var newPermissionIds = await getPermissionIdsTask; + + // 5. Build a single, efficient update operation. + var filter = Builders.Filter.Eq(e => e.Id, employeeIdString); var update = Builders.Update .AddToSetEach(e => e.ApplicationRoleIds, newRoleIds) .AddToSetEach(e => e.PermissionIds, newPermissionIds); - var result = await _collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); - if (result.MatchedCount == 0) - { - return false; - } - return true; + var options = new UpdateOptions { IsUpsert = true }; + + var result = await _collection.UpdateOneAsync(filter, update, options); + + // 6. Return a more accurate result indicating success for both updates and upserts. + // The operation is successful if an existing document was modified OR a new one was created. + return result.IsAcknowledged && (result.ModifiedCount > 0 || result.UpsertedId != null); } public async Task AddProjectsToCache(Guid employeeId, List projectIds) { var newprojectIds = projectIds.Select(p => p.ToString()).ToList(); - var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + var filter = Builders.Filter.Eq(e => e.Id, employeeId.ToString()); var update = Builders.Update .AddToSetEach(e => e.ProjectIds, newprojectIds); @@ -60,7 +76,7 @@ namespace Marco.Pms.CacheHelper } public async Task> GetProjectsFromCache(Guid employeeId) { - var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + var filter = Builders.Filter.Eq(e => e.Id, employeeId.ToString()); var result = await _collection @@ -77,7 +93,7 @@ namespace Marco.Pms.CacheHelper } public async Task> GetPermissionsFromCache(Guid employeeId) { - var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + var filter = Builders.Filter.Eq(e => e.Id, employeeId.ToString()); var result = await _collection @@ -95,7 +111,21 @@ namespace Marco.Pms.CacheHelper public async Task ClearAllProjectIdsFromCache(Guid employeeId) { var filter = Builders.Filter - .Eq(e => e.EmployeeId, employeeId.ToString()); + .Eq(e => e.Id, employeeId.ToString()); + + var update = Builders.Update + .Set(e => e.ProjectIds, new List()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + return true; + } + public async Task ClearAllProjectIdsByRoleIdFromCache(Guid roleId) + { + var filter = Builders.Filter.AnyEq(e => e.ApplicationRoleIds, roleId.ToString()); var update = Builders.Update .Set(e => e.ProjectIds, new List()); @@ -110,7 +140,7 @@ namespace Marco.Pms.CacheHelper public async Task RemoveRoleIdFromCache(Guid employeeId, Guid roleId) { var filter = Builders.Filter - .Eq(e => e.EmployeeId, employeeId.ToString()); + .Eq(e => e.Id, employeeId.ToString()); var update = Builders.Update .Pull(e => e.ApplicationRoleIds, roleId.ToString()); @@ -128,7 +158,7 @@ namespace Marco.Pms.CacheHelper public async Task ClearAllPermissionIdsByEmployeeIDFromCache(Guid employeeId) { var filter = Builders.Filter - .Eq(e => e.EmployeeId, employeeId.ToString()); + .Eq(e => e.Id, employeeId.ToString()); var update = Builders.Update .Set(e => e.PermissionIds, new List()); diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index b667694..f60884f 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -11,19 +11,21 @@ namespace Marco.Pms.CacheHelper public class ProjectCache { private readonly ApplicationDbContext _context; - private readonly IMongoDatabase _mongoDB; - //private readonly ILoggingService _logger; + private readonly IMongoCollection _projetCollection; + private readonly IMongoCollection _taskCollection; public ProjectCache(ApplicationDbContext context, IConfiguration configuration) { var connectionString = configuration["MongoDB:ConnectionString"]; _context = context; var mongoUrl = new MongoUrl(connectionString); var client = new MongoClient(mongoUrl); // Your MongoDB connection string - _mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name + var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name + _projetCollection = mongoDB.GetCollection("ProjectDetails"); + _taskCollection = mongoDB.GetCollection("WorkItemDetails"); } public async Task AddProjectDetailsToCache(Project project) { - var projectCollection = _mongoDB.GetCollection("ProjectDetails"); + //_logger.LogInfo("[AddProjectDetails] Initiated for ProjectId: {ProjectId}", project.Id); @@ -145,7 +147,7 @@ namespace Marco.Pms.CacheHelper projectDetails.PlannedWork = totalPlannedWork; projectDetails.CompletedWork = totalCompletedWork; - await projectCollection.InsertOneAsync(projectDetails); + await _projetCollection.InsertOneAsync(projectDetails); //_logger.LogInfo("[AddProjectDetails] Project details inserted in MongoDB for ProjectId: {ProjectId}", project.Id); } public async Task UpdateProjectDetailsOnlyToCache(Project project) @@ -160,8 +162,6 @@ namespace Marco.Pms.CacheHelper //_logger.LogWarning("StatusMaster not found for ProjectStatusId: {StatusId}", project.ProjectStatusId); } - var projectCollection = _mongoDB.GetCollection("ProjectDetails"); - // Build the update definition var updates = Builders.Update.Combine( Builders.Update.Set(r => r.Name, project.Name), @@ -178,7 +178,7 @@ namespace Marco.Pms.CacheHelper ); // Perform the update - var result = await projectCollection.UpdateOneAsync( + var result = await _projetCollection.UpdateOneAsync( filter: r => r.Id == project.Id.ToString(), update: updates ); @@ -194,7 +194,6 @@ namespace Marco.Pms.CacheHelper } public async Task GetProjectDetailsFromCache(Guid projectId) { - var projectCollection = _mongoDB.GetCollection("ProjectDetails"); // Build filter and projection to exclude large 'Buildings' list var filter = Builders.Filter.Eq(p => p.Id, projectId.ToString()); @@ -203,7 +202,7 @@ namespace Marco.Pms.CacheHelper //_logger.LogInfo("Fetching project details for ProjectId: {ProjectId} from MongoDB", projectId); // Perform query - var project = await projectCollection + var project = await _projetCollection .Find(filter) .Project(projection) .FirstOrDefaultAsync(); @@ -214,16 +213,23 @@ namespace Marco.Pms.CacheHelper return null; } - //// Deserialize the result manually - //var project = BsonSerializer.Deserialize(result); - //_logger.LogInfo("Successfully fetched project details (excluding Buildings) for ProjectId: {ProjectId}", projectId); return project; } + public async Task?> GetProjectDetailsListFromCache(List projectIds) + { + List stringProjectIds = projectIds.Select(p => p.ToString()).ToList(); + var filter = Builders.Filter.In(p => p.Id, stringProjectIds); + var projection = Builders.Projection.Exclude(p => p.Buildings); + var projects = await _projetCollection + .Find(filter) + .Project(projection) + .ToListAsync(); + return projects; + } public async Task AddBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId) { var stringProjectId = projectId.ToString(); - var projectCollection = _mongoDB.GetCollection("ProjectDetails"); // Add Building if (building != null) @@ -241,7 +247,7 @@ namespace Marco.Pms.CacheHelper var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); var update = Builders.Update.Push("Buildings", buildingMongo); - var result = await projectCollection.UpdateOneAsync(filter, update); + var result = await _projetCollection.UpdateOneAsync(filter, update); if (result.MatchedCount == 0) { @@ -271,7 +277,7 @@ namespace Marco.Pms.CacheHelper ); var update = Builders.Update.Push("Buildings.$.Floors", floorMongo); - var result = await projectCollection.UpdateOneAsync(filter, update); + var result = await _projetCollection.UpdateOneAsync(filter, update); if (result.MatchedCount == 0) { @@ -305,7 +311,7 @@ namespace Marco.Pms.CacheHelper var update = Builders.Update.Push("Buildings.$[b].Floors.$[f].WorkAreas", workAreaMongo); var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; - var result = await projectCollection.UpdateOneAsync(filter, update, updateOptions); + var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); if (result.MatchedCount == 0) { @@ -323,7 +329,6 @@ namespace Marco.Pms.CacheHelper public async Task UpdateBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId) { var stringProjectId = projectId.ToString(); - var projectCollection = _mongoDB.GetCollection("ProjectDetails"); // Update Building if (building != null) @@ -338,7 +343,7 @@ namespace Marco.Pms.CacheHelper Builders.Update.Set("Buildings.$.Description", building.Description) ); - var result = await projectCollection.UpdateOneAsync(filter, update); + var result = await _projetCollection.UpdateOneAsync(filter, update); if (result.MatchedCount == 0) { @@ -363,7 +368,7 @@ namespace Marco.Pms.CacheHelper var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); - var result = await projectCollection.UpdateOneAsync(filter, update, updateOptions); + var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); if (result.MatchedCount == 0) { @@ -389,7 +394,7 @@ namespace Marco.Pms.CacheHelper var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); - var result = await projectCollection.UpdateOneAsync(filter, update, updateOptions); + var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); if (result.MatchedCount == 0) { @@ -408,13 +413,12 @@ namespace Marco.Pms.CacheHelper } public async Task?> GetBuildingInfraFromCache(Guid projectId) { - var projectCollection = _mongoDB.GetCollection("ProjectDetails"); // Filter by project ID var filter = Builders.Filter.Eq(p => p.Id, projectId.ToString()); // Project only the "Buildings" field from the document - var buildings = await projectCollection + var buildings = await _projetCollection .Find(filter) .Project(p => p.Buildings) .FirstOrDefaultAsync(); @@ -430,5 +434,10 @@ namespace Marco.Pms.CacheHelper return buildings; } + + + // ------------------------------------------------------- WorkItem ------------------------------------------------------- + + } } diff --git a/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs b/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs index f141798..49c514e 100644 --- a/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs @@ -5,7 +5,7 @@ namespace Marco.Pms.Model.MongoDBModels [BsonIgnoreExtraElements] public class EmployeePermissionMongoDB { - public string EmployeeId { get; set; } = string.Empty; + public string Id { get; set; } = string.Empty; // Employee ID public List ApplicationRoleIds { get; set; } = new List(); public List PermissionIds { get; set; } = new List(); public List ProjectIds { get; set; } = new List(); diff --git a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs index 8bf1c9a..8b1612c 100644 --- a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs @@ -2,7 +2,7 @@ { public class ProjectMongoDB { - public string? Id { get; set; } + public string Id { get; set; } = string.Empty; public string? Name { get; set; } public string? ShortName { get; set; } public string? ProjectAddress { get; set; } diff --git a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs index dc7fdb9..71638a3 100644 --- a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs @@ -2,13 +2,14 @@ { public class WorkItemMongoDB { - public string? Id { get; set; } - public string? WorkAreaId { get; set; } + public string Id { get; set; } = string.Empty; + public string WorkAreaId { get; set; } = string.Empty; public ActivityMasterMongoDB? ActivityMaster { get; set; } public WorkCategoryMasterMongoDB? WorkCategoryMaster { get; set; } public string? ParentTaskId { get; set; } - public double PlannedWork { get; set; } - public double CompletedWork { get; set; } + public double PlannedWork { get; set; } = 0; + public double TodaysAssigned { get; set; } = 0; + public double CompletedWork { get; set; } = 0; public string? Description { get; set; } public DateTime TaskDate { get; set; } } diff --git a/Marco.Pms.Services/Controllers/RolesController.cs b/Marco.Pms.Services/Controllers/RolesController.cs index 4c75b3e..a67ecaf 100644 --- a/Marco.Pms.Services/Controllers/RolesController.cs +++ b/Marco.Pms.Services/Controllers/RolesController.cs @@ -292,6 +292,10 @@ namespace MarcoBMS.Services.Controllers _context.RolePermissionMappings.Add(item); modified = true; } + if (item.FeaturePermissionId == Guid.Parse("172fc9b6-755b-4f62-ab26-55c34a330614")) + { + await _cache.ClearAllProjectIdsByRoleId(id); + } } if (modified) await _context.SaveChangesAsync(); diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 75b51b5..6ff9cfe 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -28,7 +28,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while adding project to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while adding project {ProjectId} to Cache : {Error}", project.Id, ex.Message); } } public async Task UpdateProjectDetailsOnly(Project project) @@ -40,7 +40,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while updating project to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while updating project {ProjectId} to Cache: {Error}", project.Id, ex.Message); return false; } } @@ -53,15 +53,23 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while getting project to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while getting project {ProjectId} to Cache: {Error}", ex.Message); + return null; + } + } + public async Task?> GetProjectDetailsList(List projectIds) + { + try + { + var response = await _projectCache.GetProjectDetailsListFromCache(projectIds); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while getting list od project details from to Cache: {Error}", ex.Message); return null; } } - //public async Task?> GetProjectDetailsList(List projectIds) - //{ - // var response = await _projectCache.GetProjectDetailsListFromCache(projectIds); - // return response; - //} public async Task AddBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) { try @@ -70,7 +78,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while adding project infra to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while adding project infra for project {ProjectId} to Cache: {Error}", projectId, ex.Message); } } public async Task UpdateBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) @@ -85,7 +93,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while updating project infra to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while updating project infra for project {ProjectId} to Cache: {Error}", projectId, ex.Message); } } public async Task?> GetBuildingInfra(Guid projectId) @@ -97,7 +105,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while getting project infra Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while getting project infra for project {ProjectId} form Cache: {Error}", projectId, ex.Message); return null; } } @@ -124,7 +132,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while adding projectIds to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while adding projectIds for employee {EmployeeId} to Cache: {Error}", employeeId, ex.Message); return false; } } @@ -141,7 +149,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while getting projectIDs to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while getting projectIds for employee {EmployeeId} from Cache: {Error}", employeeId, ex.Message); return null; } } @@ -158,7 +166,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while getting permissionIds to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while getting permissionIds for employee {EmployeeId} from Cache: {Error}", employeeId, ex.Message); return null; } } @@ -173,10 +181,17 @@ namespace Marco.Pms.Services.Helpers _logger.LogWarning("Error occured while deleting projectIds from Cache for employee {EmployeeId}: {Error}", employeeId, ex.Message); } } - //public async Task ClearAllProjectIdsByRoleId(Guid roleId) - //{ - // await _employeeCache.ClearAllProjectIdsByRoleIdFromCache(roleId); - //} + public async Task ClearAllProjectIdsByRoleId(Guid roleId) + { + try + { + await _employeeCache.ClearAllProjectIdsByRoleIdFromCache(roleId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting projectIds from Cache for Application Role {RoleId}: {Error}", roleId, ex.Message); + } + } public async Task ClearAllPermissionIdsByEmployeeID(Guid employeeId) { try @@ -185,7 +200,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while deleting permissionIds from to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while deleting permissionIds from Cache for employee {EmployeeId}: {Error}", employeeId, ex.Message); } } public async Task ClearAllPermissionIdsByRoleId(Guid roleId) @@ -196,7 +211,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while deleting permissionIds from to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while deleting permissionIds from Cache for Application role {RoleId}: {Error}", roleId, ex.Message); } } public async Task RemoveRoleId(Guid employeeId, Guid roleId) @@ -207,7 +222,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while deleting Application roleIds from to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while deleting Application role {RoleId} from Cache for employee {EmployeeId}: {Error}", roleId, employeeId, ex.Message); } } } diff --git a/Marco.Pms.Services/Helpers/ProjectsHelper.cs b/Marco.Pms.Services/Helpers/ProjectsHelper.cs index 3ccddba..85003ae 100644 --- a/Marco.Pms.Services/Helpers/ProjectsHelper.cs +++ b/Marco.Pms.Services/Helpers/ProjectsHelper.cs @@ -1,6 +1,7 @@ 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 Microsoft.EntityFrameworkCore; @@ -59,7 +60,25 @@ namespace MarcoBMS.Services.Helpers if (projectIds != null) { - projects = await _context.Projects.Where(p => projectIds.Contains(p.Id)).ToListAsync(); + + List projectdetails = await _cache.GetProjectDetailsList(projectIds) ?? new List(); + projects = projectdetails.Select(p => new Project + { + 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 { From 56aca323e5d2407a4454758cc888f779808a31cb Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 7 Jul 2025 17:44:58 +0530 Subject: [PATCH 037/124] Storing workItem in cache and changing planned work and completed work for respective project, building, floor, and workarea --- Marco.Pms.CacheHelper/ProjectCache.cs | 120 ++++++++++++++++++ .../MongoDBModels/ActivityMasterMongoDB.cs | 2 +- .../MongoDBModels/BuildingMongoDB.cs | 2 +- Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs | 2 +- .../MongoDBModels/ProjectMongoDB.cs | 2 +- .../WorkCategoryMasterMongoDB.cs | 2 +- .../MongoDBModels/WorkItemMongoDB.cs | 2 +- .../Controllers/ProjectController.cs | 73 +++++++++-- .../Helpers/CacheUpdateHelper.cs | 65 ++++++++++ .../appsettings.Development.json | 2 +- 10 files changed, 256 insertions(+), 16 deletions(-) diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index f60884f..6f5a3d3 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -1,4 +1,5 @@ using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.Master; using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.Projects; using Microsoft.EntityFrameworkCore; @@ -434,10 +435,129 @@ namespace Marco.Pms.CacheHelper return buildings; } + public async Task UpdatePlannedAndCompleteWorksInBuildingFromCache(Guid workAreaId, double plannedWork, double completedWork) + { + var filter = Builders.Filter.Eq("Buildings.Floors.WorkAreas._id", workAreaId.ToString()); + var project = await _projetCollection.Find(filter).FirstOrDefaultAsync(); + + string? selectedBuildingId = null; + string? selectedFloorId = null; + string? selectedWorkAreaId = null; + + foreach (var building in project.Buildings) + { + foreach (var floor in building.Floors) + { + foreach (var area in floor.WorkAreas) + { + if (area.Id == workAreaId.ToString()) + { + selectedWorkAreaId = area.Id; + selectedFloorId = floor.Id; + selectedBuildingId = building.Id; + } + } + } + } + + var arrayFilters = new List + { + new JsonArrayFilterDefinition("{ 'b._id': '" + selectedBuildingId + "' }"), + new JsonArrayFilterDefinition("{ 'f._id': '" + selectedFloorId + "' }"), + new JsonArrayFilterDefinition("{ 'a._id': '" + selectedWorkAreaId + "' }") + }; + var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; + var update = Builders.Update + .Inc("Buildings.$[b].Floors.$[f].WorkAreas.$[a].PlannedWork", plannedWork) + .Inc("Buildings.$[b].Floors.$[f].WorkAreas.$[a].CompletedWork", completedWork) + .Inc("Buildings.$[b].Floors.$[f].PlannedWork", plannedWork) + .Inc("Buildings.$[b].Floors.$[f].CompletedWork", completedWork) + .Inc("Buildings.$[b].PlannedWork", plannedWork) + .Inc("Buildings.$[b].CompletedWork", completedWork) + .Inc("PlannedWork", plannedWork) + .Inc("CompletedWork", completedWork); + var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); + + } // ------------------------------------------------------- WorkItem ------------------------------------------------------- + public async Task ManageWorkItemDetailsToCache(List workItems) + { + var activityIds = workItems.Select(wi => wi.ActivityId).ToList(); + var workCategoryIds = workItems.Select(wi => wi.WorkCategoryId).ToList(); + // fetching Activity master + var activities = await _context.ActivityMasters.Where(a => activityIds.Contains(a.Id)).ToListAsync() ?? new List(); + // Fetching Work Category + var workCategories = await _context.WorkCategoryMasters.Where(wc => workCategoryIds.Contains(wc.Id)).ToListAsync() ?? new List(); + + foreach (WorkItem workItem in workItems) + { + var activity = activities.FirstOrDefault(a => a.Id == workItem.ActivityId) ?? new ActivityMaster(); + var workCategory = workCategories.FirstOrDefault(a => a.Id == workItem.WorkCategoryId) ?? new WorkCategoryMaster(); + + var filter = Builders.Filter.Eq(p => p.Id, workItem.Id.ToString()); + var updates = Builders.Update.Combine( + Builders.Update.Set(r => r.WorkAreaId, workItem.WorkAreaId.ToString()), + Builders.Update.Set(r => r.ParentTaskId, (workItem.ParentTaskId != null ? workItem.ParentTaskId.ToString() : null)), + Builders.Update.Set(r => r.PlannedWork, workItem.PlannedWork), + Builders.Update.Set(r => r.TodaysAssigned, 0), + Builders.Update.Set(r => r.CompletedWork, workItem.CompletedWork), + Builders.Update.Set(r => r.Description, workItem.Description), + Builders.Update.Set(r => r.TaskDate, workItem.TaskDate), + Builders.Update.Set(r => r.ActivityMaster, new ActivityMasterMongoDB + { + Id = activity.Id.ToString(), + ActivityName = activity.ActivityName, + UnitOfMeasurement = activity.UnitOfMeasurement + }), + Builders.Update.Set(r => r.WorkCategoryMaster, new WorkCategoryMasterMongoDB + { + Id = workCategory.Id.ToString(), + Name = workCategory.Name, + Description = workCategory.Description, + }) + ); + var options = new UpdateOptions { IsUpsert = true }; + var result = await _taskCollection.UpdateOneAsync(filter, updates, options); + } + } + public async Task> GetWorkItemDetailsByWorkAreaFromCache(Guid workAreaId) + { + var filter = Builders.Filter.Eq(p => p.WorkAreaId, workAreaId.ToString()); + + var options = new UpdateOptions { IsUpsert = true }; + var workItems = await _taskCollection + .Find(filter) + .ToListAsync(); + return workItems; + } + public async Task GetWorkItemDetailsByIdFromCache(Guid id) + { + var filter = Builders.Filter.Eq(p => p.Id, id.ToString()); + + var options = new UpdateOptions { IsUpsert = true }; + var workItem = await _taskCollection + .Find(filter) + .FirstOrDefaultAsync(); + return workItem; + } + public async Task UpdatePlannedAndCompleteWorksInWorkItem(Guid id, double plannedWork = 0, double completedWork = 0, double todaysAssigned = 0) + { + var filter = Builders.Filter.Eq(p => p.Id, id.ToString()); + var updates = Builders.Update + .Inc("PlannedWork", plannedWork) + .Inc("CompletedWork", completedWork) + .Inc("TodaysAssigned", todaysAssigned); + + var result = await _taskCollection.UpdateOneAsync(filter, updates); + if (result.ModifiedCount > 0) + { + return true; + } + return false; + } } } diff --git a/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs index 37218b7..cc77d96 100644 --- a/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs @@ -2,7 +2,7 @@ { public class ActivityMasterMongoDB { - public string? Id { get; set; } + public string Id { get; set; } = string.Empty; public string? ActivityName { get; set; } public string? UnitOfMeasurement { get; set; } } diff --git a/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs b/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs index 87ccb8d..64ccbce 100644 --- a/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs @@ -7,7 +7,7 @@ public string? Description { get; set; } public double PlannedWork { get; set; } public double CompletedWork { get; set; } - public List? Floors { get; set; } + public List Floors { get; set; } = new List(); } public class BuildingMongoDBVM { diff --git a/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs b/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs index ae3975f..57257a4 100644 --- a/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs @@ -6,7 +6,7 @@ public string? FloorName { get; set; } public double PlannedWork { get; set; } public double CompletedWork { get; set; } - public List? WorkAreas { get; set; } + public List WorkAreas { get; set; } = new List(); } public class FloorMongoDBVM diff --git a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs index 8b1612c..7f3a557 100644 --- a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs @@ -7,7 +7,7 @@ public string? ShortName { get; set; } public string? ProjectAddress { get; set; } public string? ContactPerson { get; set; } - public List? Buildings { get; set; } + public List Buildings { get; set; } = new List(); public DateTime? StartDate { get; set; } public DateTime? EndDate { get; set; } public StatusMasterMongoDB? ProjectStatus { get; set; } diff --git a/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs index aef0ada..4ea4682 100644 --- a/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs @@ -2,7 +2,7 @@ { public class WorkCategoryMasterMongoDB { - public string? Id { get; set; } + public string Id { get; set; } = string.Empty; public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; } diff --git a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs index 71638a3..850300d 100644 --- a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs @@ -6,7 +6,7 @@ public string WorkAreaId { get; set; } = string.Empty; public ActivityMasterMongoDB? ActivityMaster { get; set; } public WorkCategoryMasterMongoDB? WorkCategoryMaster { get; set; } - public string? ParentTaskId { get; set; } + public string? ParentTaskId { get; set; } = null; public double PlannedWork { get; set; } = 0; public double TodaysAssigned { get; set; } = 0; public double CompletedWork { get; set; } = 0; diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index a440c21..3ae76ed 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -734,16 +734,45 @@ namespace MarcoBMS.Services.Controllers } // Step 4: Fetch WorkItems with related Activity and Work Category data - var workItems = await _context.WorkItems - .Include(wi => wi.ActivityMaster) - .Include(wi => wi.WorkCategoryMaster) - .Where(wi => wi.WorkAreaId == workAreaId) - .ToListAsync(); + var workItemVMs = await _cache.GetWorkItemDetailsByWorkArea(workAreaId); + if (workItemVMs == null) + { + var workItems = await _context.WorkItems + .Include(wi => wi.ActivityMaster) + .Include(wi => wi.WorkCategoryMaster) + .Where(wi => wi.WorkAreaId == workAreaId) + .ToListAsync(); - _logger.LogInfo("{Count} work items fetched successfully for WorkAreaId: {WorkAreaId}", workItems.Count, workAreaId); + workItemVMs = workItems.Select(wi => new WorkItemMongoDB + { + Id = wi.Id.ToString(), + WorkAreaId = wi.WorkAreaId.ToString(), + ParentTaskId = wi.ParentTaskId.ToString(), + ActivityMaster = new ActivityMasterMongoDB + { + Id = wi.ActivityId.ToString(), + ActivityName = wi.ActivityMaster != null ? wi.ActivityMaster.ActivityName : null, + UnitOfMeasurement = wi.ActivityMaster != null ? wi.ActivityMaster.UnitOfMeasurement : null + }, + WorkCategoryMaster = new WorkCategoryMasterMongoDB + { + Id = wi.ActivityId.ToString(), + Name = wi.WorkCategoryMaster != null ? wi.WorkCategoryMaster.Name : "", + Description = wi.WorkCategoryMaster != null ? wi.WorkCategoryMaster.Description : "" + }, + PlannedWork = wi.PlannedWork, + CompletedWork = wi.CompletedWork, + Description = wi.Description, + TaskDate = wi.TaskDate, + }).ToList(); + + await _cache.ManageWorkItemDetails(workItems); + } + + _logger.LogInfo("{Count} work items fetched successfully for WorkAreaId: {WorkAreaId}", workItemVMs.Count, workAreaId); // Step 5: Return result - return Ok(ApiResponse.SuccessResponse(workItems, $"{workItems.Count} records of tasks fetched successfully", 200)); + return Ok(ApiResponse.SuccessResponse(workItemVMs, $"{workItemVMs.Count} records of tasks fetched successfully", 200)); } [HttpPost("task")] @@ -765,6 +794,8 @@ namespace MarcoBMS.Services.Controllers var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); string message = ""; List projectIds = new List(); + var workItemIds = workItemDtos.Where(wi => wi.Id != null && wi.Id != Guid.Empty).Select(wi => wi.Id).ToList(); + var workItems = await _context.WorkItems.AsNoTracking().Where(wi => workItemIds.Contains(wi.Id)).ToListAsync(); foreach (var itemDto in workItemDtos) { @@ -778,6 +809,28 @@ namespace MarcoBMS.Services.Controllers // Update existing workItemsToUpdate.Add(workItem); message = $"Task Updated in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}"; + var existingWorkItem = workItems.FirstOrDefault(wi => wi.Id == workItem.Id); + double plannedWork = 0; + double completedWork = 0; + if (existingWorkItem != null) + { + if (existingWorkItem.PlannedWork != workItem.PlannedWork && existingWorkItem.CompletedWork != workItem.CompletedWork) + { + plannedWork = workItem.PlannedWork - existingWorkItem.PlannedWork; + completedWork = workItem.CompletedWork - existingWorkItem.CompletedWork; + } + else if (existingWorkItem.PlannedWork == workItem.PlannedWork && existingWorkItem.CompletedWork != workItem.CompletedWork) + { + plannedWork = 0; + completedWork = workItem.CompletedWork - existingWorkItem.CompletedWork; + } + else if (existingWorkItem.PlannedWork != workItem.PlannedWork && existingWorkItem.CompletedWork == workItem.CompletedWork) + { + plannedWork = workItem.PlannedWork - existingWorkItem.PlannedWork; + completedWork = 0; + } + await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, plannedWork, completedWork); + } } else { @@ -785,6 +838,7 @@ namespace MarcoBMS.Services.Controllers workItem.Id = Guid.NewGuid(); workItemsToCreate.Add(workItem); message = $"Task Added in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}"; + await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, workItem.PlannedWork, workItem.CompletedWork); } responseList.Add(new WorkItemVM @@ -793,6 +847,7 @@ namespace MarcoBMS.Services.Controllers WorkItem = workItem }); projectIds.Add(building.ProjectId); + } string responseMessage = ""; // Apply DB changes @@ -801,7 +856,7 @@ namespace MarcoBMS.Services.Controllers _logger.LogInfo("Adding {Count} new work items", workItemsToCreate.Count); await _context.WorkItems.AddRangeAsync(workItemsToCreate); responseMessage = "Task Added Successfully"; - + await _cache.ManageWorkItemDetails(workItemsToCreate); } if (workItemsToUpdate.Any()) @@ -809,7 +864,7 @@ namespace MarcoBMS.Services.Controllers _logger.LogInfo("Updating {Count} existing work items", workItemsToUpdate.Count); _context.WorkItems.UpdateRange(workItemsToUpdate); responseMessage = "Task Updated Successfully"; - + await _cache.ManageWorkItemDetails(workItemsToUpdate); } await _context.SaveChangesAsync(); diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 6ff9cfe..ecce8ab 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -109,6 +109,71 @@ namespace Marco.Pms.Services.Helpers return null; } } + public async Task UpdatePlannedAndCompleteWorksInBuilding(Guid workAreaId, double plannedWork = 0, double completedWork = 0) + { + try + { + await _projectCache.UpdatePlannedAndCompleteWorksInBuildingFromCache(workAreaId, plannedWork, completedWork); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while updating planned work and completed work in building infra form Cache: {Error}", ex.Message); + } + } + + // ------------------------------------------------------- WorkItem ------------------------------------------------------- + + public async Task ManageWorkItemDetails(List workItems) + { + try + { + await _projectCache.ManageWorkItemDetailsToCache(workItems); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while saving workItems form Cache: {Error}", ex.Message); + } + } + public async Task?> GetWorkItemDetailsByWorkArea(Guid workAreaId) + { + try + { + var workItems = await _projectCache.GetWorkItemDetailsByWorkAreaFromCache(workAreaId); + if (workItems.Count > 0) + { + return workItems; + } + else + { + return null; + } + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while fetching list of workItems form Cache: {Error}", ex.Message); + return null; + } + } + public async Task GetWorkItemDetailsById(Guid id) + { + try + { + var workItem = await _projectCache.GetWorkItemDetailsByIdFromCache(id); + if (workItem.Id != "") + { + return workItem; + } + else + { + return null; + } + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while fetching list of workItems form Cache: {Error}", ex.Message); + return null; + } + } // ------------------------------------ Employee Profile Cache --------------------------------------- diff --git a/Marco.Pms.Services/appsettings.Development.json b/Marco.Pms.Services/appsettings.Development.json index 5f5e19d..030c450 100644 --- a/Marco.Pms.Services/appsettings.Development.json +++ b/Marco.Pms.Services/appsettings.Development.json @@ -48,6 +48,6 @@ }, "MongoDB": { "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs", - "ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches" + "ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches?socketTimeoutMS=500&serverSelectionTimeoutMS=500&connectTimeoutMS=500" } } From aebb344a5ac78a63ee98cd661ed99342cd4b6731 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 8 Jul 2025 12:20:54 +0530 Subject: [PATCH 038/124] Implemented the cache in task allocation --- Marco.Pms.CacheHelper/ProjectCache.cs | 4 +--- Marco.Pms.Services/Controllers/ProjectController.cs | 4 ++-- Marco.Pms.Services/Controllers/TaskController.cs | 12 +++++++++++- Marco.Pms.Services/Helpers/CacheUpdateHelper.cs | 11 +++++++++++ 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index 6f5a3d3..23df64c 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -26,8 +26,6 @@ namespace Marco.Pms.CacheHelper } public async Task AddProjectDetailsToCache(Project project) { - - //_logger.LogInfo("[AddProjectDetails] Initiated for ProjectId: {ProjectId}", project.Id); var projectDetails = new ProjectMongoDB @@ -544,7 +542,7 @@ namespace Marco.Pms.CacheHelper .FirstOrDefaultAsync(); return workItem; } - public async Task UpdatePlannedAndCompleteWorksInWorkItem(Guid id, double plannedWork = 0, double completedWork = 0, double todaysAssigned = 0) + public async Task UpdatePlannedAndCompleteWorksInWorkItemToCache(Guid id, double plannedWork, double completedWork, double todaysAssigned) { var filter = Builders.Filter.Eq(p => p.Id, id.ToString()); var updates = Builders.Update diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 3ae76ed..e12d2ad 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -29,7 +29,7 @@ namespace MarcoBMS.Services.Controllers private readonly ApplicationDbContext _context; private readonly UserHelper _userHelper; private readonly ILoggingService _logger; - private readonly RolesHelper _rolesHelper; + //private readonly RolesHelper _rolesHelper; private readonly ProjectsHelper _projectsHelper; private readonly IHubContext _signalR; private readonly PermissionServices _permission; @@ -47,7 +47,7 @@ namespace MarcoBMS.Services.Controllers _context = context; _userHelper = userHelper; _logger = logger; - _rolesHelper = rolesHelper; + //_rolesHelper = rolesHelper; _projectsHelper = projectHelper; _signalR = signalR; _cache = cache; diff --git a/Marco.Pms.Services/Controllers/TaskController.cs b/Marco.Pms.Services/Controllers/TaskController.cs index 4ad1f85..68a7132 100644 --- a/Marco.Pms.Services/Controllers/TaskController.cs +++ b/Marco.Pms.Services/Controllers/TaskController.cs @@ -6,6 +6,7 @@ using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Activities; +using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Service; using MarcoBMS.Services.Helpers; @@ -31,11 +32,12 @@ namespace MarcoBMS.Services.Controllers private readonly ILoggingService _logger; private readonly IHubContext _signalR; private readonly PermissionServices _permissionServices; + private readonly CacheUpdateHelper _cache; private readonly Guid Approve_Task; private readonly Guid Assign_Report_Task; public TaskController(ApplicationDbContext context, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permissionServices, - IHubContext signalR) + IHubContext signalR, CacheUpdateHelper cache) { _context = context; _userHelper = userHelper; @@ -43,6 +45,7 @@ namespace MarcoBMS.Services.Controllers _logger = logger; _signalR = signalR; _permissionServices = permissionServices; + _cache = cache; Approve_Task = Guid.Parse("db4e40c5-2ba9-4b6d-b8a6-a16a250ff99c"); Assign_Report_Task = Guid.Parse("6a32379b-8b3f-49a6-8c48-4b7ac1b55dc2"); } @@ -86,6 +89,8 @@ namespace MarcoBMS.Services.Controllers _context.TaskAllocations.Add(taskAllocation); await _context.SaveChangesAsync(); + await _cache.UpdatePlannedAndCompleteWorksInWorkItem(taskAllocation.WorkItemId, todaysAssigned: taskAllocation.PlannedTask); + _logger.LogInfo("Task {TaskId} assigned by Employee {EmployeeId}", taskAllocation.Id, employee.Id); var response = taskAllocation.ToAssignTaskVMFromTaskAllocation(); @@ -259,6 +264,10 @@ namespace MarcoBMS.Services.Controllers } await _context.SaveChangesAsync(); + var selectedWorkAreaId = taskAllocation.WorkItem?.WorkAreaId ?? Guid.Empty; + + await _cache.UpdatePlannedAndCompleteWorksInWorkItem(taskAllocation.WorkItemId, completedWork: taskAllocation.CompletedTask); + await _cache.UpdatePlannedAndCompleteWorksInBuilding(selectedWorkAreaId, completedWork: taskAllocation.CompletedTask); var response = taskAllocation.ToReportTaskVMFromTaskAllocation(); var comments = await _context.TaskComments @@ -679,6 +688,7 @@ namespace MarcoBMS.Services.Controllers /// /// DTO containing task approval details. /// IActionResult indicating success or failure. + [HttpPost("approve")] public async Task ApproveTask(ApproveTaskDto approveTask) { diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index ecce8ab..03fd397 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -174,6 +174,17 @@ namespace Marco.Pms.Services.Helpers return null; } } + public async Task UpdatePlannedAndCompleteWorksInWorkItem(Guid id, double plannedWork = 0, double completedWork = 0, double todaysAssigned = 0) + { + try + { + var response = await _projectCache.UpdatePlannedAndCompleteWorksInWorkItemToCache(id, plannedWork, completedWork, todaysAssigned); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while updating planned work, completed work, and today's assigned work in workItems in Cache: {Error}", ex.Message); + } + } // ------------------------------------ Employee Profile Cache --------------------------------------- From 856510baff8b8b224dd2c06e021af24dec197f4f Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 8 Jul 2025 12:48:13 +0530 Subject: [PATCH 039/124] In Project Report Email only sending data of job role assigned to that project --- Marco.Pms.Services/Controllers/ReportController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Marco.Pms.Services/Controllers/ReportController.cs b/Marco.Pms.Services/Controllers/ReportController.cs index 893c16b..8f8a790 100644 --- a/Marco.Pms.Services/Controllers/ReportController.cs +++ b/Marco.Pms.Services/Controllers/ReportController.cs @@ -232,9 +232,9 @@ namespace Marco.Pms.Services.Controllers double totalPlannedTask = todayAssignedTasks.Sum(t => t.PlannedTask); double totalCompletedTask = todayAssignedTasks.Sum(t => t.CompletedTask); - + var jobRoleIds = projectAllocations.Select(pa => pa.JobRoleId).ToList(); var jobRoles = await _context.JobRoles - .Where(j => j.TenantId == project.TenantId) + .Where(j => j.TenantId == project.TenantId && jobRoleIds.Contains(j.Id)) .ToListAsync(); // Team on site From 5c1dcd89b523759d04a632fd4ce3ffa5347086f4 Mon Sep 17 00:00:00 2001 From: Vikas Nale Date: Tue, 8 Jul 2025 15:12:36 +0530 Subject: [PATCH 040/124] directoryAdmin, directoryManager, and directoryUser are hard-coded in the helper class. Instead of hard-coding, we can create a static class and use it across the application. Ref: https://redmine.marcoaiot.com/issues/387 --- .../Entitlements/PermissionsMaster.cs | 10 ++++ Marco.Pms.Services/Helpers/DirectoryHelper.cs | 53 ++++++++----------- 2 files changed, 33 insertions(+), 30 deletions(-) create mode 100644 Marco.Pms.Model/Entitlements/PermissionsMaster.cs diff --git a/Marco.Pms.Model/Entitlements/PermissionsMaster.cs b/Marco.Pms.Model/Entitlements/PermissionsMaster.cs new file mode 100644 index 0000000..24e115f --- /dev/null +++ b/Marco.Pms.Model/Entitlements/PermissionsMaster.cs @@ -0,0 +1,10 @@ +namespace Marco.Pms.Model.Entitlements +{ + public static class PermissionsMaster + { + public static readonly Guid DirectoryAdmin = Guid.Parse("4286a13b-bb40-4879-8c6d-18e9e393beda"); + public static readonly Guid DirectoryManager = Guid.Parse("62668630-13ce-4f52-a0f0-db38af2230c5"); + public static readonly Guid DirectoryUser = Guid.Parse("0f919170-92d4-4337-abd3-49b66fc871bb"); + + } +} diff --git a/Marco.Pms.Services/Helpers/DirectoryHelper.cs b/Marco.Pms.Services/Helpers/DirectoryHelper.cs index bafa36f..37f58cf 100644 --- a/Marco.Pms.Services/Helpers/DirectoryHelper.cs +++ b/Marco.Pms.Services/Helpers/DirectoryHelper.cs @@ -1,6 +1,7 @@ using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Directory; using Marco.Pms.Model.Dtos.Directory; +using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; @@ -20,9 +21,6 @@ namespace Marco.Pms.Services.Helpers private readonly ILoggingService _logger; private readonly UserHelper _userHelper; private readonly PermissionServices _permissionServices; - private readonly Guid directoryAdmin; - private readonly Guid directoryManager; - private readonly Guid directoryUser; public DirectoryHelper(ApplicationDbContext context, ILoggingService logger, UserHelper userHelper, PermissionServices permissionServices) { @@ -30,13 +28,8 @@ namespace Marco.Pms.Services.Helpers _logger = logger; _userHelper = userHelper; _permissionServices = permissionServices; - directoryAdmin = Guid.Parse("4286a13b-bb40-4879-8c6d-18e9e393beda"); - directoryManager = Guid.Parse("62668630-13ce-4f52-a0f0-db38af2230c5"); - directoryUser = Guid.Parse("0f919170-92d4-4337-abd3-49b66fc871bb"); } - - public async Task> GetListOfContacts(string? search, bool active, ContactFilterDto? filterDto, Guid? projectId) { Guid tenantId = _userHelper.GetTenantId(); @@ -45,12 +38,12 @@ namespace Marco.Pms.Services.Helpers var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync(); List? employeeBuckets = await _context.EmployeeBucketMappings.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).ToListAsync(); List bucketIds = employeeBuckets.Select(c => c.BucketId).ToList(); - if (permissionIds.Contains(directoryAdmin)) + if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) { var buckets = await _context.Buckets.Where(b => b.TenantId == tenantId).ToListAsync(); bucketIds = buckets.Select(b => b.Id).ToList(); } - else if (permissionIds.Contains(directoryManager) || permissionIds.Contains(directoryUser)) + else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) || permissionIds.Contains(PermissionsMaster.DirectoryUser)) { var buckets = await _context.Buckets.Where(b => b.CreatedByID == LoggedInEmployee.Id).ToListAsync(); var createdBucketIds = buckets.Select(b => b.Id).ToList(); @@ -199,11 +192,11 @@ namespace Marco.Pms.Services.Helpers var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync(); EmployeeBucketMapping? employeeBucket = null; - if (permissionIds.Contains(directoryAdmin)) + if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) { employeeBucket = employeeBuckets.FirstOrDefault(); } - else if (permissionIds.Contains(directoryManager) || permissionIds.Contains(directoryUser)) + else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) || permissionIds.Contains(PermissionsMaster.DirectoryUser)) { employeeBucket = employeeBuckets.FirstOrDefault(eb => eb.EmployeeId == LoggedInEmployee.Id); } @@ -483,12 +476,12 @@ namespace Marco.Pms.Services.Helpers var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync(); List? employeeBuckets = await _context.EmployeeBucketMappings.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).ToListAsync(); List bucketIds = employeeBuckets.Select(c => c.BucketId).ToList(); - if (permissionIds.Contains(directoryAdmin)) + if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) { var buckets = await _context.Buckets.Where(b => b.TenantId == tenantId).ToListAsync(); bucketIds = buckets.Select(b => b.Id).ToList(); } - else if (permissionIds.Contains(directoryManager) || permissionIds.Contains(directoryUser)) + else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) || permissionIds.Contains(PermissionsMaster.DirectoryUser)) { var buckets = await _context.Buckets.Where(b => b.CreatedByID == LoggedInEmployee.Id).ToListAsync(); var createdBucketIds = buckets.Select(b => b.Id).ToList(); @@ -919,9 +912,9 @@ namespace Marco.Pms.Services.Helpers } // --- Permission Checks --- - var hasAdminPermission = await _permissionServices.HasPermission(directoryAdmin, loggedInEmployee.Id); - var hasManagerPermission = await _permissionServices.HasPermission(directoryManager, loggedInEmployee.Id); - var hasUserPermission = await _permissionServices.HasPermission(directoryUser, loggedInEmployee.Id); + var hasAdminPermission = await _permissionServices.HasPermission(PermissionsMaster.DirectoryAdmin, loggedInEmployee.Id); + var hasManagerPermission = await _permissionServices.HasPermission(PermissionsMaster.DirectoryAdmin, loggedInEmployee.Id); + var hasUserPermission = await _permissionServices.HasPermission(PermissionsMaster.DirectoryUser, loggedInEmployee.Id); IQueryable notesQuery = _context.ContactNotes .Include(cn => cn.UpdatedBy) @@ -1166,11 +1159,11 @@ namespace Marco.Pms.Services.Helpers var bucketIds = employeeBuckets.Select(b => b.BucketId).ToList(); List employeeBucketVM = await _context.EmployeeBucketMappings.Where(b => bucketIds.Contains(b.BucketId)).ToListAsync(); List bucketList = new List(); - if (permissionIds.Contains(directoryAdmin)) + if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) { bucketList = await _context.Buckets.Include(b => b.CreatedBy).Where(b => b.TenantId == tenantId).ToListAsync(); } - else if (permissionIds.Contains(directoryManager) || permissionIds.Contains(directoryUser)) + else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) || permissionIds.Contains(PermissionsMaster.DirectoryUser)) { bucketList = await _context.Buckets.Include(b => b.CreatedBy).Where(b => bucketIds.Contains(b.Id) || b.CreatedByID == LoggedInEmployee.Id).ToListAsync(); } @@ -1208,8 +1201,8 @@ namespace Marco.Pms.Services.Helpers { var assignedRoleIds = await _context.EmployeeRoleMappings.Where(r => r.EmployeeId == LoggedInEmployee.Id).Select(r => r.RoleId).ToListAsync(); var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync(); - var demo = !permissionIds.Contains(directoryUser); - if (!permissionIds.Contains(directoryAdmin) && !permissionIds.Contains(directoryManager) && !permissionIds.Contains(directoryUser)) + var demo = !permissionIds.Contains(PermissionsMaster.DirectoryUser); + if (!permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && !permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && !permissionIds.Contains(PermissionsMaster.DirectoryUser)) { _logger.LogError("Employee {EmployeeId} attemped to create a bucket, but do not have permission", LoggedInEmployee.Id); return ApiResponse.ErrorResponse("You don't have permission", "You don't have permission", 401); @@ -1266,15 +1259,15 @@ namespace Marco.Pms.Services.Helpers } Bucket? accessableBucket = null; - if (permissionIds.Contains(directoryAdmin)) + if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) { accessableBucket = bucket; } - else if (permissionIds.Contains(directoryManager) && bucketIds.Contains(id)) + else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && bucketIds.Contains(id)) { accessableBucket = bucket; } - else if (permissionIds.Contains(directoryUser)) + else if (permissionIds.Contains(PermissionsMaster.DirectoryUser)) { if (bucket.CreatedByID == LoggedInEmployee.Id) { @@ -1332,15 +1325,15 @@ namespace Marco.Pms.Services.Helpers var bucketIds = employeeBuckets.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).Select(eb => eb.BucketId).ToList(); var employeeBucketIds = employeeBuckets.Select(eb => eb.EmployeeId).ToList(); Bucket? accessableBucket = null; - if (permissionIds.Contains(directoryAdmin)) + if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) { accessableBucket = bucket; } - else if (permissionIds.Contains(directoryManager) && bucketIds.Contains(bucketId)) + else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && bucketIds.Contains(bucketId)) { accessableBucket = bucket; } - else if (permissionIds.Contains(directoryUser)) + else if (permissionIds.Contains(PermissionsMaster.DirectoryUser)) { if (bucket.CreatedByID == LoggedInEmployee.Id) { @@ -1433,15 +1426,15 @@ namespace Marco.Pms.Services.Helpers var bucketIds = employeeBuckets.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).Select(eb => eb.BucketId).ToList(); Bucket? accessableBucket = null; - if (permissionIds.Contains(directoryAdmin)) + if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) { accessableBucket = bucket; } - else if (permissionIds.Contains(directoryManager) && bucketIds.Contains(id)) + else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && bucketIds.Contains(id)) { accessableBucket = bucket; } - else if (permissionIds.Contains(directoryUser)) + else if (permissionIds.Contains(PermissionsMaster.DirectoryUser)) { if (bucket.CreatedByID == LoggedInEmployee.Id) { From 2304912bf88af07656bd5d62fae5473e5fea60f1 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 8 Jul 2025 16:07:27 +0530 Subject: [PATCH 041/124] Added all Feature permission in static file and use that static file to check the permission --- .../Entitlements/PermissionsMaster.cs | 20 ++++++++++++++++++- .../Controllers/EmployeeController.cs | 8 ++------ .../Controllers/TaskController.cs | 10 +++------- .../Controllers/UserController.cs | 9 +++++---- Marco.Pms.Services/Helpers/MasterHelper.cs | 13 +++++------- Marco.Pms.Services/Helpers/ProjectsHelper.cs | 4 ---- 6 files changed, 34 insertions(+), 30 deletions(-) diff --git a/Marco.Pms.Model/Entitlements/PermissionsMaster.cs b/Marco.Pms.Model/Entitlements/PermissionsMaster.cs index 24e115f..d0bef58 100644 --- a/Marco.Pms.Model/Entitlements/PermissionsMaster.cs +++ b/Marco.Pms.Model/Entitlements/PermissionsMaster.cs @@ -5,6 +5,24 @@ public static readonly Guid DirectoryAdmin = Guid.Parse("4286a13b-bb40-4879-8c6d-18e9e393beda"); public static readonly Guid DirectoryManager = Guid.Parse("62668630-13ce-4f52-a0f0-db38af2230c5"); public static readonly Guid DirectoryUser = Guid.Parse("0f919170-92d4-4337-abd3-49b66fc871bb"); - + public static readonly Guid ViewProject = Guid.Parse("6ea44136-987e-44ba-9e5d-1cf8f5837ebc"); + public static readonly Guid ManageProject = Guid.Parse("172fc9b6-755b-4f62-ab26-55c34a330614"); + public static readonly Guid ManageTeam = Guid.Parse("b94802ce-0689-4643-9e1d-11c86950c35b"); + public static readonly Guid ViewProjectInfra = Guid.Parse("8d7cc6e3-9147-41f7-aaa7-fa507e450bd4"); + public static readonly Guid ManageProjectInfra = Guid.Parse("cf2825ad-453b-46aa-91d9-27c124d63373"); + public static readonly Guid ViewTask = Guid.Parse("9fcc5f87-25e3-4846-90ac-67a71ab92e3c"); + public static readonly Guid AddAndEditTask = Guid.Parse("08752f33-3b29-4816-b76b-ea8a968ed3c5"); + public static readonly Guid AssignAndReportProgress = Guid.Parse("6a32379b-8b3f-49a6-8c48-4b7ac1b55dc2"); + public static readonly Guid ApproveTask = Guid.Parse("db4e40c5-2ba9-4b6d-b8a6-a16a250ff99c"); + public static readonly Guid ViewAllEmployees = Guid.Parse("60611762-7f8a-4fb5-b53f-b1139918796b"); + public static readonly Guid ViewTeamMembers = Guid.Parse("b82d2b7e-0d52-45f3-997b-c008ea460e7f"); + public static readonly Guid AddAndEditEmployee = Guid.Parse("a97d366a-c2bb-448d-be93-402bd2324566"); + public static readonly Guid AssignRoles = Guid.Parse("fbd213e0-0250-46f1-9f5f-4b2a1e6e76a3"); + public static readonly Guid TeamAttendance = Guid.Parse("915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"); + public static readonly Guid RegularizeAttendance = Guid.Parse("57802c4a-00aa-4a1f-a048-fd2f70dd44b6"); + public static readonly Guid SelfAttendance = Guid.Parse("ccb0589f-712b-43de-92ed-5b6088e7dc4e"); + public static readonly Guid ViewMasters = Guid.Parse("5ffbafe0-7ab0-48b1-bb50-c1bf76b65f9d"); + public static readonly Guid ManageMasters = Guid.Parse("588a8824-f924-4955-82d8-fc51956cf323"); } } + diff --git a/Marco.Pms.Services/Controllers/EmployeeController.cs b/Marco.Pms.Services/Controllers/EmployeeController.cs index 698dd67..9884e53 100644 --- a/Marco.Pms.Services/Controllers/EmployeeController.cs +++ b/Marco.Pms.Services/Controllers/EmployeeController.cs @@ -38,8 +38,6 @@ namespace MarcoBMS.Services.Controllers private readonly IHubContext _signalR; private readonly PermissionServices _permission; private readonly ProjectsHelper _projectsHelper; - private readonly Guid ViewAllEmployees; - private readonly Guid ViewTeamMembers; private readonly Guid tenantId; @@ -56,8 +54,6 @@ namespace MarcoBMS.Services.Controllers _logger = logger; _signalR = signalR; _permission = permission; - ViewAllEmployees = Guid.Parse("60611762-7f8a-4fb5-b53f-b1139918796b"); - ViewTeamMembers = Guid.Parse("b82d2b7e-0d52-45f3-997b-c008ea460e7f"); _projectsHelper = projectsHelper; tenantId = _userHelper.GetTenantId(); } @@ -126,8 +122,8 @@ namespace MarcoBMS.Services.Controllers List projects = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); var projectIds = projects.Select(p => p.Id).ToList(); - var hasViewAllEmployeesPermission = await _permission.HasPermission(ViewAllEmployees, loggedInEmployee.Id); - var hasViewTeamMembersPermission = await _permission.HasPermission(ViewTeamMembers, loggedInEmployee.Id); + var hasViewAllEmployeesPermission = await _permission.HasPermission(PermissionsMaster.ViewAllEmployees, loggedInEmployee.Id); + var hasViewTeamMembersPermission = await _permission.HasPermission(PermissionsMaster.ViewTeamMembers, loggedInEmployee.Id); List result = new(); diff --git a/Marco.Pms.Services/Controllers/TaskController.cs b/Marco.Pms.Services/Controllers/TaskController.cs index 4ad1f85..ca24f1a 100644 --- a/Marco.Pms.Services/Controllers/TaskController.cs +++ b/Marco.Pms.Services/Controllers/TaskController.cs @@ -31,8 +31,6 @@ namespace MarcoBMS.Services.Controllers private readonly ILoggingService _logger; private readonly IHubContext _signalR; private readonly PermissionServices _permissionServices; - private readonly Guid Approve_Task; - private readonly Guid Assign_Report_Task; public TaskController(ApplicationDbContext context, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permissionServices, IHubContext signalR) @@ -43,8 +41,6 @@ namespace MarcoBMS.Services.Controllers _logger = logger; _signalR = signalR; _permissionServices = permissionServices; - Approve_Task = Guid.Parse("db4e40c5-2ba9-4b6d-b8a6-a16a250ff99c"); - Assign_Report_Task = Guid.Parse("6a32379b-8b3f-49a6-8c48-4b7ac1b55dc2"); } private Guid GetTenantId() @@ -72,7 +68,7 @@ namespace MarcoBMS.Services.Controllers var employee = await _userHelper.GetCurrentEmployeeAsync(); // Check for permission to approve tasks - var hasPermission = await _permissionServices.HasPermission(Assign_Report_Task, employee.Id); + var hasPermission = await _permissionServices.HasPermission(PermissionsMaster.AssignAndReportProgress, employee.Id); if (!hasPermission) { _logger.LogWarning("Employee {EmployeeId} attempted to assign Task without permission", employee.Id); @@ -136,7 +132,7 @@ namespace MarcoBMS.Services.Controllers var tenantId = GetTenantId(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var hasPermission = await _permissionServices.HasPermission(Assign_Report_Task, loggedInEmployee.Id); + var hasPermission = await _permissionServices.HasPermission(PermissionsMaster.AssignAndReportProgress, loggedInEmployee.Id); if (!hasPermission) { _logger.LogWarning("Unauthorized task report attempt by Employee {EmployeeId} for Task {TaskId}", loggedInEmployee.Id, reportTask.Id); @@ -701,7 +697,7 @@ namespace MarcoBMS.Services.Controllers } // Check for permission to approve tasks - var hasPermission = await _permissionServices.HasPermission(Approve_Task, loggedInEmployee.Id); + var hasPermission = await _permissionServices.HasPermission(PermissionsMaster.ApproveTask, loggedInEmployee.Id); if (!hasPermission) { _logger.LogWarning("Employee {EmployeeId} attempted to approve Task {TaskId} without permission", loggedInEmployee.Id, approveTask.Id); diff --git a/Marco.Pms.Services/Controllers/UserController.cs b/Marco.Pms.Services/Controllers/UserController.cs index 2d33b15..2aeb208 100644 --- a/Marco.Pms.Services/Controllers/UserController.cs +++ b/Marco.Pms.Services/Controllers/UserController.cs @@ -22,7 +22,7 @@ namespace MarcoBMS.Services.Controllers private readonly ProjectsHelper _projectsHelper; private readonly RolesHelper _rolesHelper; - public UserController(EmployeeHelper employeeHelper, ProjectsHelper projectsHelper, UserHelper userHelper, RolesHelper rolesHelper) + public UserController(EmployeeHelper employeeHelper, ProjectsHelper projectsHelper, UserHelper userHelper, RolesHelper rolesHelper) { _userHelper = userHelper; _employeeHelper = employeeHelper; @@ -45,7 +45,7 @@ namespace MarcoBMS.Services.Controllers var user = await _userHelper.GetCurrentUserAsync(); Employee emp = new Employee { }; - if(user != null) + if (user != null) { emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id); } @@ -54,9 +54,10 @@ namespace MarcoBMS.Services.Controllers string[] projectsId = []; /* User with permission manage project can see all projects */ - if (featurePermission != null && featurePermission.Exists(c => c.Id.ToString() == "172fc9b6-755b-4f62-ab26-55c34a330614")) { + if (featurePermission != null && featurePermission.Exists(c => c.Id.ToString() == "172fc9b6-755b-4f62-ab26-55c34a330614")) + { List projects = await _projectsHelper.GetAllProjectByTanentID(emp.TenantId); - projectsId = projects.Select(c=>c.Id.ToString()).ToArray(); + projectsId = projects.Select(c => c.Id.ToString()).ToArray(); } else { diff --git a/Marco.Pms.Services/Helpers/MasterHelper.cs b/Marco.Pms.Services/Helpers/MasterHelper.cs index cdad89c..f994639 100644 --- a/Marco.Pms.Services/Helpers/MasterHelper.cs +++ b/Marco.Pms.Services/Helpers/MasterHelper.cs @@ -2,6 +2,7 @@ using Marco.Pms.Model.Directory; using Marco.Pms.Model.Dtos.Master; using Marco.Pms.Model.Employees; +using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Master; using Marco.Pms.Model.Utilities; @@ -19,8 +20,6 @@ namespace Marco.Pms.Services.Helpers private readonly ILoggingService _logger; private readonly UserHelper _userHelper; private readonly PermissionServices _permissionService; - private readonly Guid View_Master; - private readonly Guid Manage_Master; public MasterHelper(ApplicationDbContext context, ILoggingService logger, UserHelper userHelper, PermissionServices permissionServices) @@ -29,8 +28,6 @@ namespace Marco.Pms.Services.Helpers _logger = logger; _userHelper = userHelper; _permissionService = permissionServices; - View_Master = Guid.Parse("5ffbafe0-7ab0-48b1-bb50-c1bf76b65f9d"); - Manage_Master = Guid.Parse("588a8824-f924-4955-82d8-fc51956cf323"); } // -------------------------------- Contact Category -------------------------------- public async Task> CreateContactCategory(CreateContactCategoryDto contactCategoryDto) @@ -267,7 +264,7 @@ namespace Marco.Pms.Services.Helpers var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); // Step 2: Check permission to view master data - bool hasViewPermission = await _permissionService.HasPermission(View_Master, loggedInEmployee.Id); + bool hasViewPermission = await _permissionService.HasPermission(PermissionsMaster.ViewMasters, loggedInEmployee.Id); if (!hasViewPermission) { _logger.LogWarning("Access denied for employeeId: {EmployeeId}", loggedInEmployee.Id); @@ -312,7 +309,7 @@ namespace Marco.Pms.Services.Helpers var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); // Step 2: Check if user has permission to manage master data - var hasManageMasterPermission = await _permissionService.HasPermission(Manage_Master, loggedInEmployee.Id); + var hasManageMasterPermission = await _permissionService.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasManageMasterPermission) { _logger.LogWarning("Access denied for employeeId: {EmployeeId}", loggedInEmployee.Id); @@ -368,7 +365,7 @@ namespace Marco.Pms.Services.Helpers var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); // Step 3: Check permissions - var hasManageMasterPermission = await _permissionService.HasPermission(Manage_Master, loggedInEmployee.Id); + var hasManageMasterPermission = await _permissionService.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasManageMasterPermission) { _logger.LogWarning("Access denied. EmployeeId: {EmployeeId} does not have Manage Master permission.", loggedInEmployee.Id); @@ -421,7 +418,7 @@ namespace Marco.Pms.Services.Helpers var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); // Step 2: Check permission to manage master data - var hasManageMasterPermission = await _permissionService.HasPermission(Manage_Master, loggedInEmployee.Id); + var hasManageMasterPermission = await _permissionService.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasManageMasterPermission) { _logger.LogWarning("Delete denied. EmployeeId: {EmployeeId} lacks Manage_Master permission.", loggedInEmployee.Id); diff --git a/Marco.Pms.Services/Helpers/ProjectsHelper.cs b/Marco.Pms.Services/Helpers/ProjectsHelper.cs index 8ccbc85..50bb9a0 100644 --- a/Marco.Pms.Services/Helpers/ProjectsHelper.cs +++ b/Marco.Pms.Services/Helpers/ProjectsHelper.cs @@ -2,11 +2,7 @@ using Marco.Pms.Model.Employees; using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Projects; -using Marco.Pms.Model.Utilities; -using Marco.Pms.Model.ViewModels.Projects; -using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using ModelServices.Helpers; namespace MarcoBMS.Services.Helpers { From de5485b8f61ef5e3f7457accea41e2c2039424c6 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 9 Jul 2025 10:35:35 +0530 Subject: [PATCH 042/124] Changed the signalR keyword for work item --- Marco.Pms.Services/Controllers/ProjectController.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index e12d2ad..022729d 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -793,7 +793,7 @@ namespace MarcoBMS.Services.Controllers var responseList = new List(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); string message = ""; - List projectIds = new List(); + List workAreaIds = new List(); var workItemIds = workItemDtos.Where(wi => wi.Id != null && wi.Id != Guid.Empty).Select(wi => wi.Id).ToList(); var workItems = await _context.WorkItems.AsNoTracking().Where(wi => workItemIds.Contains(wi.Id)).ToListAsync(); @@ -846,7 +846,7 @@ namespace MarcoBMS.Services.Controllers WorkItemId = workItem.Id, WorkItem = workItem }); - projectIds.Add(building.ProjectId); + workAreaIds.Add(workItem.WorkAreaId); } string responseMessage = ""; @@ -873,7 +873,7 @@ namespace MarcoBMS.Services.Controllers - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = message }; + var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = message }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); @@ -885,7 +885,7 @@ namespace MarcoBMS.Services.Controllers { Guid tenantId = _userHelper.GetTenantId(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - List projectIds = new List(); + List workAreaIds = new List(); WorkItem? task = await _context.WorkItems.AsNoTracking().Include(t => t.WorkArea).FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId); if (task != null) { @@ -902,9 +902,9 @@ namespace MarcoBMS.Services.Controllers var floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == floorId); - projectIds.Add(floor?.Building?.ProjectId ?? Guid.Empty); + workAreaIds.Add(task.WorkAreaId); - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = $"Task Deleted in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}, in Area: {task.WorkArea?.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}" }; + var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = $"Task Deleted in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}, in Area: {task.WorkArea?.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}" }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); } else From c4ad3fdad5eb3ee4bb0cee27dcb6d56bbb1d1c13 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 9 Jul 2025 12:39:27 +0530 Subject: [PATCH 043/124] Added the caching project report API and added expiry in workItems in cache --- Marco.Pms.CacheHelper/ProjectCache.cs | 72 ++++- .../MongoDBModels/BuildingMongoDB.cs | 6 +- Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs | 4 + .../MongoDBModels/WorkAreaInfoMongoDB.cs | 13 + .../MongoDBModels/WorkAreaMongoDB.cs | 1 + .../MongoDBModels/WorkItemMongoDB.cs | 1 + .../Controllers/ProjectController.cs | 4 + .../Controllers/ReportController.cs | 161 +--------- Marco.Pms.Services/Dockerfile | 2 +- .../Helpers/CacheUpdateHelper.cs | 30 ++ Marco.Pms.Services/Helpers/ReportHelper.cs | 274 ++++++++++++++++++ Marco.Pms.Services/Program.cs | 1 + 12 files changed, 411 insertions(+), 158 deletions(-) create mode 100644 Marco.Pms.Model/MongoDBModels/WorkAreaInfoMongoDB.cs create mode 100644 Marco.Pms.Services/Helpers/ReportHelper.cs diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index 23df64c..9b2036d 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -106,6 +106,7 @@ namespace Marco.Pms.CacheHelper workAreaMongoList.Add(new WorkAreaMongoDB { Id = wa.Id.ToString(), + FloorId = wa.FloorId.ToString(), AreaName = wa.AreaName, PlannedWork = waPlanned, CompletedWork = waCompleted @@ -118,6 +119,7 @@ namespace Marco.Pms.CacheHelper floorMongoList.Add(new FloorMongoDB { Id = floor.Id.ToString(), + BuildingId = floor.BuildingId.ToString(), FloorName = floor.FloorName, PlannedWork = floorPlanned, CompletedWork = floorCompleted, @@ -131,6 +133,7 @@ namespace Marco.Pms.CacheHelper buildingMongoList.Add(new BuildingMongoDB { Id = building.Id.ToString(), + ProjectId = building.ProjectId.ToString(), BuildingName = building.Name, Description = building.Description, PlannedWork = buildingPlanned, @@ -477,7 +480,59 @@ namespace Marco.Pms.CacheHelper var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); } + public async Task GetBuildingAndFloorByWorkAreaIdFromCache(Guid workAreaId) + { + var pipeline = new[] + { + new BsonDocument("$unwind", "$Buildings"), + new BsonDocument("$unwind", "$Buildings.Floors"), + new BsonDocument("$unwind", "$Buildings.Floors.WorkAreas"), + new BsonDocument("$match", new BsonDocument("Buildings.Floors.WorkAreas._id", workAreaId.ToString())), + new BsonDocument("$project", new BsonDocument + { + { "_id", 0 }, + { "ProjectId", "$_id" }, + { "ProjectName", "$Name" }, + { "PlannedWork", "$PlannedWork" }, + { "CompletedWork", "$CompletedWork" }, + { + "Building", new BsonDocument + { + { "_id", "$Buildings._id" }, + { "BuildingName", "$Buildings.BuildingName" }, + { "Description", "$Buildings.Description" }, + { "PlannedWork", "$Buildings.PlannedWork" }, + { "CompletedWork", "$Buildings.CompletedWork" } + } + }, + { + "Floor", new BsonDocument + { + { "_id", "$Buildings.Floors._id" }, + { "FloorName", "$Buildings.Floors.FloorName" }, + { "PlannedWork", "$Buildings.Floors.PlannedWork" }, + { "CompletedWork", "$Buildings.Floors.CompletedWork" } + } + }, + { "WorkArea", "$Buildings.Floors.WorkAreas" } + }) + }; + var result = await _projetCollection.Aggregate(pipeline).FirstOrDefaultAsync(); + if (result == null) + return null; + return result; + } + public async Task> GetWorkItemsByWorkAreaIdsFromCache(List workAreaIds) + { + var stringWorkAreaIds = workAreaIds.Select(wa => wa.ToString()).ToList(); + var filter = Builders.Filter.In(w => w.WorkAreaId, stringWorkAreaIds); + var workItems = await _taskCollection // replace with your actual collection name + .Find(filter) + .ToListAsync(); + + return workItems; + } // ------------------------------------------------------- WorkItem ------------------------------------------------------- @@ -485,12 +540,14 @@ namespace Marco.Pms.CacheHelper { var activityIds = workItems.Select(wi => wi.ActivityId).ToList(); var workCategoryIds = workItems.Select(wi => wi.WorkCategoryId).ToList(); + var workItemIds = workItems.Select(wi => wi.Id).ToList(); // fetching Activity master var activities = await _context.ActivityMasters.Where(a => activityIds.Contains(a.Id)).ToListAsync() ?? new List(); // Fetching Work Category var workCategories = await _context.WorkCategoryMasters.Where(wc => workCategoryIds.Contains(wc.Id)).ToListAsync() ?? new List(); - + var task = await _context.TaskAllocations.Where(t => workItemIds.Contains(t.WorkItemId) && t.AssignmentDate == DateTime.UtcNow).ToListAsync(); + var todaysAssign = task.Sum(t => t.PlannedTask); foreach (WorkItem workItem in workItems) { var activity = activities.FirstOrDefault(a => a.Id == workItem.ActivityId) ?? new ActivityMaster(); @@ -501,10 +558,11 @@ namespace Marco.Pms.CacheHelper Builders.Update.Set(r => r.WorkAreaId, workItem.WorkAreaId.ToString()), Builders.Update.Set(r => r.ParentTaskId, (workItem.ParentTaskId != null ? workItem.ParentTaskId.ToString() : null)), Builders.Update.Set(r => r.PlannedWork, workItem.PlannedWork), - Builders.Update.Set(r => r.TodaysAssigned, 0), + Builders.Update.Set(r => r.TodaysAssigned, todaysAssign), Builders.Update.Set(r => r.CompletedWork, workItem.CompletedWork), Builders.Update.Set(r => r.Description, workItem.Description), Builders.Update.Set(r => r.TaskDate, workItem.TaskDate), + Builders.Update.Set(r => r.ExpireAt, DateTime.UtcNow.Date.AddDays(1)), Builders.Update.Set(r => r.ActivityMaster, new ActivityMasterMongoDB { Id = activity.Id.ToString(), @@ -520,6 +578,16 @@ namespace Marco.Pms.CacheHelper ); var options = new UpdateOptions { IsUpsert = true }; var result = await _taskCollection.UpdateOneAsync(filter, updates, options); + if (result.UpsertedId != null) + { + var indexKeys = Builders.IndexKeys.Ascending(x => x.ExpireAt); + var indexOptions = new CreateIndexOptions + { + ExpireAfter = TimeSpan.Zero // required for fixed expiration time + }; + var indexModel = new CreateIndexModel(indexKeys, indexOptions); + await _taskCollection.Indexes.CreateOneAsync(indexModel); + } } } public async Task> GetWorkItemDetailsByWorkAreaFromCache(Guid workAreaId) diff --git a/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs b/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs index 64ccbce..786ceb5 100644 --- a/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs @@ -7,12 +7,16 @@ public string? Description { get; set; } public double PlannedWork { get; set; } public double CompletedWork { get; set; } + public string ProjectId { get; set; } = string.Empty; public List Floors { get; set; } = new List(); } public class BuildingMongoDBVM { public string Id { get; set; } = string.Empty; - public string? Name { get; set; } + public string? BuildingName { get; set; } public string? Description { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } + public string ProjectId { get; set; } = string.Empty; } } diff --git a/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs b/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs index 57257a4..15d3060 100644 --- a/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs @@ -3,6 +3,7 @@ public class FloorMongoDB { public string Id { get; set; } = string.Empty; + public string BuildingId { get; set; } = string.Empty; public string? FloorName { get; set; } public double PlannedWork { get; set; } public double CompletedWork { get; set; } @@ -12,6 +13,9 @@ public class FloorMongoDBVM { public string Id { get; set; } = string.Empty; + public string BuildingId { get; set; } = string.Empty; public string? FloorName { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } } } diff --git a/Marco.Pms.Model/MongoDBModels/WorkAreaInfoMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkAreaInfoMongoDB.cs new file mode 100644 index 0000000..da1001b --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/WorkAreaInfoMongoDB.cs @@ -0,0 +1,13 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class WorkAreaInfoMongoDB + { + public string ProjectId { get; set; } = string.Empty; + public string? ProjectName { get; set; } + public BuildingMongoDBVM? Building { get; set; } + public FloorMongoDBVM? Floor { get; set; } + public WorkAreaMongoDB? WorkArea { get; set; } + public double CompletedWork { get; set; } + public double PlannedWork { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs index d17f52c..412c940 100644 --- a/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs @@ -3,6 +3,7 @@ public class WorkAreaMongoDB { public string Id { get; set; } = string.Empty; + public string FloorId { get; set; } = string.Empty; public string? AreaName { get; set; } public double PlannedWork { get; set; } public double CompletedWork { get; set; } diff --git a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs index 850300d..cf798f3 100644 --- a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs @@ -12,5 +12,6 @@ public double CompletedWork { get; set; } = 0; public string? Description { get; set; } public DateTime TaskDate { get; set; } + public DateTime ExpireAt { get; set; } = DateTime.UtcNow.Date.AddDays(1); } } diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 022729d..620ae75 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -235,6 +235,10 @@ namespace MarcoBMS.Services.Controllers .Include(c => c.ProjectStatus) .FirstOrDefaultAsync(c => c.TenantId == tenantId && c.Id == id); projectVM = GetProjectViewModel(project); + if (project != null) + { + await _cache.AddProjectDetails(project); + } } else { diff --git a/Marco.Pms.Services/Controllers/ReportController.cs b/Marco.Pms.Services/Controllers/ReportController.cs index 8f8a790..11dec58 100644 --- a/Marco.Pms.Services/Controllers/ReportController.cs +++ b/Marco.Pms.Services/Controllers/ReportController.cs @@ -1,12 +1,10 @@ using System.Data; -using System.Globalization; using Marco.Pms.DataAccess.Data; -using Marco.Pms.Model.Dtos.Attendance; using Marco.Pms.Model.Dtos.Mail; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Mail; using Marco.Pms.Model.Utilities; -using Marco.Pms.Model.ViewModels.Report; +using Marco.Pms.Services.Helpers; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; @@ -26,13 +24,15 @@ namespace Marco.Pms.Services.Controllers private readonly ILoggingService _logger; private readonly UserHelper _userHelper; private readonly IWebHostEnvironment _env; - public ReportController(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, UserHelper userHelper, IWebHostEnvironment env) + private readonly ReportHelper _reportHelper; + public ReportController(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, UserHelper userHelper, IWebHostEnvironment env, ReportHelper reportHelper) { _context = context; _emailSender = emailSender; _logger = logger; _userHelper = userHelper; _env = env; + _reportHelper = reportHelper; } [HttpPost("set-mail")] @@ -151,7 +151,6 @@ namespace Marco.Pms.Services.Controllers /// An ApiResponse indicating the success or failure of retrieving statistics and sending the email. private async Task> GetProjectStatistics(Guid projectId, List recipientEmails, string body, string subject, Guid tenantId) { - DateTime reportDate = DateTime.UtcNow.AddDays(-1).Date; if (projectId == Guid.Empty) { @@ -159,161 +158,15 @@ namespace Marco.Pms.Services.Controllers return ApiResponse.ErrorResponse("Provided empty Project ID.", "Provided empty Project ID.", 400); } - var project = await _context.Projects - .AsNoTracking() - .FirstOrDefaultAsync(p => p.Id == projectId); - if (project == null) + var statisticReport = await _reportHelper.GetDailyProjectReport(projectId, tenantId); + + if (statisticReport == null) { _logger.LogWarning("User attempted to fetch project progress for project ID {ProjectId} but not found.", projectId); return ApiResponse.ErrorResponse("Project not found.", "Project not found.", 404); } - var statisticReport = new ProjectStatisticReport - { - Date = reportDate, - ProjectName = project.Name ?? "", - TimeStamp = DateTime.Now.ToString("dd-MMM-yyyy HH:mm:ss", CultureInfo.InvariantCulture) - }; - - // Preload relevant data - var projectAllocations = await _context.ProjectAllocations - .Include(p => p.Employee) - .Where(p => p.ProjectId == project.Id && p.IsActive) - .ToListAsync(); - - var assignedEmployeeIds = projectAllocations.Select(p => p.EmployeeId).ToHashSet(); - - var attendances = await _context.Attendes - .AsNoTracking() - .Where(a => a.ProjectID == project.Id && a.InTime != null && a.InTime.Value.Date == reportDate) - .ToListAsync(); - - var checkedInEmployeeIds = attendances.Select(a => a.EmployeeID).Distinct().ToHashSet(); - var checkoutPendingIds = attendances.Where(a => a.OutTime == null).Select(a => a.EmployeeID).Distinct().ToHashSet(); - var regularizationIds = attendances - .Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE) - .Select(a => a.EmployeeID).Distinct().ToHashSet(); - - // Preload buildings, floors, areas - var buildings = await _context.Buildings.Where(b => b.ProjectId == project.Id).ToListAsync(); - var buildingIds = buildings.Select(b => b.Id).ToList(); - - var floors = await _context.Floor.Where(f => buildingIds.Contains(f.BuildingId)).ToListAsync(); - var floorIds = floors.Select(f => f.Id).ToList(); - - var areas = await _context.WorkAreas.Where(a => floorIds.Contains(a.FloorId)).ToListAsync(); - var areaIds = areas.Select(a => a.Id).ToList(); - - var workItems = await _context.WorkItems - .Include(w => w.ActivityMaster) - .Where(w => areaIds.Contains(w.WorkAreaId)) - .ToListAsync(); - - var itemIds = workItems.Select(i => i.Id).ToList(); - - var tasks = await _context.TaskAllocations - .Where(t => itemIds.Contains(t.WorkItemId)) - .ToListAsync(); - - var taskIds = tasks.Select(t => t.Id).ToList(); - - var taskMembers = await _context.TaskMembers - .Include(m => m.Employee) - .Where(m => taskIds.Contains(m.TaskAllocationId)) - .ToListAsync(); - - // Aggregate data - double totalPlannedWork = workItems.Sum(w => w.PlannedWork); - double totalCompletedWork = workItems.Sum(w => w.CompletedWork); - - var todayAssignedTasks = tasks.Where(t => t.AssignmentDate.Date == reportDate).ToList(); - var reportPending = tasks.Where(t => t.ReportedDate == null).ToList(); - - double totalPlannedTask = todayAssignedTasks.Sum(t => t.PlannedTask); - double totalCompletedTask = todayAssignedTasks.Sum(t => t.CompletedTask); - var jobRoleIds = projectAllocations.Select(pa => pa.JobRoleId).ToList(); - var jobRoles = await _context.JobRoles - .Where(j => j.TenantId == project.TenantId && jobRoleIds.Contains(j.Id)) - .ToListAsync(); - - // Team on site - var teamOnSite = jobRoles - .Select(role => - { - var count = projectAllocations.Count(p => p.JobRoleId == role.Id && checkedInEmployeeIds.Contains(p.EmployeeId)); - return new TeamOnSite { RoleName = role.Name, NumberofEmployees = count }; - }) - .OrderByDescending(t => t.NumberofEmployees) - .ToList(); - - // Task details - var performedTasks = todayAssignedTasks.Select(task => - { - var workItem = workItems.FirstOrDefault(w => w.Id == task.WorkItemId); - var area = areas.FirstOrDefault(a => a.Id == workItem?.WorkAreaId); - var floor = floors.FirstOrDefault(f => f.Id == area?.FloorId); - var building = buildings.FirstOrDefault(b => b.Id == floor?.BuildingId); - - string activityName = workItem?.ActivityMaster?.ActivityName ?? ""; - string location = $"{building?.Name} > {floor?.FloorName}
{floor?.FloorName}-{area?.AreaName}"; - double pending = (workItem?.PlannedWork ?? 0) - (workItem?.CompletedWork ?? 0); - - var taskTeam = taskMembers - .Where(m => m.TaskAllocationId == task.Id) - .Select(m => - { - string name = $"{m.Employee?.FirstName ?? ""} {m.Employee?.LastName ?? ""}"; - var role = jobRoles.FirstOrDefault(r => r.Id == m.Employee?.JobRoleId); - return new TaskTeam { Name = name, RoleName = role?.Name ?? "" }; - }) - .ToList(); - - return new PerformedTask - { - Activity = activityName, - Location = location, - AssignedToday = task.PlannedTask, - CompletedToday = task.CompletedTask, - Pending = pending, - Comment = task.Description, - Team = taskTeam - }; - }).ToList(); - - // Attendance details - var performedAttendance = attendances.Select(att => - { - var alloc = projectAllocations.FirstOrDefault(p => p.EmployeeId == att.EmployeeID); - var role = jobRoles.FirstOrDefault(r => r.Id == alloc?.JobRoleId); - string name = $"{alloc?.Employee?.FirstName ?? ""} {alloc?.Employee?.LastName ?? ""}"; - - return new PerformedAttendance - { - Name = name, - RoleName = role?.Name ?? "", - InTime = att.InTime ?? DateTime.UtcNow, - OutTime = att.OutTime, - Comment = att.Comment - }; - }).ToList(); - - // Fill report - statisticReport.TodaysAttendances = checkedInEmployeeIds.Count; - statisticReport.TotalEmployees = assignedEmployeeIds.Count; - statisticReport.RegularizationPending = regularizationIds.Count; - statisticReport.CheckoutPending = checkoutPendingIds.Count; - statisticReport.TotalPlannedWork = totalPlannedWork; - statisticReport.TotalCompletedWork = totalCompletedWork; - statisticReport.TotalPlannedTask = totalPlannedTask; - statisticReport.TotalCompletedTask = totalCompletedTask; - statisticReport.CompletionStatus = totalPlannedWork > 0 ? totalCompletedWork / totalPlannedWork : 0; - statisticReport.TodaysAssignTasks = todayAssignedTasks.Count; - statisticReport.ReportPending = reportPending.Count; - statisticReport.TeamOnSite = teamOnSite; - statisticReport.PerformedTasks = performedTasks; - statisticReport.PerformedAttendance = performedAttendance; - // Send Email var emailBody = await _emailSender.SendProjectStatisticsEmail(recipientEmails, body, subject, statisticReport); var employee = await _context.Employees.FirstOrDefaultAsync(e => e.Email != null && recipientEmails.Contains(e.Email)) ?? new Employee(); diff --git a/Marco.Pms.Services/Dockerfile b/Marco.Pms.Services/Dockerfile index 77311ee..2aa24ea 100644 --- a/Marco.Pms.Services/Dockerfile +++ b/Marco.Pms.Services/Dockerfile @@ -19,7 +19,7 @@ COPY ["Marco.Pms.Services/Marco.Pms.Services.csproj", "Marco.Pms.Services/"] COPY ["Marco.Pms.DataAccess/Marco.Pms.DataAccess.csproj", "Marco.Pms.DataAccess/"] COPY ["Marco.Pms.Model/Marco.Pms.Model.csproj", "Marco.Pms.Model/"] COPY ["Marco.Pms.Utility/Marco.Pms.Utility.csproj", "Marco.Pms.Utility/"] -COPY ["Marco.Pms.Utility/Marco.Pms.CacheHelper.csproj", "Marco.Pms.CacheHelper/"] +COPY ["Marco.Pms.CacheHelper/Marco.Pms.CacheHelper.csproj", "Marco.Pms.CacheHelper/"] RUN dotnet restore "./Marco.Pms.Services/Marco.Pms.Services.csproj" COPY . . WORKDIR "/src/Marco.Pms.Services" diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 03fd397..216ec6e 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -120,6 +120,36 @@ namespace Marco.Pms.Services.Helpers _logger.LogWarning("Error occured while updating planned work and completed work in building infra form Cache: {Error}", ex.Message); } } + public async Task GetBuildingAndFloorByWorkAreaId(Guid workAreaId) + { + try + { + var response = await _projectCache.GetBuildingAndFloorByWorkAreaIdFromCache(workAreaId); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while fetching workArea Details using its ID form Cache: {Error}", ex.Message); + return null; + } + } + public async Task?> GetWorkItemsByWorkAreaIds(List workAreaIds) + { + try + { + var response = await _projectCache.GetWorkItemsByWorkAreaIdsFromCache(workAreaIds); + if (response.Count > 0) + { + return response; + } + return null; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while fetching workItems list using workArea IDs list form Cache: {Error}", ex.Message); + return null; + } + } // ------------------------------------------------------- WorkItem ------------------------------------------------------- diff --git a/Marco.Pms.Services/Helpers/ReportHelper.cs b/Marco.Pms.Services/Helpers/ReportHelper.cs new file mode 100644 index 0000000..e7632fd --- /dev/null +++ b/Marco.Pms.Services/Helpers/ReportHelper.cs @@ -0,0 +1,274 @@ +using System.Globalization; +using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.Dtos.Attendance; +using Marco.Pms.Model.MongoDBModels; +using Marco.Pms.Model.ViewModels.Report; +using Microsoft.EntityFrameworkCore; + +namespace Marco.Pms.Services.Helpers +{ + public class ReportHelper + { + private readonly ApplicationDbContext _context; + private readonly CacheUpdateHelper _cache; + public ReportHelper(CacheUpdateHelper cache, ApplicationDbContext context) + { + _cache = cache; + _context = context; + } + public async Task GetDailyProjectReport(Guid projectId, Guid tenantId) + { + // await _cache.GetBuildingAndFloorByWorkAreaId(); + DateTime reportDate = DateTime.UtcNow.AddDays(-1).Date; + var project = await _cache.GetProjectDetails(projectId); + if (project == null) + { + var projectSQL = await _context.Projects + .AsNoTracking() + .FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId); + if (projectSQL != null) + { + project = new ProjectMongoDB + { + Id = projectSQL.Id.ToString(), + Name = projectSQL.Name, + ShortName = projectSQL.ShortName, + ProjectAddress = projectSQL.ProjectAddress, + ContactPerson = projectSQL.ContactPerson + }; + await _cache.AddProjectDetails(projectSQL); + } + } + if (project != null) + { + + var statisticReport = new ProjectStatisticReport + { + Date = reportDate, + ProjectName = project.Name ?? "", + TimeStamp = DateTime.Now.ToString("dd-MMM-yyyy HH:mm:ss", CultureInfo.InvariantCulture) + }; + + // Preload relevant data + var projectAllocations = await _context.ProjectAllocations + .Include(p => p.Employee) + .Where(p => p.ProjectId == projectId && p.IsActive) + .ToListAsync(); + + var assignedEmployeeIds = projectAllocations.Select(p => p.EmployeeId).ToHashSet(); + + var attendances = await _context.Attendes + .AsNoTracking() + .Where(a => a.ProjectID == projectId && a.InTime != null && a.InTime.Value.Date == reportDate) + .ToListAsync(); + + var checkedInEmployeeIds = attendances.Select(a => a.EmployeeID).Distinct().ToHashSet(); + var checkoutPendingIds = attendances.Where(a => a.OutTime == null).Select(a => a.EmployeeID).Distinct().ToHashSet(); + var regularizationIds = attendances + .Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE) + .Select(a => a.EmployeeID).Distinct().ToHashSet(); + + // Preload buildings, floors, areas + List? buildings = null; + List? floors = null; + List? areas = null; + List? workItems = null; + + // Fetch Buildings + buildings = project.Buildings + .Select(b => new BuildingMongoDBVM + { + Id = b.Id, + ProjectId = b.ProjectId, + BuildingName = b.BuildingName, + Description = b.Description + }).ToList(); + if (buildings == null) + { + buildings = await _context.Buildings + .Where(b => b.ProjectId == projectId) + .Select(b => new BuildingMongoDBVM + { + Id = b.Id.ToString(), + ProjectId = b.ProjectId.ToString(), + BuildingName = b.Name, + Description = b.Description + }) + .ToListAsync(); + } + + // fetch Floors + floors = project.Buildings + .SelectMany(b => b.Floors.Select(f => new FloorMongoDBVM + { + Id = f.Id.ToString(), + BuildingId = f.BuildingId, + FloorName = f.FloorName + })).ToList(); + if (floors == null) + { + var buildingIds = buildings.Select(b => Guid.Parse(b.Id)).ToList(); + floors = await _context.Floor + .Where(f => buildingIds.Contains(f.BuildingId)) + .Select(f => new FloorMongoDBVM + { + Id = f.Id.ToString(), + BuildingId = f.BuildingId.ToString(), + FloorName = f.FloorName + }) + .ToListAsync(); + } + + // fetch Work Areas + areas = project.Buildings + .SelectMany(b => b.Floors) + .SelectMany(f => f.WorkAreas).ToList(); + if (areas == null) + { + var floorIds = floors.Select(f => Guid.Parse(f.Id)).ToList(); + areas = await _context.WorkAreas + .Where(a => floorIds.Contains(a.FloorId)) + .Select(wa => new WorkAreaMongoDB + { + Id = wa.Id.ToString(), + FloorId = wa.FloorId.ToString(), + AreaName = wa.AreaName, + }) + .ToListAsync(); + } + + var areaIds = areas.Select(a => Guid.Parse(a.Id)).ToList(); + + // fetch Work Items + workItems = await _cache.GetWorkItemsByWorkAreaIds(areaIds); + if (workItems == null) + { + workItems = await _context.WorkItems + .Include(w => w.ActivityMaster) + .Where(w => areaIds.Contains(w.WorkAreaId)) + .Select(wi => new WorkItemMongoDB + { + Id = wi.Id.ToString(), + WorkAreaId = wi.WorkAreaId.ToString(), + PlannedWork = wi.PlannedWork, + CompletedWork = wi.CompletedWork, + Description = wi.Description, + TaskDate = wi.TaskDate, + ActivityMaster = new ActivityMasterMongoDB + { + ActivityName = wi.ActivityMaster != null ? wi.ActivityMaster.ActivityName : null, + UnitOfMeasurement = wi.ActivityMaster != null ? wi.ActivityMaster.UnitOfMeasurement : null + } + }) + .ToListAsync(); + } + + var itemIds = workItems.Select(i => Guid.Parse(i.Id)).ToList(); + + var tasks = await _context.TaskAllocations + .Where(t => itemIds.Contains(t.WorkItemId)) + .ToListAsync(); + + var taskIds = tasks.Select(t => t.Id).ToList(); + + var taskMembers = await _context.TaskMembers + .Include(m => m.Employee) + .Where(m => taskIds.Contains(m.TaskAllocationId)) + .ToListAsync(); + + // Aggregate data + double totalPlannedWork = workItems.Sum(w => w.PlannedWork); + double totalCompletedWork = workItems.Sum(w => w.CompletedWork); + + var todayAssignedTasks = tasks.Where(t => t.AssignmentDate.Date == reportDate).ToList(); + var reportPending = tasks.Where(t => t.ReportedDate == null).ToList(); + + double totalPlannedTask = todayAssignedTasks.Sum(t => t.PlannedTask); + double totalCompletedTask = todayAssignedTasks.Sum(t => t.CompletedTask); + var jobRoleIds = projectAllocations.Select(pa => pa.JobRoleId).ToList(); + var jobRoles = await _context.JobRoles + .Where(j => j.TenantId == tenantId && jobRoleIds.Contains(j.Id)) + .ToListAsync(); + + // Team on site + var teamOnSite = jobRoles + .Select(role => + { + var count = projectAllocations.Count(p => p.JobRoleId == role.Id && checkedInEmployeeIds.Contains(p.EmployeeId)); + return new TeamOnSite { RoleName = role.Name, NumberofEmployees = count }; + }) + .OrderByDescending(t => t.NumberofEmployees) + .ToList(); + + // Task details + var performedTasks = todayAssignedTasks.Select(task => + { + var workItem = workItems.FirstOrDefault(w => w.Id == task.WorkItemId.ToString()); + var area = areas.FirstOrDefault(a => a.Id == workItem?.WorkAreaId); + var floor = floors.FirstOrDefault(f => f.Id == area?.FloorId); + var building = buildings.FirstOrDefault(b => b.Id == floor?.BuildingId); + + string activityName = workItem?.ActivityMaster?.ActivityName ?? ""; + string location = $"{building?.BuildingName} > {floor?.FloorName}
{floor?.FloorName}-{area?.AreaName}"; + double pending = (workItem?.PlannedWork ?? 0) - (workItem?.CompletedWork ?? 0); + + var taskTeam = taskMembers + .Where(m => m.TaskAllocationId == task.Id) + .Select(m => + { + string name = $"{m.Employee?.FirstName ?? ""} {m.Employee?.LastName ?? ""}"; + var role = jobRoles.FirstOrDefault(r => r.Id == m.Employee?.JobRoleId); + return new TaskTeam { Name = name, RoleName = role?.Name ?? "" }; + }) + .ToList(); + + return new PerformedTask + { + Activity = activityName, + Location = location, + AssignedToday = task.PlannedTask, + CompletedToday = task.CompletedTask, + Pending = pending, + Comment = task.Description, + Team = taskTeam + }; + }).ToList(); + + // Attendance details + var performedAttendance = attendances.Select(att => + { + var alloc = projectAllocations.FirstOrDefault(p => p.EmployeeId == att.EmployeeID); + var role = jobRoles.FirstOrDefault(r => r.Id == alloc?.JobRoleId); + string name = $"{alloc?.Employee?.FirstName ?? ""} {alloc?.Employee?.LastName ?? ""}"; + + return new PerformedAttendance + { + Name = name, + RoleName = role?.Name ?? "", + InTime = att.InTime ?? DateTime.UtcNow, + OutTime = att.OutTime, + Comment = att.Comment + }; + }).ToList(); + + // Fill report + statisticReport.TodaysAttendances = checkedInEmployeeIds.Count; + statisticReport.TotalEmployees = assignedEmployeeIds.Count; + statisticReport.RegularizationPending = regularizationIds.Count; + statisticReport.CheckoutPending = checkoutPendingIds.Count; + statisticReport.TotalPlannedWork = totalPlannedWork; + statisticReport.TotalCompletedWork = totalCompletedWork; + statisticReport.TotalPlannedTask = totalPlannedTask; + statisticReport.TotalCompletedTask = totalCompletedTask; + statisticReport.CompletionStatus = totalPlannedWork > 0 ? totalCompletedWork / totalPlannedWork : 0; + statisticReport.TodaysAssignTasks = todayAssignedTasks.Count; + statisticReport.ReportPending = reportPending.Count; + statisticReport.TeamOnSite = teamOnSite; + statisticReport.PerformedTasks = performedTasks; + statisticReport.PerformedAttendance = performedAttendance; + return statisticReport; + } + return null; + } + } +} diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index 1d9b4b3..30831c6 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -137,6 +137,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); From 17c56be712eb644b1e0c1b3a12318bf340603dca Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 9 Jul 2025 15:11:08 +0530 Subject: [PATCH 044/124] Added new parameter in log "Origin" --- Marco.Pms.Services/Middleware/LoggingMiddleware.cs | 4 +++- Marco.Pms.Services/Service/RefreshTokenService.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Marco.Pms.Services/Middleware/LoggingMiddleware.cs b/Marco.Pms.Services/Middleware/LoggingMiddleware.cs index dd10d7d..c57f05c 100644 --- a/Marco.Pms.Services/Middleware/LoggingMiddleware.cs +++ b/Marco.Pms.Services/Middleware/LoggingMiddleware.cs @@ -24,7 +24,7 @@ namespace MarcoBMS.Services.Middleware var response = context.Response; var request = context.Request; var tenantId = context.User.FindFirst("TenantId")?.Value; - + string origin = request.Headers["Origin"].FirstOrDefault() ?? ""; using (LogContext.PushProperty("TenantId", tenantId)) using (LogContext.PushProperty("TraceId", context.TraceIdentifier)) @@ -33,6 +33,8 @@ namespace MarcoBMS.Services.Middleware using (LogContext.PushProperty("Timestamp", DateTime.UtcNow)) using (LogContext.PushProperty("IpAddress", context.Connection.RemoteIpAddress?.ToString())) using (LogContext.PushProperty("RequestPath", request.Path)) + using (LogContext.PushProperty("Origin", origin)) + try diff --git a/Marco.Pms.Services/Service/RefreshTokenService.cs b/Marco.Pms.Services/Service/RefreshTokenService.cs index 018de68..231e27c 100644 --- a/Marco.Pms.Services/Service/RefreshTokenService.cs +++ b/Marco.Pms.Services/Service/RefreshTokenService.cs @@ -218,7 +218,7 @@ namespace MarcoBMS.Services.Service catch (Exception ex) { // Token is invalid - Console.WriteLine($"Token validation failed: {ex.Message}"); + _logger.LogError($"Token validation failed: {ex.Message}"); return null; } } From c2a9a42af55982de4b7012fa8f9ce9c230abf198 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 10 Jul 2025 09:52:18 +0530 Subject: [PATCH 045/124] Added old project Details API --- .../ViewModels/Projects/OldProjectVM.cs | 10 ++ .../Controllers/ProjectController.cs | 134 ++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 Marco.Pms.Model/ViewModels/Projects/OldProjectVM.cs diff --git a/Marco.Pms.Model/ViewModels/Projects/OldProjectVM.cs b/Marco.Pms.Model/ViewModels/Projects/OldProjectVM.cs new file mode 100644 index 0000000..cb38dfc --- /dev/null +++ b/Marco.Pms.Model/ViewModels/Projects/OldProjectVM.cs @@ -0,0 +1,10 @@ +using Marco.Pms.Model.Dtos.Project; + +namespace Marco.Pms.Model.ViewModels.Projects +{ + public class OldProjectVM : ProjectDto + { + public List? Buildings { get; set; } + + } +} diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index e12d2ad..fbc9bf6 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -1,6 +1,8 @@ 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; @@ -288,6 +290,138 @@ namespace MarcoBMS.Services.Controllers }; } + [HttpGet("details-old/{id}")] + public async Task DetailsOld([FromRoute] Guid id) + { + // ProjectDetailsVM vm = new ProjectDetailsVM(); + + if (!ModelState.IsValid) + { + var errors = ModelState.Values + .SelectMany(v => v.Errors) + .Select(e => e.ErrorMessage) + .ToList(); + return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); + + } + + var project = await _context.Projects.Where(c => c.TenantId == _userHelper.GetTenantId() && c.Id == id).Include(c => c.ProjectStatus).SingleOrDefaultAsync(); // includeProperties: "ProjectStatus,Tenant"); //_context.Stock.FindAsync(id); + + if (project == null) + { + return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); + + } + else + { + //var project = projects.Where(c => c.Id == id).SingleOrDefault(); + ProjectDetailsVM vm = await GetProjectViewModel(id, project); + + OldProjectVM projectVM = new OldProjectVM(); + if (vm.project != null) + { + projectVM.Id = vm.project.Id; + projectVM.Name = vm.project.Name; + projectVM.ShortName = vm.project.ShortName; + projectVM.ProjectAddress = vm.project.ProjectAddress; + projectVM.ContactPerson = vm.project.ContactPerson; + projectVM.StartDate = vm.project.StartDate; + projectVM.EndDate = vm.project.EndDate; + projectVM.ProjectStatusId = vm.project.ProjectStatusId; + } + projectVM.Buildings = new List(); + if (vm.buildings != null) + { + foreach (Building build in vm.buildings) + { + BuildingVM buildVM = new BuildingVM() { Id = build.Id, Description = build.Description, Name = build.Name }; + buildVM.Floors = new List(); + if (vm.floors != null) + { + foreach (Floor floorDto in vm.floors.Where(c => c.BuildingId == build.Id).ToList()) + { + FloorsVM floorVM = new FloorsVM() { FloorName = floorDto.FloorName, Id = floorDto.Id }; + floorVM.WorkAreas = new List(); + + if (vm.workAreas != null) + { + foreach (WorkArea workAreaDto in vm.workAreas.Where(c => c.FloorId == floorVM.Id).ToList()) + { + WorkAreaVM workAreaVM = new WorkAreaVM() { Id = workAreaDto.Id, AreaName = workAreaDto.AreaName, WorkItems = new List() }; + + if (vm.workItems != null) + { + foreach (WorkItem workItemDto in vm.workItems.Where(c => c.WorkAreaId == workAreaDto.Id).ToList()) + { + WorkItemVM workItemVM = new WorkItemVM() { WorkItemId = workItemDto.Id, WorkItem = workItemDto }; + + workItemVM.WorkItem.WorkArea = new WorkArea(); + + if (workItemVM.WorkItem.ActivityMaster != null) + { + workItemVM.WorkItem.ActivityMaster.Tenant = new Tenant(); + } + workItemVM.WorkItem.Tenant = new Tenant(); + + double todaysAssigned = 0; + if (vm.Tasks != null) + { + var tasks = vm.Tasks.Where(t => t.WorkItemId == workItemDto.Id).ToList(); + foreach (TaskAllocation task in tasks) + { + todaysAssigned += task.PlannedTask; + } + } + workItemVM.TodaysAssigned = todaysAssigned; + + workAreaVM.WorkItems.Add(workItemVM); + } + } + + floorVM.WorkAreas.Add(workAreaVM); + } + } + + buildVM.Floors.Add(floorVM); + } + } + projectVM.Buildings.Add(buildVM); + } + } + return Ok(ApiResponse.SuccessResponse(projectVM, "Success.", 200)); + } + + + } + + private async Task GetProjectViewModel(Guid? id, Project project) + { + ProjectDetailsVM vm = new ProjectDetailsVM(); + + // List buildings = _unitOfWork.Building.GetAll(c => c.ProjectId == id).ToList(); + List buildings = await _context.Buildings.Where(c => c.ProjectId == id).ToListAsync(); + List idList = buildings.Select(o => o.Id).ToList(); + // List floors = _unitOfWork.Floor.GetAll(c => idList.Contains(c.Id)).ToList(); + List floors = await _context.Floor.Where(c => idList.Contains(c.BuildingId)).ToListAsync(); + idList = floors.Select(o => o.Id).ToList(); + //List workAreas = _unitOfWork.WorkArea.GetAll(c => idList.Contains(c.Id), includeProperties: "WorkItems,WorkItems.ActivityMaster").ToList(); + + List workAreas = await _context.WorkAreas.Where(c => idList.Contains(c.FloorId)).ToListAsync(); + + idList = workAreas.Select(o => o.Id).ToList(); + List workItems = await _context.WorkItems.Include(c => c.WorkCategoryMaster).Where(c => idList.Contains(c.WorkAreaId)).Include(c => c.ActivityMaster).ToListAsync(); + // List workItems = _unitOfWork.WorkItem.GetAll(c => idList.Contains(c.WorkAreaId), includeProperties: "ActivityMaster").ToList(); + idList = workItems.Select(t => t.Id).ToList(); + List tasks = await _context.TaskAllocations.Where(t => idList.Contains(t.WorkItemId) && t.AssignmentDate.Date == DateTime.UtcNow.Date).ToListAsync(); + vm.project = project; + vm.buildings = buildings; + vm.floors = floors; + vm.workAreas = workAreas; + vm.workItems = workItems; + vm.Tasks = tasks; + return vm; + } + private Guid GetTenantId() { return _userHelper.GetTenantId(); From 40ce4ced42bc67a7e79829077bbb31449f10937f Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 10 Jul 2025 14:59:28 +0530 Subject: [PATCH 046/124] Added the workcategory in WorkItem --- Marco.Pms.Services/Controllers/ProjectController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index fde715f..09858d5 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -894,7 +894,7 @@ namespace MarcoBMS.Services.Controllers }, WorkCategoryMaster = new WorkCategoryMasterMongoDB { - Id = wi.ActivityId.ToString(), + Id = wi.WorkCategoryId.ToString() ?? "", Name = wi.WorkCategoryMaster != null ? wi.WorkCategoryMaster.Name : "", Description = wi.WorkCategoryMaster != null ? wi.WorkCategoryMaster.Description : "" }, From bb76c45195aee9d63c6412224055f0e1709a34c9 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 10 Jul 2025 15:57:08 +0530 Subject: [PATCH 047/124] Removing the project stored in cache for employee who have the project manage permission --- Marco.Pms.CacheHelper/EmployeeCache.cs | 14 +++++ .../Controllers/ProjectController.cs | 59 ++++++++++++++----- .../Helpers/CacheUpdateHelper.cs | 11 ++++ 3 files changed, 68 insertions(+), 16 deletions(-) diff --git a/Marco.Pms.CacheHelper/EmployeeCache.cs b/Marco.Pms.CacheHelper/EmployeeCache.cs index 5c86e6f..c2a1f7b 100644 --- a/Marco.Pms.CacheHelper/EmployeeCache.cs +++ b/Marco.Pms.CacheHelper/EmployeeCache.cs @@ -137,6 +137,20 @@ namespace Marco.Pms.CacheHelper return true; } + public async Task ClearAllProjectIdsByPermissionIdFromCache(Guid permissionId) + { + var filter = Builders.Filter.AnyEq(e => e.PermissionIds, permissionId.ToString()); + + var update = Builders.Update + .Set(e => e.ProjectIds, new List()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + return true; + } public async Task RemoveRoleIdFromCache(Guid employeeId, Guid roleId) { var filter = Builders.Filter diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 09858d5..07ddbfd 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -36,6 +36,7 @@ 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; @@ -44,7 +45,7 @@ namespace MarcoBMS.Services.Controllers public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, ProjectsHelper projectHelper, - IHubContext signalR, PermissionServices permission, CacheUpdateHelper cache) + IHubContext signalR, PermissionServices permission, CacheUpdateHelper cache, IServiceScopeFactory serviceScopeFactory) { _context = context; _userHelper = userHelper; @@ -59,6 +60,7 @@ namespace MarcoBMS.Services.Controllers ViewInfra = Guid.Parse("8d7cc6e3-9147-41f7-aaa7-fa507e450bd4"); ManageInfra = Guid.Parse("f2aee20a-b754-4537-8166-f9507b44585b"); tenantId = _userHelper.GetTenantId(); + _serviceScopeFactory = serviceScopeFactory; } [HttpGet("list/basic")] @@ -436,31 +438,56 @@ namespace MarcoBMS.Services.Controllers [HttpPost] public async Task Create([FromBody] CreateProjectDto projectDto) { - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + // 1. Validate input first (early exit) if (!ModelState.IsValid) { - var errors = ModelState.Values - .SelectMany(v => v.Errors) - .Select(e => e.ErrorMessage) - .ToList(); + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); - } - Guid TenantId = GetTenantId(); - var project = projectDto.ToProjectFromCreateProjectDto(TenantId); + // 2. Prepare data without I/O + Guid tenantId = _userHelper.GetTenantId(); // Assuming this is fast and from claims + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var loggedInUserId = loggedInEmployee.Id; + var project = projectDto.ToProjectFromCreateProjectDto(tenantId); - _context.Projects.Add(project); + // 3. Store it to database + try + { + _context.Projects.Add(project); + await _context.SaveChangesAsync(); + } + catch (Exception ex) + { + // Log the detailed exception + _logger.LogError("Failed to create project in database. Rolling back transaction. : {Error}", ex.Message); + // Return a server error as the primary operation failed + return StatusCode(500, ApiResponse.ErrorResponse("An error occurred while saving the project.", ex.Message, 500)); + } - await _context.SaveChangesAsync(); + // 4. Perform non-critical side-effects (caching, notifications) concurrently + try + { + // These operations do not depend on each other, so they can run in parallel. + Task cacheAddDetailsTask = _cache.AddProjectDetails(project); + Task cacheClearListTask = _cache.ClearAllProjectIdsByPermissionId(ManageProject); - await _cache.AddProjectDetails(project); + 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) + Task notificationTask = _signalR.Clients.Group(tenantId.ToString()).SendAsync("NotificationEventHandler", notification); - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Create_Project", Response = project.ToProjectDto() }; + // Await all side-effect tasks to complete in parallel + await Task.WhenAll(cacheAddDetailsTask, cacheClearListTask, notificationTask); + } + catch (Exception ex) + { + // The project was created successfully, but a side-effect failed. + // Log this as a warning, as the primary operation succeeded. Don't return an error to the user. + _logger.LogWarning("Project {ProjectId} was created, but a post-creation side-effect (caching/notification) failed. : {Error}", project.Id, ex.Message); + } - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); - - return Ok(ApiResponse.SuccessResponse(project.ToProjectDto(), "Success.", 200)); + // 5. Return a success response to the user as soon as the critical data is saved. + return Ok(ApiResponse.SuccessResponse(project.ToProjectDto(), "Project created successfully.", 200)); } [HttpPut] diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 216ec6e..ae6264e 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -298,6 +298,17 @@ namespace Marco.Pms.Services.Helpers _logger.LogWarning("Error occured while deleting projectIds from Cache for Application Role {RoleId}: {Error}", roleId, ex.Message); } } + public async Task ClearAllProjectIdsByPermissionId(Guid permissionId) + { + try + { + await _employeeCache.ClearAllProjectIdsByPermissionIdFromCache(permissionId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting projectIds from Cache for Permission {PermissionId}: {Error}", permissionId, ex.Message); + } + } public async Task ClearAllPermissionIdsByEmployeeID(Guid employeeId) { try From de3fa6b929617c9f49e22272e98476c985db9a08 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 1 Jul 2025 12:39:07 +0530 Subject: [PATCH 048/124] project details API is split into three APIs. --- .../ViewModels/Projects/ProjectVM.cs | 13 +- .../Controllers/ProjectController.cs | 281 +++++++++++------- 2 files changed, 179 insertions(+), 115 deletions(-) diff --git a/Marco.Pms.Model/ViewModels/Projects/ProjectVM.cs b/Marco.Pms.Model/ViewModels/Projects/ProjectVM.cs index cd349bb..240b35f 100644 --- a/Marco.Pms.Model/ViewModels/Projects/ProjectVM.cs +++ b/Marco.Pms.Model/ViewModels/Projects/ProjectVM.cs @@ -1,10 +1,17 @@ -using Marco.Pms.Model.Dtos.Project; +using Marco.Pms.Model.Master; namespace Marco.Pms.Model.ViewModels.Projects { - public class ProjectVM : ProjectDto + public class ProjectVM { - public List? Buildings { get; set; } + public Guid Id { get; set; } + public string? Name { get; set; } + public string? ShortName { get; set; } + public string? ProjectAddress { get; set; } + public string? ContactPerson { get; set; } + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + public StatusMaster? ProjectStatus { get; set; } } } diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 6b83a6c..6490c54 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -1,14 +1,13 @@ 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.Projects; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Model.ViewModels.Projects; using Marco.Pms.Services.Hubs; +using Marco.Pms.Services.Service; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; @@ -29,9 +28,16 @@ namespace MarcoBMS.Services.Controllers private readonly RolesHelper _rolesHelper; private readonly ProjectsHelper _projectsHelper; private readonly IHubContext _signalR; + private readonly PermissionServices _permission; + private readonly Guid ViewProjects; + private readonly Guid ManageProject; + private readonly Guid ViewInfra; + private readonly Guid ManageInfra; + private readonly Guid tenantId; - public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, ProjectsHelper projectHelper, IHubContext signalR) + public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, ProjectsHelper projectHelper, + IHubContext signalR, PermissionServices permission) { _context = context; _userHelper = userHelper; @@ -39,6 +45,12 @@ namespace MarcoBMS.Services.Controllers _rolesHelper = rolesHelper; _projectsHelper = projectHelper; _signalR = signalR; + _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"); + tenantId = _userHelper.GetTenantId(); } @@ -177,133 +189,68 @@ namespace MarcoBMS.Services.Controllers [HttpGet("details/{id}")] public async Task Details([FromRoute] Guid id) { - // ProjectDetailsVM vm = new ProjectDetailsVM(); - + // Step 1: Validate model state if (!ModelState.IsValid) { var errors = ModelState.Values .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); + _logger.LogWarning("Invalid model state in Details endpoint. Errors: {@Errors}", errors); + return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } - var project = await _context.Projects.Where(c => c.TenantId == _userHelper.GetTenantId() && c.Id == id).Include(c => c.ProjectStatus).SingleOrDefaultAsync(); // includeProperties: "ProjectStatus,Tenant"); //_context.Stock.FindAsync(id); + // Step 2: Get logged-in employee + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + _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); + if (!hasViewProjectPermission) + { + _logger.LogWarning("ViewProjects permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); + return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have permission to view projects", 403)); + } + + // Step 4: Check permission for this specific project + var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, id.ToString()); + if (!hasProjectPermission) + { + _logger.LogWarning("Project-specific access denied. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployee.Id, id); + return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have access to this project", 403)); + } + + // Step 5: Fetch project with status + var project = await _context.Projects + .Include(c => c.ProjectStatus) + .FirstOrDefaultAsync(c => c.TenantId == tenantId && c.Id == id); if (project == null) { + _logger.LogWarning("Project not found. ProjectId: {ProjectId}", id); return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); - - } - else - { - //var project = projects.Where(c => c.Id == id).SingleOrDefault(); - ProjectDetailsVM vm = await GetProjectViewModel(id, project); - - ProjectVM projectVM = new ProjectVM(); - if (vm.project != null) - { - projectVM.Id = vm.project.Id; - projectVM.Name = vm.project.Name; - projectVM.ShortName = vm.project.ShortName; - projectVM.ProjectAddress = vm.project.ProjectAddress; - projectVM.ContactPerson = vm.project.ContactPerson; - projectVM.StartDate = vm.project.StartDate; - projectVM.EndDate = vm.project.EndDate; - projectVM.ProjectStatusId = vm.project.ProjectStatusId; - } - projectVM.Buildings = new List(); - if (vm.buildings != null) - { - foreach (Building build in vm.buildings) - { - BuildingVM buildVM = new BuildingVM() { Id = build.Id, Description = build.Description, Name = build.Name }; - buildVM.Floors = new List(); - if (vm.floors != null) - { - foreach (Floor floorDto in vm.floors.Where(c => c.BuildingId == build.Id).ToList()) - { - FloorsVM floorVM = new FloorsVM() { FloorName = floorDto.FloorName, Id = floorDto.Id }; - floorVM.WorkAreas = new List(); - - if (vm.workAreas != null) - { - foreach (WorkArea workAreaDto in vm.workAreas.Where(c => c.FloorId == floorVM.Id).ToList()) - { - WorkAreaVM workAreaVM = new WorkAreaVM() { Id = workAreaDto.Id, AreaName = workAreaDto.AreaName, WorkItems = new List() }; - - if (vm.workItems != null) - { - foreach (WorkItem workItemDto in vm.workItems.Where(c => c.WorkAreaId == workAreaDto.Id).ToList()) - { - WorkItemVM workItemVM = new WorkItemVM() { WorkItemId = workItemDto.Id, WorkItem = workItemDto }; - - workItemVM.WorkItem.WorkArea = new WorkArea(); - - if (workItemVM.WorkItem.ActivityMaster != null) - { - workItemVM.WorkItem.ActivityMaster.Tenant = new Tenant(); - } - workItemVM.WorkItem.Tenant = new Tenant(); - - double todaysAssigned = 0; - if (vm.Tasks != null) - { - var tasks = vm.Tasks.Where(t => t.WorkItemId == workItemDto.Id).ToList(); - foreach (TaskAllocation task in tasks) - { - todaysAssigned += task.PlannedTask; - } - } - workItemVM.TodaysAssigned = todaysAssigned; - - workAreaVM.WorkItems.Add(workItemVM); - } - } - - floorVM.WorkAreas.Add(workAreaVM); - } - } - - buildVM.Floors.Add(floorVM); - } - } - projectVM.Buildings.Add(buildVM); - } - } - return Ok(ApiResponse.SuccessResponse(projectVM, "Success.", 200)); } - + // Step 6: Map and return result + var projectVM = GetProjectViewModel(project); + _logger.LogInfo("Project details fetched successfully. ProjectId: {ProjectId}", id); + return Ok(ApiResponse.SuccessResponse(projectVM, "Project details fetched successfully", 200)); } - private async Task GetProjectViewModel(Guid? id, Project project) + private ProjectVM GetProjectViewModel(Project project) { - ProjectDetailsVM vm = new ProjectDetailsVM(); - - // List buildings = _unitOfWork.Building.GetAll(c => c.ProjectId == id).ToList(); - List buildings = await _context.Buildings.Where(c => c.ProjectId == id).ToListAsync(); - List idList = buildings.Select(o => o.Id).ToList(); - // List floors = _unitOfWork.Floor.GetAll(c => idList.Contains(c.Id)).ToList(); - List floors = await _context.Floor.Where(c => idList.Contains(c.BuildingId)).ToListAsync(); - idList = floors.Select(o => o.Id).ToList(); - //List workAreas = _unitOfWork.WorkArea.GetAll(c => idList.Contains(c.Id), includeProperties: "WorkItems,WorkItems.ActivityMaster").ToList(); - - List workAreas = await _context.WorkAreas.Where(c => idList.Contains(c.FloorId)).ToListAsync(); - - idList = workAreas.Select(o => o.Id).ToList(); - List workItems = await _context.WorkItems.Include(c => c.WorkCategoryMaster).Where(c => idList.Contains(c.WorkAreaId)).Include(c => c.ActivityMaster).ToListAsync(); - // List workItems = _unitOfWork.WorkItem.GetAll(c => idList.Contains(c.WorkAreaId), includeProperties: "ActivityMaster").ToList(); - idList = workItems.Select(t => t.Id).ToList(); - List tasks = await _context.TaskAllocations.Where(t => idList.Contains(t.WorkItemId) && t.AssignmentDate.Date == DateTime.UtcNow.Date).ToListAsync(); - vm.project = project; - vm.buildings = buildings; - vm.floors = floors; - vm.workAreas = workAreas; - vm.workItems = workItems; - vm.Tasks = tasks; - return vm; + 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, + }; } private Guid GetTenantId() @@ -594,6 +541,116 @@ namespace MarcoBMS.Services.Controllers } + + [HttpGet("infra-details/{projectId}")] + public async Task GetInfraDetails(Guid projectId) + { + _logger.LogInfo("GetInfraDetails called for ProjectId: {ProjectId}", projectId); + + // Step 1: Get logged-in employee + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // Step 2: Check project-specific permission + var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.ToString()); + if (!hasProjectPermission) + { + _logger.LogWarning("Project access denied for EmployeeId: {EmployeeId} on ProjectId: {ProjectId}", loggedInEmployee.Id, projectId); + return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have access to this project", 403)); + } + + // Step 3: Check 'ViewInfra' permission + var hasViewInfraPermission = await _permission.HasPermission(ViewInfra, loggedInEmployee.Id); + if (!hasViewInfraPermission) + { + _logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); + return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have access to view infra", 403)); + } + + // Step 4: Fetch buildings for the project + var buildings = await _context.Buildings + .Where(b => b.ProjectId == projectId) + .ToListAsync(); + + var buildingIds = buildings.Select(b => b.Id).ToList(); + + // Step 5: Fetch floors associated with the buildings + var floors = await _context.Floor + .Where(f => buildingIds.Contains(f.BuildingId)) + .ToListAsync(); + + var floorIds = floors.Select(f => f.Id).ToList(); + + // Step 6: Fetch work areas associated with the floors + var workAreas = await _context.WorkAreas + .Where(wa => floorIds.Contains(wa.FloorId)) + .ToListAsync(); + + // Step 7: Build the infra hierarchy (Building > Floors > Work Areas) + var infraVM = buildings.Select(b => + { + var selectedFloors = floors + .Where(f => f.BuildingId == b.Id) + .Select(f => new + { + Id = f.Id, + FloorName = f.FloorName, + WorkAreas = workAreas + .Where(wa => wa.FloorId == f.Id) + .Select(wa => new { wa.Id, wa.AreaName }) + .ToList() + }).ToList(); + + return new + { + Id = b.Id, + BuildingName = b.Name, + Floors = selectedFloors + }; + }).ToList(); + + _logger.LogInfo("Infra details fetched successfully for ProjectId: {ProjectId}, EmployeeId: {EmployeeId}, Buildings: {Count}", + projectId, loggedInEmployee.Id, infraVM.Count); + + return Ok(ApiResponse.SuccessResponse(infraVM, "Infra details fetched successfully", 200)); + } + + [HttpGet("tasks/{workAreaId}")] + public async Task GetWorkItems(Guid workAreaId) + { + _logger.LogInfo("GetWorkItems called for WorkAreaId: {WorkAreaId}", workAreaId); + + // Step 1: Get the currently logged-in employee + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // Step 2: Check if the employee has ViewInfra permission + var hasViewInfraPermission = await _permission.HasPermission(ViewInfra, loggedInEmployee.Id); + if (!hasViewInfraPermission) + { + _logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); + return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have permission to view infrastructure", 403)); + } + + // Step 3: Check if the specified Work Area exists + var isWorkAreaExist = await _context.WorkAreas.AnyAsync(wa => wa.Id == workAreaId); + if (!isWorkAreaExist) + { + _logger.LogWarning("Work Area not found for WorkAreaId: {WorkAreaId}", workAreaId); + return NotFound(ApiResponse.ErrorResponse("Work Area not found", "Work Area not found in database", 404)); + } + + // Step 4: Fetch WorkItems with related Activity and Work Category data + var workItems = await _context.WorkItems + .Include(wi => wi.ActivityMaster) + .Include(wi => wi.WorkCategoryMaster) + .Where(wi => wi.WorkAreaId == workAreaId) + .ToListAsync(); + + _logger.LogInfo("{Count} work items fetched successfully for WorkAreaId: {WorkAreaId}", workItems.Count, workAreaId); + + // Step 5: Return result + return Ok(ApiResponse.SuccessResponse(workItems, $"{workItems.Count} records of tasks fetched successfully", 200)); + } + [HttpPost("task")] public async Task CreateProjectTask(List workItemDtos) { From 3e8ef856d4dd9e91b78ec5858939f2f5e0f34472 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Fri, 4 Jul 2025 17:49:25 +0530 Subject: [PATCH 049/124] Saving project details with infrastructure, employee permissions and assigned project for that employee in mongodb --- Marco.Pms.CacheHelper/EmployeeCache.cs | 158 +++++++ .../Marco.Pms.CacheHelper.csproj | 18 + Marco.Pms.CacheHelper/ProjectCache.cs | 434 ++++++++++++++++++ Marco.Pms.Model/Marco.Pms.Model.csproj | 1 + .../MongoDBModels/ActivityMasterMongoDB.cs | 9 + .../MongoDBModels/BuildingMongoDB.cs | 18 + .../EmployeePermissionMongoDB.cs | 13 + Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs | 17 + .../MongoDBModels/ProjectMongoDB.cs | 18 + .../MongoDBModels/StatusMasterMongoDB.cs | 8 + .../MongoDBModels/WorkAreaMongoDB.cs | 15 + .../WorkCategoryMasterMongoDB.cs | 9 + .../MongoDBModels/WorkItemMongoDB.cs | 15 + .../Controllers/ProjectController.cs | 221 +++++++-- .../Controllers/RolesController.cs | 12 +- Marco.Pms.Services/Dockerfile | 1 + .../Helpers/CacheUpdateHelper.cs | 98 ++++ Marco.Pms.Services/Helpers/ProjectsHelper.cs | 65 ++- Marco.Pms.Services/Helpers/RolesHelper.cs | 7 +- Marco.Pms.Services/Marco.Pms.Services.csproj | 1 + Marco.Pms.Services/Program.cs | 6 +- .../Service/PermissionServices.cs | 18 +- .../appsettings.Development.json | 4 +- .../appsettings.Production.json | 5 +- marco.pms.api.sln | 6 + 25 files changed, 1090 insertions(+), 87 deletions(-) create mode 100644 Marco.Pms.CacheHelper/EmployeeCache.cs create mode 100644 Marco.Pms.CacheHelper/Marco.Pms.CacheHelper.csproj create mode 100644 Marco.Pms.CacheHelper/ProjectCache.cs create mode 100644 Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs create mode 100644 Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs create mode 100644 Marco.Pms.Services/Helpers/CacheUpdateHelper.cs diff --git a/Marco.Pms.CacheHelper/EmployeeCache.cs b/Marco.Pms.CacheHelper/EmployeeCache.cs new file mode 100644 index 0000000..7d75407 --- /dev/null +++ b/Marco.Pms.CacheHelper/EmployeeCache.cs @@ -0,0 +1,158 @@ +using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.MongoDBModels; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using MongoDB.Driver; + +namespace Marco.Pms.CacheHelper +{ + public class EmployeeCache + { + private readonly ApplicationDbContext _context; + //private readonly IMongoDatabase _mongoDB; + private readonly IMongoCollection _collection; + public EmployeeCache(ApplicationDbContext context, IConfiguration configuration) + { + var connectionString = configuration["MongoDB:ConnectionString"]; + _context = context; + var mongoUrl = new MongoUrl(connectionString); + var client = new MongoClient(mongoUrl); // Your MongoDB connection string + var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name + _collection = mongoDB.GetCollection("EmployeeProfile"); + } + public async Task AddApplicationRoleToCache(Guid employeeId, List roleIds) + { + var newRoleIds = roleIds.Select(r => r.ToString()).ToList(); + var newPermissionIds = await _context.RolePermissionMappings + .Where(rp => roleIds.Contains(rp.ApplicationRoleId)) + .Select(p => p.FeaturePermissionId.ToString()) + .Distinct() + .ToListAsync(); + + var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + + var update = Builders.Update + .AddToSetEach(e => e.ApplicationRoleIds, newRoleIds) + .AddToSetEach(e => e.PermissionIds, newPermissionIds); + + var result = await _collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); + if (result.MatchedCount == 0) + { + return false; + } + return true; + } + public async Task AddProjectsToCache(Guid employeeId, List projectIds) + { + var newprojectIds = projectIds.Select(p => p.ToString()).ToList(); + + var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + + var update = Builders.Update + .AddToSetEach(e => e.ProjectIds, newprojectIds); + + var result = await _collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); + if (result.MatchedCount == 0) + { + return false; + } + return true; + } + public async Task> GetProjectsFromCache(Guid employeeId) + { + var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + + + var result = await _collection + .Find(filter) + .FirstOrDefaultAsync(); + + var projectIds = new List(); + if (result != null) + { + projectIds = result.ProjectIds.Select(Guid.Parse).ToList(); + } + + return projectIds; + } + public async Task> GetPermissionsFromCache(Guid employeeId) + { + var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + + + var result = await _collection + .Find(filter) + .FirstOrDefaultAsync(); + + var permissionIds = new List(); + if (result != null) + { + permissionIds = result.PermissionIds.Select(Guid.Parse).ToList(); + } + + return permissionIds; + } + public async Task ClearAllProjectIdsFromCache(Guid employeeId) + { + var filter = Builders.Filter + .Eq(e => e.EmployeeId, employeeId.ToString()); + + var update = Builders.Update + .Set(e => e.ProjectIds, new List()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + return true; + } + public async Task RemoveRoleIdFromCache(Guid employeeId, Guid roleId) + { + var filter = Builders.Filter + .Eq(e => e.EmployeeId, employeeId.ToString()); + + var update = Builders.Update + .Pull(e => e.ApplicationRoleIds, roleId.ToString()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + if (result.ModifiedCount == 0) + return false; + + return true; + } + public async Task ClearAllPermissionIdsByEmployeeIDFromCache(Guid employeeId) + { + var filter = Builders.Filter + .Eq(e => e.EmployeeId, employeeId.ToString()); + + var update = Builders.Update + .Set(e => e.PermissionIds, new List()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + return true; + } + public async Task ClearAllPermissionIdsByRoleIdFromCache(Guid roleId) + { + var filter = Builders.Filter.AnyEq(e => e.ApplicationRoleIds, roleId.ToString()); + + var update = Builders.Update + .Set(e => e.PermissionIds, new List()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + return true; + } + } +} diff --git a/Marco.Pms.CacheHelper/Marco.Pms.CacheHelper.csproj b/Marco.Pms.CacheHelper/Marco.Pms.CacheHelper.csproj new file mode 100644 index 0000000..e12ac6c --- /dev/null +++ b/Marco.Pms.CacheHelper/Marco.Pms.CacheHelper.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs new file mode 100644 index 0000000..b667694 --- /dev/null +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -0,0 +1,434 @@ +using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.MongoDBModels; +using Marco.Pms.Model.Projects; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Marco.Pms.CacheHelper +{ + public class ProjectCache + { + private readonly ApplicationDbContext _context; + private readonly IMongoDatabase _mongoDB; + //private readonly ILoggingService _logger; + public ProjectCache(ApplicationDbContext context, IConfiguration configuration) + { + var connectionString = configuration["MongoDB:ConnectionString"]; + _context = context; + var mongoUrl = new MongoUrl(connectionString); + var client = new MongoClient(mongoUrl); // Your MongoDB connection string + _mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name + } + public async Task AddProjectDetailsToCache(Project project) + { + var projectCollection = _mongoDB.GetCollection("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(), + AreaName = wa.AreaName, + PlannedWork = waPlanned, + CompletedWork = waCompleted + }); + + floorPlanned += waPlanned; + floorCompleted += waCompleted; + } + + floorMongoList.Add(new FloorMongoDB + { + Id = floor.Id.ToString(), + FloorName = floor.FloorName, + PlannedWork = floorPlanned, + CompletedWork = floorCompleted, + WorkAreas = workAreaMongoList + }); + + buildingPlanned += floorPlanned; + buildingCompleted += floorCompleted; + } + + buildingMongoList.Add(new BuildingMongoDB + { + Id = building.Id.ToString(), + BuildingName = building.Name, + Description = building.Description, + PlannedWork = buildingPlanned, + CompletedWork = buildingCompleted, + Floors = floorMongoList + }); + + totalPlannedWork += buildingPlanned; + totalCompletedWork += buildingCompleted; + } + + projectDetails.Buildings = buildingMongoList; + projectDetails.PlannedWork = totalPlannedWork; + projectDetails.CompletedWork = totalCompletedWork; + + await projectCollection.InsertOneAsync(projectDetails); + //_logger.LogInfo("[AddProjectDetails] Project details inserted in MongoDB for ProjectId: {ProjectId}", project.Id); + } + public async Task UpdateProjectDetailsOnlyToCache(Project project) + { + //_logger.LogInfo("Starting update for project: {ProjectId}", project.Id); + + var projectStatus = await _context.StatusMasters + .FirstOrDefaultAsync(s => s.Id == project.ProjectStatusId); + + if (projectStatus == null) + { + //_logger.LogWarning("StatusMaster not found for ProjectStatusId: {StatusId}", project.ProjectStatusId); + } + + var projectCollection = _mongoDB.GetCollection("ProjectDetails"); + + // Build the update definition + var updates = Builders.Update.Combine( + Builders.Update.Set(r => r.Name, project.Name), + Builders.Update.Set(r => r.ProjectAddress, project.ProjectAddress), + Builders.Update.Set(r => r.ShortName, project.ShortName), + Builders.Update.Set(r => r.ProjectStatus, new StatusMasterMongoDB + { + Id = projectStatus?.Id.ToString(), + Status = projectStatus?.Status + }), + Builders.Update.Set(r => r.StartDate, project.StartDate), + Builders.Update.Set(r => r.EndDate, project.EndDate), + Builders.Update.Set(r => r.ContactPerson, project.ContactPerson) + ); + + // Perform the update + var result = await projectCollection.UpdateOneAsync( + filter: r => r.Id == project.Id.ToString(), + update: updates + ); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("No project matched in MongoDB for update. ProjectId: {ProjectId}", project.Id); + return false; + } + + //_logger.LogInfo("Project {ProjectId} successfully updated in MongoDB", project.Id); + return true; + } + public async Task GetProjectDetailsFromCache(Guid projectId) + { + var projectCollection = _mongoDB.GetCollection("ProjectDetails"); + + // Build filter and projection to exclude large 'Buildings' list + var filter = Builders.Filter.Eq(p => p.Id, projectId.ToString()); + var projection = Builders.Projection.Exclude(p => p.Buildings); + + //_logger.LogInfo("Fetching project details for ProjectId: {ProjectId} from MongoDB", projectId); + + // Perform query + var project = await projectCollection + .Find(filter) + .Project(projection) + .FirstOrDefaultAsync(); + + if (project == null) + { + //_logger.LogWarning("No project found in MongoDB for ProjectId: {ProjectId}", projectId); + return null; + } + + //// Deserialize the result manually + //var project = BsonSerializer.Deserialize(result); + + //_logger.LogInfo("Successfully fetched project details (excluding Buildings) for ProjectId: {ProjectId}", projectId); + return project; + } + public async Task AddBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId) + { + var stringProjectId = projectId.ToString(); + var projectCollection = _mongoDB.GetCollection("ProjectDetails"); + + // Add Building + if (building != null) + { + var buildingMongo = new BuildingMongoDB + { + Id = building.Id.ToString(), + BuildingName = building.Name, + Description = building.Description, + PlannedWork = 0, + CompletedWork = 0, + Floors = new List() + }; + + var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); + var update = Builders.Update.Push("Buildings", buildingMongo); + + var result = await projectCollection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("Project not found while adding building. ProjectId: {ProjectId}", projectId); + return; + } + + //_logger.LogInfo("Building {BuildingId} added to project {ProjectId}", building.Id, projectId); + return; + } + + // Add Floor + if (floor != null) + { + var floorMongo = new FloorMongoDB + { + Id = floor.Id.ToString(), + FloorName = floor.FloorName, + PlannedWork = 0, + CompletedWork = 0, + WorkAreas = new List() + }; + + var filter = Builders.Filter.And( + Builders.Filter.Eq(p => p.Id, stringProjectId), + Builders.Filter.Eq("Buildings._id", floor.BuildingId.ToString()) + ); + + var update = Builders.Update.Push("Buildings.$.Floors", floorMongo); + var result = await projectCollection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("Project or building not found while adding floor. ProjectId: {ProjectId}, BuildingId: {BuildingId}", projectId, floor.BuildingId); + return; + } + + //_logger.LogInfo("Floor {FloorId} added to building {BuildingId} in project {ProjectId}", floor.Id, floor.BuildingId, projectId); + return; + } + + // Add WorkArea + if (workArea != null && buildingId != null) + { + var workAreaMongo = new WorkAreaMongoDB + { + Id = workArea.Id.ToString(), + AreaName = workArea.AreaName, + PlannedWork = 0, + CompletedWork = 0 + }; + + var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); + + var arrayFilters = new List + { + new JsonArrayFilterDefinition("{ 'b._id': '" + buildingId + "' }"), + new JsonArrayFilterDefinition("{ 'f._id': '" + workArea.FloorId + "' }") + }; + + var update = Builders.Update.Push("Buildings.$[b].Floors.$[f].WorkAreas", workAreaMongo); + var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; + + var result = await projectCollection.UpdateOneAsync(filter, update, updateOptions); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("Project or nested structure not found while adding work area. ProjectId: {ProjectId}, BuildingId: {BuildingId}, FloorId: {FloorId}", projectId, buildingId, workArea.FloorId); + return; + } + + //_logger.LogInfo("WorkArea {WorkAreaId} added to floor {FloorId} in building {BuildingId}, ProjectId: {ProjectId}", workArea.Id, workArea.FloorId, buildingId, projectId); + return; + } + + // Fallback case when no valid data was passed + //_logger.LogWarning("No valid infra data provided to add for ProjectId: {ProjectId}", projectId); + } + public async Task UpdateBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId) + { + var stringProjectId = projectId.ToString(); + var projectCollection = _mongoDB.GetCollection("ProjectDetails"); + + // Update Building + if (building != null) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(p => p.Id, stringProjectId), + Builders.Filter.Eq("Buildings._id", building.Id.ToString()) + ); + + var update = Builders.Update.Combine( + Builders.Update.Set("Buildings.$.BuildingName", building.Name), + Builders.Update.Set("Buildings.$.Description", building.Description) + ); + + var result = await projectCollection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("Update failed: Project or Building not found. ProjectId: {ProjectId}, BuildingId: {BuildingId}", projectId, building.Id); + return false; + } + + //_logger.LogInfo("Building {BuildingId} updated successfully in project {ProjectId}", building.Id, projectId); + return true; + } + + // Update Floor + if (floor != null) + { + var arrayFilters = new List + { + new JsonArrayFilterDefinition("{ 'b._id': '" + floor.BuildingId + "' }"), + new JsonArrayFilterDefinition("{ 'f._id': '" + floor.Id + "' }") + }; + + var update = Builders.Update.Set("Buildings.$[b].Floors.$[f].FloorName", floor.FloorName); + var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; + var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); + + var result = await projectCollection.UpdateOneAsync(filter, update, updateOptions); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("Update failed: Project or Floor not found. ProjectId: {ProjectId}, BuildingId: {BuildingId}, FloorId: {FloorId}", projectId, floor.BuildingId, floor.Id); + return false; + } + + //_logger.LogInfo("Floor {FloorId} updated successfully in Building {BuildingId}, ProjectId: {ProjectId}", floor.Id, floor.BuildingId, projectId); + return true; + } + + // Update WorkArea + if (workArea != null && buildingId != null) + { + var arrayFilters = new List + { + new JsonArrayFilterDefinition("{ 'b._id': '" + buildingId + "' }"), + new JsonArrayFilterDefinition("{ 'f._id': '" + workArea.FloorId + "' }"), + new JsonArrayFilterDefinition("{ 'a._id': '" + workArea.Id + "' }") + }; + + var update = Builders.Update.Set("Buildings.$[b].Floors.$[f].WorkAreas.$[a].AreaName", workArea.AreaName); + var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; + var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); + + var result = await projectCollection.UpdateOneAsync(filter, update, updateOptions); + + if (result.MatchedCount == 0) + { + //_logger.LogWarning("Update failed: Project or WorkArea not found. ProjectId: {ProjectId}, BuildingId: {BuildingId}, FloorId: {FloorId}, WorkAreaId: {WorkAreaId}", + //projectId, buildingId, workArea.FloorId, workArea.Id); + return false; + } + + //_logger.LogInfo("WorkArea {WorkAreaId} updated successfully in Floor {FloorId}, Building {BuildingId}, ProjectId: {ProjectId}", + //workArea.Id, workArea.FloorId, buildingId, projectId); + return true; + } + + //_logger.LogWarning("No update performed. Missing or invalid data for ProjectId: {ProjectId}", projectId); + return false; + } + public async Task?> GetBuildingInfraFromCache(Guid projectId) + { + var projectCollection = _mongoDB.GetCollection("ProjectDetails"); + + // Filter by project ID + var filter = Builders.Filter.Eq(p => p.Id, projectId.ToString()); + + // Project only the "Buildings" field from the document + var buildings = await projectCollection + .Find(filter) + .Project(p => p.Buildings) + .FirstOrDefaultAsync(); + + //if (buildings == null) + //{ + // _logger.LogWarning("No building infrastructure found for ProjectId: {ProjectId}", projectId); + //} + //else + //{ + // _logger.LogInfo("Fetched {Count} buildings for ProjectId: {ProjectId}", buildings.Count, projectId); + //} + + return buildings; + } + } +} diff --git a/Marco.Pms.Model/Marco.Pms.Model.csproj b/Marco.Pms.Model/Marco.Pms.Model.csproj index d5927ce..a1a21a5 100644 --- a/Marco.Pms.Model/Marco.Pms.Model.csproj +++ b/Marco.Pms.Model/Marco.Pms.Model.csproj @@ -10,6 +10,7 @@ + diff --git a/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs new file mode 100644 index 0000000..37218b7 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs @@ -0,0 +1,9 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class ActivityMasterMongoDB + { + public string? Id { get; set; } + public string? ActivityName { get; set; } + public string? UnitOfMeasurement { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs b/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs new file mode 100644 index 0000000..87ccb8d --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs @@ -0,0 +1,18 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class BuildingMongoDB + { + public string Id { get; set; } = string.Empty; + public string? BuildingName { get; set; } + public string? Description { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } + public List? Floors { get; set; } + } + public class BuildingMongoDBVM + { + public string Id { get; set; } = string.Empty; + public string? Name { get; set; } + public string? Description { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs b/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs new file mode 100644 index 0000000..f141798 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs @@ -0,0 +1,13 @@ +using MongoDB.Bson.Serialization.Attributes; + +namespace Marco.Pms.Model.MongoDBModels +{ + [BsonIgnoreExtraElements] + public class EmployeePermissionMongoDB + { + public string EmployeeId { get; set; } = string.Empty; + public List ApplicationRoleIds { get; set; } = new List(); + public List PermissionIds { get; set; } = new List(); + public List ProjectIds { get; set; } = new List(); + } +} diff --git a/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs b/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs new file mode 100644 index 0000000..ae3975f --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs @@ -0,0 +1,17 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class FloorMongoDB + { + public string Id { get; set; } = string.Empty; + public string? FloorName { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } + public List? WorkAreas { get; set; } + } + + public class FloorMongoDBVM + { + public string Id { get; set; } = string.Empty; + public string? FloorName { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs new file mode 100644 index 0000000..8bf1c9a --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs @@ -0,0 +1,18 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class ProjectMongoDB + { + public string? Id { get; set; } + public string? Name { get; set; } + public string? ShortName { get; set; } + public string? ProjectAddress { get; set; } + public string? ContactPerson { get; set; } + public List? Buildings { get; set; } + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + public StatusMasterMongoDB? ProjectStatus { get; set; } + public int TeamSize { get; set; } + public double CompletedWork { get; set; } + public double PlannedWork { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs new file mode 100644 index 0000000..01a0552 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs @@ -0,0 +1,8 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class StatusMasterMongoDB + { + public string? Id { get; set; } + public string? Status { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs new file mode 100644 index 0000000..d17f52c --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs @@ -0,0 +1,15 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class WorkAreaMongoDB + { + public string Id { get; set; } = string.Empty; + public string? AreaName { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } + } + public class WorkAreaMongoDBVM + { + public string Id { get; set; } = string.Empty; + public string? AreaName { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs new file mode 100644 index 0000000..aef0ada --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs @@ -0,0 +1,9 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class WorkCategoryMasterMongoDB + { + public string? Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + } +} diff --git a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs new file mode 100644 index 0000000..dc7fdb9 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs @@ -0,0 +1,15 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class WorkItemMongoDB + { + public string? Id { get; set; } + public string? WorkAreaId { get; set; } + public ActivityMasterMongoDB? ActivityMaster { get; set; } + public WorkCategoryMasterMongoDB? WorkCategoryMaster { get; set; } + public string? ParentTaskId { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } + public string? Description { get; set; } + public DateTime TaskDate { get; set; } + } +} diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 6490c54..a440c21 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -2,10 +2,13 @@ using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Employees; 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; using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Model.ViewModels.Projects; +using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Service; using MarcoBMS.Services.Helpers; @@ -14,6 +17,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; +using MongoDB.Driver; namespace MarcoBMS.Services.Controllers { @@ -29,6 +33,7 @@ namespace MarcoBMS.Services.Controllers private readonly ProjectsHelper _projectsHelper; private readonly IHubContext _signalR; private readonly PermissionServices _permission; + private readonly CacheUpdateHelper _cache; private readonly Guid ViewProjects; private readonly Guid ManageProject; private readonly Guid ViewInfra; @@ -37,7 +42,7 @@ namespace MarcoBMS.Services.Controllers public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, ProjectsHelper projectHelper, - IHubContext signalR, PermissionServices permission) + IHubContext signalR, PermissionServices permission, CacheUpdateHelper cache) { _context = context; _userHelper = userHelper; @@ -45,13 +50,13 @@ namespace MarcoBMS.Services.Controllers _rolesHelper = rolesHelper; _projectsHelper = projectHelper; _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"); tenantId = _userHelper.GetTenantId(); - } [HttpGet("list/basic")] @@ -222,24 +227,54 @@ namespace MarcoBMS.Services.Controllers } // Step 5: Fetch project with status - var project = await _context.Projects + var projectDetails = await _cache.GetProjectDetails(id); + ProjectVM? projectVM = null; + if (projectDetails == null) + { + var project = await _context.Projects .Include(c => c.ProjectStatus) .FirstOrDefaultAsync(c => c.TenantId == tenantId && c.Id == id); + projectVM = GetProjectViewModel(project); + } + else + { + 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 (project == null) + if (projectVM == null) { _logger.LogWarning("Project not found. ProjectId: {ProjectId}", id); return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); } - // Step 6: Map and return result - var projectVM = GetProjectViewModel(project); + // Step 6: Return result + _logger.LogInfo("Project details fetched successfully. ProjectId: {ProjectId}", id); return Ok(ApiResponse.SuccessResponse(projectVM, "Project details fetched successfully", 200)); } - private ProjectVM GetProjectViewModel(Project project) + private ProjectVM? GetProjectViewModel(Project? project) { + if (project == null) + { + return null; + } return new ProjectVM { Id = project.Id, @@ -280,6 +315,9 @@ namespace MarcoBMS.Services.Controllers _context.Projects.Add(project); await _context.SaveChangesAsync(); + + await _cache.AddProjectDetails(project); + var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Create_Project", Response = project.ToProjectDto() }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); @@ -310,6 +348,13 @@ namespace MarcoBMS.Services.Controllers await _context.SaveChangesAsync(); + // Cache functions + bool isUpdated = await _cache.UpdateProjectDetailsOnly(project); + if (!isUpdated) + { + await _cache.AddProjectDetails(project); + } + var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Update_Project", Response = project.ToProjectDto() }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); @@ -524,6 +569,7 @@ namespace MarcoBMS.Services.Controllers employeeIds.Add(projectAllocation.EmployeeId); projectIds.Add(projectAllocation.ProjectId); } + await _cache.ClearAllProjectIds(item.EmpID); } catch (Exception ex) @@ -565,53 +611,102 @@ namespace MarcoBMS.Services.Controllers _logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have access to view infra", 403)); } - - // Step 4: Fetch buildings for the project - var buildings = await _context.Buildings - .Where(b => b.ProjectId == projectId) - .ToListAsync(); - - var buildingIds = buildings.Select(b => b.Id).ToList(); - - // Step 5: Fetch floors associated with the buildings - var floors = await _context.Floor - .Where(f => buildingIds.Contains(f.BuildingId)) - .ToListAsync(); - - var floorIds = floors.Select(f => f.Id).ToList(); - - // Step 6: Fetch work areas associated with the floors - var workAreas = await _context.WorkAreas - .Where(wa => floorIds.Contains(wa.FloorId)) - .ToListAsync(); - - // Step 7: Build the infra hierarchy (Building > Floors > Work Areas) - var infraVM = buildings.Select(b => + var result = await _cache.GetBuildingInfra(projectId); + if (result == null) { - var selectedFloors = floors - .Where(f => f.BuildingId == b.Id) - .Select(f => new - { - Id = f.Id, - FloorName = f.FloorName, - WorkAreas = workAreas - .Where(wa => wa.FloorId == f.Id) - .Select(wa => new { wa.Id, wa.AreaName }) - .ToList() - }).ToList(); - return new + // Step 4: Fetch buildings for the project + var buildings = await _context.Buildings + .Where(b => b.ProjectId == projectId) + .ToListAsync(); + + var buildingIds = buildings.Select(b => b.Id).ToList(); + + // Step 5: Fetch floors associated with the buildings + var floors = await _context.Floor + .Where(f => buildingIds.Contains(f.BuildingId)) + .ToListAsync(); + + var floorIds = floors.Select(f => f.Id).ToList(); + + // Step 6: Fetch work areas associated with the floors + var workAreas = await _context.WorkAreas + .Where(wa => floorIds.Contains(wa.FloorId)) + .ToListAsync(); + var workAreaIds = workAreas.Select(wa => wa.Id).ToList(); + + // Step 7: Fetch work items associated with the work area + var workItems = await _context.WorkItems + .Where(wi => workAreaIds.Contains(wi.WorkAreaId)) + .ToListAsync(); + + // Step 8: Build the infra hierarchy (Building > Floors > Work Areas) + List Buildings = new List(); + foreach (var building in buildings) { - Id = b.Id, - BuildingName = b.Name, - Floors = selectedFloors - }; - }).ToList(); + double buildingPlannedWorks = 0; + double buildingCompletedWorks = 0; + + var selectedFloors = floors.Where(f => f.BuildingId == building.Id).ToList(); + List Floors = new List(); + foreach (var floor in selectedFloors) + { + double floorPlannedWorks = 0; + double floorCompletedWorks = 0; + var selectedWorkAreas = workAreas.Where(wa => wa.FloorId == floor.Id).ToList(); + List WorkAreas = new List(); + foreach (var workArea in selectedWorkAreas) + { + double workAreaPlannedWorks = 0; + double workAreaCompletedWorks = 0; + var selectedWorkItems = workItems.Where(wi => wi.WorkAreaId == workArea.Id).ToList(); + foreach (var workItem in selectedWorkItems) + { + workAreaPlannedWorks += workItem.PlannedWork; + workAreaCompletedWorks += workItem.CompletedWork; + } + WorkAreaMongoDB workAreaMongo = new WorkAreaMongoDB + { + Id = workArea.Id.ToString(), + AreaName = workArea.AreaName, + PlannedWork = workAreaPlannedWorks, + CompletedWork = workAreaCompletedWorks + }; + WorkAreas.Add(workAreaMongo); + floorPlannedWorks += workAreaPlannedWorks; + floorCompletedWorks += workAreaCompletedWorks; + } + FloorMongoDB floorMongoDB = new FloorMongoDB + { + Id = floor.Id.ToString(), + FloorName = floor.FloorName, + PlannedWork = floorPlannedWorks, + CompletedWork = floorCompletedWorks, + WorkAreas = WorkAreas + }; + Floors.Add(floorMongoDB); + buildingPlannedWorks += floorPlannedWorks; + buildingCompletedWorks += floorCompletedWorks; + } + + var buildingMongo = new BuildingMongoDB + { + Id = building.Id.ToString(), + BuildingName = building.Name, + Description = building.Description, + PlannedWork = buildingPlannedWorks, + CompletedWork = buildingCompletedWorks, + Floors = Floors + }; + Buildings.Add(buildingMongo); + } + result = Buildings; + } _logger.LogInfo("Infra details fetched successfully for ProjectId: {ProjectId}, EmployeeId: {EmployeeId}, Buildings: {Count}", - projectId, loggedInEmployee.Id, infraVM.Count); + projectId, loggedInEmployee.Id, result.Count); - return Ok(ApiResponse.SuccessResponse(infraVM, "Infra details fetched successfully", 200)); + return Ok(ApiResponse.SuccessResponse(result, "Infra details fetched successfully", 200)); } [HttpGet("tasks/{workAreaId}")] @@ -807,6 +902,7 @@ namespace MarcoBMS.Services.Controllers responseData.building = building; responseMessage = "Buliding Added Successfully"; message = "Building Added"; + await _cache.AddBuildngInfra(building.ProjectId, building); } else { @@ -816,7 +912,7 @@ namespace MarcoBMS.Services.Controllers responseData.building = building; responseMessage = "Buliding Updated Successfully"; message = "Building Updated"; - + await _cache.UpdateBuildngInfra(building.ProjectId, building); } projectIds.Add(building.ProjectId); } @@ -824,6 +920,7 @@ namespace MarcoBMS.Services.Controllers { Floor floor = item.Floor.ToFloorFromFloorDto(tenantId); floor.TenantId = GetTenantId(); + bool isCreated = false; if (item.Floor.Id == null) { @@ -833,6 +930,7 @@ namespace MarcoBMS.Services.Controllers responseData.floor = floor; responseMessage = "Floor Added Successfully"; message = "Floor Added"; + isCreated = true; } else { @@ -844,13 +942,23 @@ namespace MarcoBMS.Services.Controllers message = "Floor Updated"; } Building? building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == floor.BuildingId); - projectIds.Add(building?.ProjectId ?? Guid.Empty); + var projectId = building?.ProjectId ?? Guid.Empty; + projectIds.Add(projectId); message = $"{message} in Building: {building?.Name}"; + if (isCreated) + { + await _cache.AddBuildngInfra(projectId, floor: floor); + } + else + { + await _cache.UpdateBuildngInfra(projectId, floor: floor); + } } if (item.WorkArea != null) { WorkArea workArea = item.WorkArea.ToWorkAreaFromWorkAreaDto(tenantId); workArea.TenantId = GetTenantId(); + bool isCreated = false; if (item.WorkArea.Id == null) { @@ -860,6 +968,7 @@ namespace MarcoBMS.Services.Controllers responseData.workArea = workArea; responseMessage = "Work Area Added Successfully"; message = "Work Area Added"; + isCreated = true; } else { @@ -871,8 +980,17 @@ namespace MarcoBMS.Services.Controllers message = "Work Area Updated"; } Floor? floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == workArea.FloorId); - projectIds.Add(floor?.Building?.ProjectId ?? Guid.Empty); + var projectId = floor?.Building?.ProjectId ?? Guid.Empty; + projectIds.Add(projectId); message = $"{message} in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}"; + if (isCreated) + { + await _cache.AddBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); + } + else + { + await _cache.UpdateBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); + } } } message = $"{message} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}"; @@ -996,6 +1114,7 @@ namespace MarcoBMS.Services.Controllers return Ok(ApiResponse.ErrorResponse(ex.Message, ex, 400)); } } + await _cache.ClearAllProjectIds(employeeId); var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeId = employeeId }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); diff --git a/Marco.Pms.Services/Controllers/RolesController.cs b/Marco.Pms.Services/Controllers/RolesController.cs index 2ac2b07..4c75b3e 100644 --- a/Marco.Pms.Services/Controllers/RolesController.cs +++ b/Marco.Pms.Services/Controllers/RolesController.cs @@ -10,6 +10,7 @@ using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels; using Marco.Pms.Model.ViewModels.Master; using Marco.Pms.Model.ViewModels.Roles; +using Marco.Pms.Services.Helpers; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; @@ -29,14 +30,17 @@ namespace MarcoBMS.Services.Controllers private readonly UserHelper _userHelper; private readonly UserManager _userManager; private readonly ILoggingService _logger; + private readonly CacheUpdateHelper _cache; - public RolesController(UserManager userManager, ApplicationDbContext context, RolesHelper rolesHelper, UserHelper userHelper, ILoggingService logger) + public RolesController(UserManager userManager, ApplicationDbContext context, RolesHelper rolesHelper, UserHelper userHelper, ILoggingService logger, + CacheUpdateHelper cache) { _context = context; _userManager = userManager; _rolesHelper = rolesHelper; _userHelper = userHelper; _logger = logger; + _cache = cache; } private Guid GetTenantId() @@ -292,6 +296,8 @@ namespace MarcoBMS.Services.Controllers if (modified) await _context.SaveChangesAsync(); + await _cache.ClearAllPermissionIdsByRoleId(id); + ApplicationRolesVM response = role.ToRoleVMFromApplicationRole(); List permissions = await _rolesHelper.GetFeaturePermissionByRoleID(response.Id); response.FeaturePermission = permissions.Select(c => c.ToFeaturePermissionVMFromFeaturePermission()).ToList(); @@ -424,12 +430,16 @@ namespace MarcoBMS.Services.Controllers if (role.IsEnabled == true) { _context.EmployeeRoleMappings.Add(mapping); + await _cache.AddApplicationRole(role.EmployeeId, [mapping.RoleId]); } } else if (role.IsEnabled == false) { _context.EmployeeRoleMappings.Remove(existingItem); + await _cache.RemoveRoleId(existingItem.EmployeeId, existingItem.RoleId); + await _cache.ClearAllPermissionIdsByEmployeeID(existingItem.EmployeeId); } + await _cache.ClearAllProjectIds(role.EmployeeId); } await _context.SaveChangesAsync(); diff --git a/Marco.Pms.Services/Dockerfile b/Marco.Pms.Services/Dockerfile index 5444e56..77311ee 100644 --- a/Marco.Pms.Services/Dockerfile +++ b/Marco.Pms.Services/Dockerfile @@ -19,6 +19,7 @@ COPY ["Marco.Pms.Services/Marco.Pms.Services.csproj", "Marco.Pms.Services/"] COPY ["Marco.Pms.DataAccess/Marco.Pms.DataAccess.csproj", "Marco.Pms.DataAccess/"] COPY ["Marco.Pms.Model/Marco.Pms.Model.csproj", "Marco.Pms.Model/"] COPY ["Marco.Pms.Utility/Marco.Pms.Utility.csproj", "Marco.Pms.Utility/"] +COPY ["Marco.Pms.Utility/Marco.Pms.CacheHelper.csproj", "Marco.Pms.CacheHelper/"] RUN dotnet restore "./Marco.Pms.Services/Marco.Pms.Services.csproj" COPY . . WORKDIR "/src/Marco.Pms.Services" diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs new file mode 100644 index 0000000..1c3ee70 --- /dev/null +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -0,0 +1,98 @@ +using Marco.Pms.CacheHelper; +using Marco.Pms.Model.MongoDBModels; +using Marco.Pms.Model.Projects; +using Project = Marco.Pms.Model.Projects.Project; + +namespace Marco.Pms.Services.Helpers +{ + public class CacheUpdateHelper + { + private readonly ProjectCache _projectCache; + private readonly EmployeeCache _employeeCache; + + public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache) + { + _projectCache = projectCache; + _employeeCache = employeeCache; + } + + // ------------------------------------ Project Details and Infrastructure Cache --------------------------------------- + public async Task AddProjectDetails(Project project) + { + await _projectCache.AddProjectDetailsToCache(project); + } + public async Task UpdateProjectDetailsOnly(Project project) + { + bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project); + return response; + } + public async Task GetProjectDetails(Guid projectId) + { + var response = await _projectCache.GetProjectDetailsFromCache(projectId); + return response; + } + public async Task AddBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) + { + await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + } + public async Task UpdateBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) + { + var response = await _projectCache.UpdateBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + if (!response) + { + await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + } + } + public async Task?> GetBuildingInfra(Guid projectId) + { + var response = await _projectCache.GetBuildingInfraFromCache(projectId); + return response; + } + + + // ------------------------------------ Employee Profile Cache --------------------------------------- + public async Task AddApplicationRole(Guid employeeId, List roleIds) + { + var response = await _employeeCache.AddApplicationRoleToCache(employeeId, roleIds); + } + public async Task AddProjects(Guid employeeId, List projectIds) + { + var response = await _employeeCache.AddProjectsToCache(employeeId, projectIds); + return response; + } + public async Task?> GetProjects(Guid employeeId) + { + var response = await _employeeCache.GetProjectsFromCache(employeeId); + if (response.Count > 0) + { + return response; + } + return null; + } + public async Task?> GetPermissions(Guid employeeId) + { + var response = await _employeeCache.GetPermissionsFromCache(employeeId); + if (response.Count > 0) + { + return response; + } + return null; + } + public async Task ClearAllProjectIds(Guid employeeId) + { + var response = await _employeeCache.ClearAllProjectIdsFromCache(employeeId); + } + public async Task ClearAllPermissionIdsByEmployeeID(Guid employeeId) + { + var response = await _employeeCache.ClearAllPermissionIdsByEmployeeIDFromCache(employeeId); + } + public async Task ClearAllPermissionIdsByRoleId(Guid roleId) + { + var response = await _employeeCache.ClearAllPermissionIdsByRoleIdFromCache(roleId); + } + public async Task RemoveRoleId(Guid employeeId, Guid roleId) + { + var response = await _employeeCache.RemoveRoleIdFromCache(employeeId, roleId); + } + } +} diff --git a/Marco.Pms.Services/Helpers/ProjectsHelper.cs b/Marco.Pms.Services/Helpers/ProjectsHelper.cs index 50bb9a0..3ccddba 100644 --- a/Marco.Pms.Services/Helpers/ProjectsHelper.cs +++ b/Marco.Pms.Services/Helpers/ProjectsHelper.cs @@ -2,6 +2,7 @@ using Marco.Pms.Model.Employees; using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Projects; +using Marco.Pms.Services.Helpers; using Microsoft.EntityFrameworkCore; namespace MarcoBMS.Services.Helpers @@ -10,12 +11,14 @@ namespace MarcoBMS.Services.Helpers { private readonly ApplicationDbContext _context; private readonly RolesHelper _rolesHelper; + private readonly CacheUpdateHelper _cache; - public ProjectsHelper(ApplicationDbContext context, RolesHelper rolesHelper) + public ProjectsHelper(ApplicationDbContext context, RolesHelper rolesHelper, CacheUpdateHelper cache) { _context = context; _rolesHelper = rolesHelper; + _cache = cache; } public async Task> GetAllProjectByTanentID(Guid tanentID) @@ -49,40 +52,56 @@ namespace MarcoBMS.Services.Helpers public async Task> GetMyProjects(Guid tenantId, Employee LoggedInEmployee) { - List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(LoggedInEmployee.Id); - string[] projectsId = []; List projects = new List(); - // Define a common queryable base for projects - IQueryable projectQuery = _context.Projects.Where(c => c.TenantId == tenantId); + var projectIds = await _cache.GetProjects(LoggedInEmployee.Id); - // 2. Optimized Project Retrieval Logic - // 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) { - // 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 + projects = await _context.Projects.Where(p => projectIds.Contains(p.Id)).ToListAsync(); } 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()) + var featurePermissionIds = await _cache.GetPermissions(LoggedInEmployee.Id); + if (featurePermissionIds == null) { - return new List(); + 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); - // Use LINQ's Contains for efficient filtering by ProjectId - var projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); // Get distinct Guids + // 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 + } + else + { + // 3. Efficiently get project allocations and then filter projects + // Load allocations only once + var allocation = await GetProjectByEmployeeID(LoggedInEmployee.Id); - // Filter projects based on the retrieved ProjectIds - projects = await projectQuery.Where(c => projectIds.Contains(c.Id)).ToListAsync(); + // If there are no allocations, return an empty list early + if (allocation == null || !allocation.Any()) + { + return new List(); + } + + // 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(); + + } + projectIds = projects.Select(p => p.Id).ToList(); + await _cache.AddProjects(LoggedInEmployee.Id, projectIds); } return projects; diff --git a/Marco.Pms.Services/Helpers/RolesHelper.cs b/Marco.Pms.Services/Helpers/RolesHelper.cs index b571d03..15bf0b1 100644 --- a/Marco.Pms.Services/Helpers/RolesHelper.cs +++ b/Marco.Pms.Services/Helpers/RolesHelper.cs @@ -2,6 +2,7 @@ using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Entitlements; +using Marco.Pms.Services.Helpers; using Microsoft.EntityFrameworkCore; namespace MarcoBMS.Services.Helpers @@ -9,15 +10,19 @@ namespace MarcoBMS.Services.Helpers public class RolesHelper { private readonly ApplicationDbContext _context; - public RolesHelper(ApplicationDbContext context) + private readonly CacheUpdateHelper _cache; + public RolesHelper(ApplicationDbContext context, CacheUpdateHelper cache) { _context = context; + _cache = cache; } public async Task> GetFeaturePermissionByEmployeeID(Guid EmployeeID) { List roleMappings = await _context.EmployeeRoleMappings.Where(c => c.EmployeeId == EmployeeID && c.IsEnabled == true).Select(c => c.RoleId).ToListAsync(); + await _cache.AddApplicationRole(EmployeeID, roleMappings); + // _context.RolePermissionMappings var result = await (from rpm in _context.RolePermissionMappings diff --git a/Marco.Pms.Services/Marco.Pms.Services.csproj b/Marco.Pms.Services/Marco.Pms.Services.csproj index 7bef32f..a235e6a 100644 --- a/Marco.Pms.Services/Marco.Pms.Services.csproj +++ b/Marco.Pms.Services/Marco.Pms.Services.csproj @@ -44,6 +44,7 @@ + diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index 17eb5c7..1d9b4b3 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -1,4 +1,5 @@ using System.Text; +using Marco.Pms.CacheHelper; using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Authentication; using Marco.Pms.Model.Entitlements; @@ -136,6 +137,9 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddSingleton(); @@ -225,7 +229,7 @@ app.UseStaticFiles(); // Enables serving static files app.UseHttpsRedirection(); - +app.UseAuthentication(); app.UseAuthorization(); app.MapHub("/hubs/marco"); app.MapControllers(); diff --git a/Marco.Pms.Services/Service/PermissionServices.cs b/Marco.Pms.Services/Service/PermissionServices.cs index f3ddb58..ce7476b 100644 --- a/Marco.Pms.Services/Service/PermissionServices.cs +++ b/Marco.Pms.Services/Service/PermissionServices.cs @@ -2,6 +2,7 @@ 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,21 +13,24 @@ namespace Marco.Pms.Services.Service private readonly ApplicationDbContext _context; private readonly RolesHelper _rolesHelper; private readonly ProjectsHelper _projectsHelper; - public PermissionServices(ApplicationDbContext context, RolesHelper rolesHelper, ProjectsHelper projectsHelper) + private readonly CacheUpdateHelper _cache; + public PermissionServices(ApplicationDbContext context, RolesHelper rolesHelper, ProjectsHelper projectsHelper, CacheUpdateHelper cache) { _context = context; _rolesHelper = rolesHelper; _projectsHelper = projectsHelper; + _cache = cache; } public async Task HasPermission(Guid featurePermissionId, Guid employeeId) { - var hasPermission = await _context.EmployeeRoleMappings - .Where(er => er.EmployeeId == employeeId) - .Select(er => er.RoleId) - .Distinct() - .AnyAsync(roleId => _context.RolePermissionMappings - .Any(rp => rp.FeaturePermissionId == featurePermissionId && rp.ApplicationRoleId == roleId)); + var featurePermissionIds = await _cache.GetPermissions(employeeId); + if (featurePermissionIds == null) + { + List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(employeeId); + featurePermissionIds = featurePermission.Select(fp => fp.Id).ToList(); + } + var hasPermission = featurePermissionIds.Contains(featurePermissionId); return hasPermission; } public async Task HasProjectPermission(Employee emp, string projectId) diff --git a/Marco.Pms.Services/appsettings.Development.json b/Marco.Pms.Services/appsettings.Development.json index 1565018..ce80dc0 100644 --- a/Marco.Pms.Services/appsettings.Development.json +++ b/Marco.Pms.Services/appsettings.Development.json @@ -47,6 +47,8 @@ "BucketName": "testenv-marco-pms-documents" }, "MongoDB": { - "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs" + "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs", + "ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches" + //"DatabaseName": "" } } diff --git a/Marco.Pms.Services/appsettings.Production.json b/Marco.Pms.Services/appsettings.Production.json index 81aa998..0abe3f1 100644 --- a/Marco.Pms.Services/appsettings.Production.json +++ b/Marco.Pms.Services/appsettings.Production.json @@ -6,7 +6,7 @@ }, "Environment": { "Name": "Production", - "Title": "" + "Title": "" }, "ConnectionStrings": { "DefaultConnectionString": "Server=147.93.98.152;User ID=devuser;Password=AppUser@123$;Database=MarcoBMS1" @@ -40,6 +40,7 @@ "BucketName": "testenv-marco-pms-documents" }, "MongoDB": { - "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs" + "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs", + "ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches" } } \ No newline at end of file diff --git a/marco.pms.api.sln b/marco.pms.api.sln index 49d3e8c..424b709 100644 --- a/marco.pms.api.sln +++ b/marco.pms.api.sln @@ -11,6 +11,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Marco.Pms.Utility", "Marco. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Marco.Pms.Services", "Marco.Pms.Services\Marco.Pms.Services.csproj", "{27A83653-5B7F-4135-9886-01594D54AFAE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Marco.Pms.CacheHelper", "Marco.Pms.CacheHelper\Marco.Pms.CacheHelper.csproj", "{1A105C22-4ED7-4F54-8834-6923DDD96852}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,6 +35,10 @@ Global {27A83653-5B7F-4135-9886-01594D54AFAE}.Debug|Any CPU.Build.0 = Debug|Any CPU {27A83653-5B7F-4135-9886-01594D54AFAE}.Release|Any CPU.ActiveCfg = Release|Any CPU {27A83653-5B7F-4135-9886-01594D54AFAE}.Release|Any CPU.Build.0 = Release|Any CPU + {1A105C22-4ED7-4F54-8834-6923DDD96852}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A105C22-4ED7-4F54-8834-6923DDD96852}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A105C22-4ED7-4F54-8834-6923DDD96852}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A105C22-4ED7-4F54-8834-6923DDD96852}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 129ccf7faef77e2e971d66d153376bfa0e7eb231 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Fri, 4 Jul 2025 17:50:27 +0530 Subject: [PATCH 050/124] removed comented code from appsetting file --- Marco.Pms.Services/appsettings.Development.json | 1 - 1 file changed, 1 deletion(-) diff --git a/Marco.Pms.Services/appsettings.Development.json b/Marco.Pms.Services/appsettings.Development.json index ce80dc0..5f5e19d 100644 --- a/Marco.Pms.Services/appsettings.Development.json +++ b/Marco.Pms.Services/appsettings.Development.json @@ -49,6 +49,5 @@ "MongoDB": { "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs", "ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches" - //"DatabaseName": "" } } From 3d8e91d58d0d1d7039bc6eb36a60c6e37b8d0b43 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Sat, 5 Jul 2025 15:25:01 +0530 Subject: [PATCH 051/124] Added error handling in cache helper --- .../Helpers/CacheUpdateHelper.cs | 170 +++++++++++++++--- 1 file changed, 143 insertions(+), 27 deletions(-) diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 1c3ee70..75b51b5 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -1,6 +1,7 @@ using Marco.Pms.CacheHelper; using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.Projects; +using MarcoBMS.Services.Service; using Project = Marco.Pms.Model.Projects.Project; namespace Marco.Pms.Services.Helpers @@ -9,90 +10,205 @@ namespace Marco.Pms.Services.Helpers { private readonly ProjectCache _projectCache; private readonly EmployeeCache _employeeCache; + private readonly ILoggingService _logger; - public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache) + public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache, ILoggingService logger) { _projectCache = projectCache; _employeeCache = employeeCache; + _logger = logger; } // ------------------------------------ Project Details and Infrastructure Cache --------------------------------------- public async Task AddProjectDetails(Project project) { - await _projectCache.AddProjectDetailsToCache(project); + try + { + await _projectCache.AddProjectDetailsToCache(project); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while adding project to Cache: {Error}", ex.Message); + } } public async Task UpdateProjectDetailsOnly(Project project) { - bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project); - return response; + try + { + bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while updating project to Cache: {Error}", ex.Message); + return false; + } } public async Task GetProjectDetails(Guid projectId) { - var response = await _projectCache.GetProjectDetailsFromCache(projectId); - return response; + try + { + var response = await _projectCache.GetProjectDetailsFromCache(projectId); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while getting project to Cache: {Error}", ex.Message); + return null; + } } + //public async Task?> GetProjectDetailsList(List projectIds) + //{ + // var response = await _projectCache.GetProjectDetailsListFromCache(projectIds); + // return response; + //} public async Task AddBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) { - await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + try + { + await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while adding project infra to Cache: {Error}", ex.Message); + } } public async Task UpdateBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) { - var response = await _projectCache.UpdateBuildngInfraToCache(projectId, building, floor, workArea, buildingId); - if (!response) + try { - await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + var response = await _projectCache.UpdateBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + if (!response) + { + await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + } + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while updating project infra to Cache: {Error}", ex.Message); } } public async Task?> GetBuildingInfra(Guid projectId) { - var response = await _projectCache.GetBuildingInfraFromCache(projectId); - return response; + try + { + var response = await _projectCache.GetBuildingInfraFromCache(projectId); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while getting project infra Cache: {Error}", ex.Message); + return null; + } } // ------------------------------------ Employee Profile Cache --------------------------------------- public async Task AddApplicationRole(Guid employeeId, List roleIds) { - var response = await _employeeCache.AddApplicationRoleToCache(employeeId, roleIds); + try + { + var response = await _employeeCache.AddApplicationRoleToCache(employeeId, roleIds); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while adding Application roleIds to Cache to employee {Employee}: {Error}", employeeId, ex.Message); + } } public async Task AddProjects(Guid employeeId, List projectIds) { - var response = await _employeeCache.AddProjectsToCache(employeeId, projectIds); - return response; + try + { + var response = await _employeeCache.AddProjectsToCache(employeeId, projectIds); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while adding projectIds to Cache: {Error}", ex.Message); + return false; + } } public async Task?> GetProjects(Guid employeeId) { - var response = await _employeeCache.GetProjectsFromCache(employeeId); - if (response.Count > 0) + try { - return response; + var response = await _employeeCache.GetProjectsFromCache(employeeId); + if (response.Count > 0) + { + return response; + } + return null; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while getting projectIDs to Cache: {Error}", ex.Message); + return null; } - return null; } public async Task?> GetPermissions(Guid employeeId) { - var response = await _employeeCache.GetPermissionsFromCache(employeeId); - if (response.Count > 0) + try { - return response; + var response = await _employeeCache.GetPermissionsFromCache(employeeId); + if (response.Count > 0) + { + return response; + } + return null; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while getting permissionIds to Cache: {Error}", ex.Message); + return null; } - return null; } public async Task ClearAllProjectIds(Guid employeeId) { - var response = await _employeeCache.ClearAllProjectIdsFromCache(employeeId); + try + { + var response = await _employeeCache.ClearAllProjectIdsFromCache(employeeId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting projectIds from Cache for employee {EmployeeId}: {Error}", employeeId, ex.Message); + } } + //public async Task ClearAllProjectIdsByRoleId(Guid roleId) + //{ + // await _employeeCache.ClearAllProjectIdsByRoleIdFromCache(roleId); + //} public async Task ClearAllPermissionIdsByEmployeeID(Guid employeeId) { - var response = await _employeeCache.ClearAllPermissionIdsByEmployeeIDFromCache(employeeId); + try + { + var response = await _employeeCache.ClearAllPermissionIdsByEmployeeIDFromCache(employeeId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting permissionIds from to Cache: {Error}", ex.Message); + } } public async Task ClearAllPermissionIdsByRoleId(Guid roleId) { - var response = await _employeeCache.ClearAllPermissionIdsByRoleIdFromCache(roleId); + try + { + var response = await _employeeCache.ClearAllPermissionIdsByRoleIdFromCache(roleId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting permissionIds from to Cache: {Error}", ex.Message); + } } public async Task RemoveRoleId(Guid employeeId, Guid roleId) { - var response = await _employeeCache.RemoveRoleIdFromCache(employeeId, roleId); + try + { + var response = await _employeeCache.RemoveRoleIdFromCache(employeeId, roleId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting Application roleIds from to Cache: {Error}", ex.Message); + } } } } From d8cf87aee4dd55fc0e417a60bb439ae3dc5e27fb Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 7 Jul 2025 10:04:11 +0530 Subject: [PATCH 052/124] Implemented the methods for deleting permission am asigned project from caches for certien employee --- Marco.Pms.CacheHelper/EmployeeCache.cs | 60 ++++++++++++++----- Marco.Pms.CacheHelper/ProjectCache.cs | 55 ++++++++++------- .../EmployeePermissionMongoDB.cs | 2 +- .../MongoDBModels/ProjectMongoDB.cs | 2 +- .../MongoDBModels/WorkItemMongoDB.cs | 9 +-- .../Controllers/RolesController.cs | 4 ++ .../Helpers/CacheUpdateHelper.cs | 57 +++++++++++------- Marco.Pms.Services/Helpers/ProjectsHelper.cs | 21 ++++++- 8 files changed, 144 insertions(+), 66 deletions(-) diff --git a/Marco.Pms.CacheHelper/EmployeeCache.cs b/Marco.Pms.CacheHelper/EmployeeCache.cs index 7d75407..5c86e6f 100644 --- a/Marco.Pms.CacheHelper/EmployeeCache.cs +++ b/Marco.Pms.CacheHelper/EmployeeCache.cs @@ -22,31 +22,47 @@ namespace Marco.Pms.CacheHelper } public async Task AddApplicationRoleToCache(Guid employeeId, List roleIds) { - var newRoleIds = roleIds.Select(r => r.ToString()).ToList(); - var newPermissionIds = await _context.RolePermissionMappings + // 1. Guard Clause: Avoid unnecessary database work if there are no roles to add. + if (roleIds == null || !roleIds.Any()) + { + return false; // Nothing to add, so the operation did not result in a change. + } + + // 2. Perform database queries concurrently for better performance. + var employeeIdString = employeeId.ToString(); + + Task> getPermissionIdsTask = _context.RolePermissionMappings .Where(rp => roleIds.Contains(rp.ApplicationRoleId)) .Select(p => p.FeaturePermissionId.ToString()) .Distinct() .ToListAsync(); - var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + // 3. Prepare role IDs in parallel with the database query. + var newRoleIds = roleIds.Select(r => r.ToString()).ToList(); + + // 4. Await the database query result. + var newPermissionIds = await getPermissionIdsTask; + + // 5. Build a single, efficient update operation. + var filter = Builders.Filter.Eq(e => e.Id, employeeIdString); var update = Builders.Update .AddToSetEach(e => e.ApplicationRoleIds, newRoleIds) .AddToSetEach(e => e.PermissionIds, newPermissionIds); - var result = await _collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); - if (result.MatchedCount == 0) - { - return false; - } - return true; + var options = new UpdateOptions { IsUpsert = true }; + + var result = await _collection.UpdateOneAsync(filter, update, options); + + // 6. Return a more accurate result indicating success for both updates and upserts. + // The operation is successful if an existing document was modified OR a new one was created. + return result.IsAcknowledged && (result.ModifiedCount > 0 || result.UpsertedId != null); } public async Task AddProjectsToCache(Guid employeeId, List projectIds) { var newprojectIds = projectIds.Select(p => p.ToString()).ToList(); - var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + var filter = Builders.Filter.Eq(e => e.Id, employeeId.ToString()); var update = Builders.Update .AddToSetEach(e => e.ProjectIds, newprojectIds); @@ -60,7 +76,7 @@ namespace Marco.Pms.CacheHelper } public async Task> GetProjectsFromCache(Guid employeeId) { - var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + var filter = Builders.Filter.Eq(e => e.Id, employeeId.ToString()); var result = await _collection @@ -77,7 +93,7 @@ namespace Marco.Pms.CacheHelper } public async Task> GetPermissionsFromCache(Guid employeeId) { - var filter = Builders.Filter.Eq(e => e.EmployeeId, employeeId.ToString()); + var filter = Builders.Filter.Eq(e => e.Id, employeeId.ToString()); var result = await _collection @@ -95,7 +111,21 @@ namespace Marco.Pms.CacheHelper public async Task ClearAllProjectIdsFromCache(Guid employeeId) { var filter = Builders.Filter - .Eq(e => e.EmployeeId, employeeId.ToString()); + .Eq(e => e.Id, employeeId.ToString()); + + var update = Builders.Update + .Set(e => e.ProjectIds, new List()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + return true; + } + public async Task ClearAllProjectIdsByRoleIdFromCache(Guid roleId) + { + var filter = Builders.Filter.AnyEq(e => e.ApplicationRoleIds, roleId.ToString()); var update = Builders.Update .Set(e => e.ProjectIds, new List()); @@ -110,7 +140,7 @@ namespace Marco.Pms.CacheHelper public async Task RemoveRoleIdFromCache(Guid employeeId, Guid roleId) { var filter = Builders.Filter - .Eq(e => e.EmployeeId, employeeId.ToString()); + .Eq(e => e.Id, employeeId.ToString()); var update = Builders.Update .Pull(e => e.ApplicationRoleIds, roleId.ToString()); @@ -128,7 +158,7 @@ namespace Marco.Pms.CacheHelper public async Task ClearAllPermissionIdsByEmployeeIDFromCache(Guid employeeId) { var filter = Builders.Filter - .Eq(e => e.EmployeeId, employeeId.ToString()); + .Eq(e => e.Id, employeeId.ToString()); var update = Builders.Update .Set(e => e.PermissionIds, new List()); diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index b667694..f60884f 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -11,19 +11,21 @@ namespace Marco.Pms.CacheHelper public class ProjectCache { private readonly ApplicationDbContext _context; - private readonly IMongoDatabase _mongoDB; - //private readonly ILoggingService _logger; + private readonly IMongoCollection _projetCollection; + private readonly IMongoCollection _taskCollection; public ProjectCache(ApplicationDbContext context, IConfiguration configuration) { var connectionString = configuration["MongoDB:ConnectionString"]; _context = context; var mongoUrl = new MongoUrl(connectionString); var client = new MongoClient(mongoUrl); // Your MongoDB connection string - _mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name + var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name + _projetCollection = mongoDB.GetCollection("ProjectDetails"); + _taskCollection = mongoDB.GetCollection("WorkItemDetails"); } public async Task AddProjectDetailsToCache(Project project) { - var projectCollection = _mongoDB.GetCollection("ProjectDetails"); + //_logger.LogInfo("[AddProjectDetails] Initiated for ProjectId: {ProjectId}", project.Id); @@ -145,7 +147,7 @@ namespace Marco.Pms.CacheHelper projectDetails.PlannedWork = totalPlannedWork; projectDetails.CompletedWork = totalCompletedWork; - await projectCollection.InsertOneAsync(projectDetails); + await _projetCollection.InsertOneAsync(projectDetails); //_logger.LogInfo("[AddProjectDetails] Project details inserted in MongoDB for ProjectId: {ProjectId}", project.Id); } public async Task UpdateProjectDetailsOnlyToCache(Project project) @@ -160,8 +162,6 @@ namespace Marco.Pms.CacheHelper //_logger.LogWarning("StatusMaster not found for ProjectStatusId: {StatusId}", project.ProjectStatusId); } - var projectCollection = _mongoDB.GetCollection("ProjectDetails"); - // Build the update definition var updates = Builders.Update.Combine( Builders.Update.Set(r => r.Name, project.Name), @@ -178,7 +178,7 @@ namespace Marco.Pms.CacheHelper ); // Perform the update - var result = await projectCollection.UpdateOneAsync( + var result = await _projetCollection.UpdateOneAsync( filter: r => r.Id == project.Id.ToString(), update: updates ); @@ -194,7 +194,6 @@ namespace Marco.Pms.CacheHelper } public async Task GetProjectDetailsFromCache(Guid projectId) { - var projectCollection = _mongoDB.GetCollection("ProjectDetails"); // Build filter and projection to exclude large 'Buildings' list var filter = Builders.Filter.Eq(p => p.Id, projectId.ToString()); @@ -203,7 +202,7 @@ namespace Marco.Pms.CacheHelper //_logger.LogInfo("Fetching project details for ProjectId: {ProjectId} from MongoDB", projectId); // Perform query - var project = await projectCollection + var project = await _projetCollection .Find(filter) .Project(projection) .FirstOrDefaultAsync(); @@ -214,16 +213,23 @@ namespace Marco.Pms.CacheHelper return null; } - //// Deserialize the result manually - //var project = BsonSerializer.Deserialize(result); - //_logger.LogInfo("Successfully fetched project details (excluding Buildings) for ProjectId: {ProjectId}", projectId); return project; } + public async Task?> GetProjectDetailsListFromCache(List projectIds) + { + List stringProjectIds = projectIds.Select(p => p.ToString()).ToList(); + var filter = Builders.Filter.In(p => p.Id, stringProjectIds); + var projection = Builders.Projection.Exclude(p => p.Buildings); + var projects = await _projetCollection + .Find(filter) + .Project(projection) + .ToListAsync(); + return projects; + } public async Task AddBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId) { var stringProjectId = projectId.ToString(); - var projectCollection = _mongoDB.GetCollection("ProjectDetails"); // Add Building if (building != null) @@ -241,7 +247,7 @@ namespace Marco.Pms.CacheHelper var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); var update = Builders.Update.Push("Buildings", buildingMongo); - var result = await projectCollection.UpdateOneAsync(filter, update); + var result = await _projetCollection.UpdateOneAsync(filter, update); if (result.MatchedCount == 0) { @@ -271,7 +277,7 @@ namespace Marco.Pms.CacheHelper ); var update = Builders.Update.Push("Buildings.$.Floors", floorMongo); - var result = await projectCollection.UpdateOneAsync(filter, update); + var result = await _projetCollection.UpdateOneAsync(filter, update); if (result.MatchedCount == 0) { @@ -305,7 +311,7 @@ namespace Marco.Pms.CacheHelper var update = Builders.Update.Push("Buildings.$[b].Floors.$[f].WorkAreas", workAreaMongo); var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; - var result = await projectCollection.UpdateOneAsync(filter, update, updateOptions); + var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); if (result.MatchedCount == 0) { @@ -323,7 +329,6 @@ namespace Marco.Pms.CacheHelper public async Task UpdateBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId) { var stringProjectId = projectId.ToString(); - var projectCollection = _mongoDB.GetCollection("ProjectDetails"); // Update Building if (building != null) @@ -338,7 +343,7 @@ namespace Marco.Pms.CacheHelper Builders.Update.Set("Buildings.$.Description", building.Description) ); - var result = await projectCollection.UpdateOneAsync(filter, update); + var result = await _projetCollection.UpdateOneAsync(filter, update); if (result.MatchedCount == 0) { @@ -363,7 +368,7 @@ namespace Marco.Pms.CacheHelper var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); - var result = await projectCollection.UpdateOneAsync(filter, update, updateOptions); + var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); if (result.MatchedCount == 0) { @@ -389,7 +394,7 @@ namespace Marco.Pms.CacheHelper var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); - var result = await projectCollection.UpdateOneAsync(filter, update, updateOptions); + var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); if (result.MatchedCount == 0) { @@ -408,13 +413,12 @@ namespace Marco.Pms.CacheHelper } public async Task?> GetBuildingInfraFromCache(Guid projectId) { - var projectCollection = _mongoDB.GetCollection("ProjectDetails"); // Filter by project ID var filter = Builders.Filter.Eq(p => p.Id, projectId.ToString()); // Project only the "Buildings" field from the document - var buildings = await projectCollection + var buildings = await _projetCollection .Find(filter) .Project(p => p.Buildings) .FirstOrDefaultAsync(); @@ -430,5 +434,10 @@ namespace Marco.Pms.CacheHelper return buildings; } + + + // ------------------------------------------------------- WorkItem ------------------------------------------------------- + + } } diff --git a/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs b/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs index f141798..49c514e 100644 --- a/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs @@ -5,7 +5,7 @@ namespace Marco.Pms.Model.MongoDBModels [BsonIgnoreExtraElements] public class EmployeePermissionMongoDB { - public string EmployeeId { get; set; } = string.Empty; + public string Id { get; set; } = string.Empty; // Employee ID public List ApplicationRoleIds { get; set; } = new List(); public List PermissionIds { get; set; } = new List(); public List ProjectIds { get; set; } = new List(); diff --git a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs index 8bf1c9a..8b1612c 100644 --- a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs @@ -2,7 +2,7 @@ { public class ProjectMongoDB { - public string? Id { get; set; } + public string Id { get; set; } = string.Empty; public string? Name { get; set; } public string? ShortName { get; set; } public string? ProjectAddress { get; set; } diff --git a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs index dc7fdb9..71638a3 100644 --- a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs @@ -2,13 +2,14 @@ { public class WorkItemMongoDB { - public string? Id { get; set; } - public string? WorkAreaId { get; set; } + public string Id { get; set; } = string.Empty; + public string WorkAreaId { get; set; } = string.Empty; public ActivityMasterMongoDB? ActivityMaster { get; set; } public WorkCategoryMasterMongoDB? WorkCategoryMaster { get; set; } public string? ParentTaskId { get; set; } - public double PlannedWork { get; set; } - public double CompletedWork { get; set; } + public double PlannedWork { get; set; } = 0; + public double TodaysAssigned { get; set; } = 0; + public double CompletedWork { get; set; } = 0; public string? Description { get; set; } public DateTime TaskDate { get; set; } } diff --git a/Marco.Pms.Services/Controllers/RolesController.cs b/Marco.Pms.Services/Controllers/RolesController.cs index 4c75b3e..a67ecaf 100644 --- a/Marco.Pms.Services/Controllers/RolesController.cs +++ b/Marco.Pms.Services/Controllers/RolesController.cs @@ -292,6 +292,10 @@ namespace MarcoBMS.Services.Controllers _context.RolePermissionMappings.Add(item); modified = true; } + if (item.FeaturePermissionId == Guid.Parse("172fc9b6-755b-4f62-ab26-55c34a330614")) + { + await _cache.ClearAllProjectIdsByRoleId(id); + } } if (modified) await _context.SaveChangesAsync(); diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 75b51b5..6ff9cfe 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -28,7 +28,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while adding project to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while adding project {ProjectId} to Cache : {Error}", project.Id, ex.Message); } } public async Task UpdateProjectDetailsOnly(Project project) @@ -40,7 +40,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while updating project to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while updating project {ProjectId} to Cache: {Error}", project.Id, ex.Message); return false; } } @@ -53,15 +53,23 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while getting project to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while getting project {ProjectId} to Cache: {Error}", ex.Message); + return null; + } + } + public async Task?> GetProjectDetailsList(List projectIds) + { + try + { + var response = await _projectCache.GetProjectDetailsListFromCache(projectIds); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while getting list od project details from to Cache: {Error}", ex.Message); return null; } } - //public async Task?> GetProjectDetailsList(List projectIds) - //{ - // var response = await _projectCache.GetProjectDetailsListFromCache(projectIds); - // return response; - //} public async Task AddBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) { try @@ -70,7 +78,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while adding project infra to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while adding project infra for project {ProjectId} to Cache: {Error}", projectId, ex.Message); } } public async Task UpdateBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) @@ -85,7 +93,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while updating project infra to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while updating project infra for project {ProjectId} to Cache: {Error}", projectId, ex.Message); } } public async Task?> GetBuildingInfra(Guid projectId) @@ -97,7 +105,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while getting project infra Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while getting project infra for project {ProjectId} form Cache: {Error}", projectId, ex.Message); return null; } } @@ -124,7 +132,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while adding projectIds to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while adding projectIds for employee {EmployeeId} to Cache: {Error}", employeeId, ex.Message); return false; } } @@ -141,7 +149,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while getting projectIDs to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while getting projectIds for employee {EmployeeId} from Cache: {Error}", employeeId, ex.Message); return null; } } @@ -158,7 +166,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while getting permissionIds to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while getting permissionIds for employee {EmployeeId} from Cache: {Error}", employeeId, ex.Message); return null; } } @@ -173,10 +181,17 @@ namespace Marco.Pms.Services.Helpers _logger.LogWarning("Error occured while deleting projectIds from Cache for employee {EmployeeId}: {Error}", employeeId, ex.Message); } } - //public async Task ClearAllProjectIdsByRoleId(Guid roleId) - //{ - // await _employeeCache.ClearAllProjectIdsByRoleIdFromCache(roleId); - //} + public async Task ClearAllProjectIdsByRoleId(Guid roleId) + { + try + { + await _employeeCache.ClearAllProjectIdsByRoleIdFromCache(roleId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting projectIds from Cache for Application Role {RoleId}: {Error}", roleId, ex.Message); + } + } public async Task ClearAllPermissionIdsByEmployeeID(Guid employeeId) { try @@ -185,7 +200,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while deleting permissionIds from to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while deleting permissionIds from Cache for employee {EmployeeId}: {Error}", employeeId, ex.Message); } } public async Task ClearAllPermissionIdsByRoleId(Guid roleId) @@ -196,7 +211,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while deleting permissionIds from to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while deleting permissionIds from Cache for Application role {RoleId}: {Error}", roleId, ex.Message); } } public async Task RemoveRoleId(Guid employeeId, Guid roleId) @@ -207,7 +222,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while deleting Application roleIds from to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while deleting Application role {RoleId} from Cache for employee {EmployeeId}: {Error}", roleId, employeeId, ex.Message); } } } diff --git a/Marco.Pms.Services/Helpers/ProjectsHelper.cs b/Marco.Pms.Services/Helpers/ProjectsHelper.cs index 3ccddba..85003ae 100644 --- a/Marco.Pms.Services/Helpers/ProjectsHelper.cs +++ b/Marco.Pms.Services/Helpers/ProjectsHelper.cs @@ -1,6 +1,7 @@ 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 Microsoft.EntityFrameworkCore; @@ -59,7 +60,25 @@ namespace MarcoBMS.Services.Helpers if (projectIds != null) { - projects = await _context.Projects.Where(p => projectIds.Contains(p.Id)).ToListAsync(); + + List projectdetails = await _cache.GetProjectDetailsList(projectIds) ?? new List(); + projects = projectdetails.Select(p => new Project + { + 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 { From cbcc3398c31396533e2f897d98e0a14cc5d5827e Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 7 Jul 2025 17:44:58 +0530 Subject: [PATCH 053/124] Storing workItem in cache and changing planned work and completed work for respective project, building, floor, and workarea --- Marco.Pms.CacheHelper/ProjectCache.cs | 120 ++++++++++++++++++ .../MongoDBModels/ActivityMasterMongoDB.cs | 2 +- .../MongoDBModels/BuildingMongoDB.cs | 2 +- Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs | 2 +- .../MongoDBModels/ProjectMongoDB.cs | 2 +- .../WorkCategoryMasterMongoDB.cs | 2 +- .../MongoDBModels/WorkItemMongoDB.cs | 2 +- .../Controllers/ProjectController.cs | 73 +++++++++-- .../Helpers/CacheUpdateHelper.cs | 65 ++++++++++ .../appsettings.Development.json | 2 +- 10 files changed, 256 insertions(+), 16 deletions(-) diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index f60884f..6f5a3d3 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -1,4 +1,5 @@ using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.Master; using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.Projects; using Microsoft.EntityFrameworkCore; @@ -434,10 +435,129 @@ namespace Marco.Pms.CacheHelper return buildings; } + public async Task UpdatePlannedAndCompleteWorksInBuildingFromCache(Guid workAreaId, double plannedWork, double completedWork) + { + var filter = Builders.Filter.Eq("Buildings.Floors.WorkAreas._id", workAreaId.ToString()); + var project = await _projetCollection.Find(filter).FirstOrDefaultAsync(); + + string? selectedBuildingId = null; + string? selectedFloorId = null; + string? selectedWorkAreaId = null; + + foreach (var building in project.Buildings) + { + foreach (var floor in building.Floors) + { + foreach (var area in floor.WorkAreas) + { + if (area.Id == workAreaId.ToString()) + { + selectedWorkAreaId = area.Id; + selectedFloorId = floor.Id; + selectedBuildingId = building.Id; + } + } + } + } + + var arrayFilters = new List + { + new JsonArrayFilterDefinition("{ 'b._id': '" + selectedBuildingId + "' }"), + new JsonArrayFilterDefinition("{ 'f._id': '" + selectedFloorId + "' }"), + new JsonArrayFilterDefinition("{ 'a._id': '" + selectedWorkAreaId + "' }") + }; + var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; + var update = Builders.Update + .Inc("Buildings.$[b].Floors.$[f].WorkAreas.$[a].PlannedWork", plannedWork) + .Inc("Buildings.$[b].Floors.$[f].WorkAreas.$[a].CompletedWork", completedWork) + .Inc("Buildings.$[b].Floors.$[f].PlannedWork", plannedWork) + .Inc("Buildings.$[b].Floors.$[f].CompletedWork", completedWork) + .Inc("Buildings.$[b].PlannedWork", plannedWork) + .Inc("Buildings.$[b].CompletedWork", completedWork) + .Inc("PlannedWork", plannedWork) + .Inc("CompletedWork", completedWork); + var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); + + } // ------------------------------------------------------- WorkItem ------------------------------------------------------- + public async Task ManageWorkItemDetailsToCache(List workItems) + { + var activityIds = workItems.Select(wi => wi.ActivityId).ToList(); + var workCategoryIds = workItems.Select(wi => wi.WorkCategoryId).ToList(); + // fetching Activity master + var activities = await _context.ActivityMasters.Where(a => activityIds.Contains(a.Id)).ToListAsync() ?? new List(); + // Fetching Work Category + var workCategories = await _context.WorkCategoryMasters.Where(wc => workCategoryIds.Contains(wc.Id)).ToListAsync() ?? new List(); + + foreach (WorkItem workItem in workItems) + { + var activity = activities.FirstOrDefault(a => a.Id == workItem.ActivityId) ?? new ActivityMaster(); + var workCategory = workCategories.FirstOrDefault(a => a.Id == workItem.WorkCategoryId) ?? new WorkCategoryMaster(); + + var filter = Builders.Filter.Eq(p => p.Id, workItem.Id.ToString()); + var updates = Builders.Update.Combine( + Builders.Update.Set(r => r.WorkAreaId, workItem.WorkAreaId.ToString()), + Builders.Update.Set(r => r.ParentTaskId, (workItem.ParentTaskId != null ? workItem.ParentTaskId.ToString() : null)), + Builders.Update.Set(r => r.PlannedWork, workItem.PlannedWork), + Builders.Update.Set(r => r.TodaysAssigned, 0), + Builders.Update.Set(r => r.CompletedWork, workItem.CompletedWork), + Builders.Update.Set(r => r.Description, workItem.Description), + Builders.Update.Set(r => r.TaskDate, workItem.TaskDate), + Builders.Update.Set(r => r.ActivityMaster, new ActivityMasterMongoDB + { + Id = activity.Id.ToString(), + ActivityName = activity.ActivityName, + UnitOfMeasurement = activity.UnitOfMeasurement + }), + Builders.Update.Set(r => r.WorkCategoryMaster, new WorkCategoryMasterMongoDB + { + Id = workCategory.Id.ToString(), + Name = workCategory.Name, + Description = workCategory.Description, + }) + ); + var options = new UpdateOptions { IsUpsert = true }; + var result = await _taskCollection.UpdateOneAsync(filter, updates, options); + } + } + public async Task> GetWorkItemDetailsByWorkAreaFromCache(Guid workAreaId) + { + var filter = Builders.Filter.Eq(p => p.WorkAreaId, workAreaId.ToString()); + + var options = new UpdateOptions { IsUpsert = true }; + var workItems = await _taskCollection + .Find(filter) + .ToListAsync(); + return workItems; + } + public async Task GetWorkItemDetailsByIdFromCache(Guid id) + { + var filter = Builders.Filter.Eq(p => p.Id, id.ToString()); + + var options = new UpdateOptions { IsUpsert = true }; + var workItem = await _taskCollection + .Find(filter) + .FirstOrDefaultAsync(); + return workItem; + } + public async Task UpdatePlannedAndCompleteWorksInWorkItem(Guid id, double plannedWork = 0, double completedWork = 0, double todaysAssigned = 0) + { + var filter = Builders.Filter.Eq(p => p.Id, id.ToString()); + var updates = Builders.Update + .Inc("PlannedWork", plannedWork) + .Inc("CompletedWork", completedWork) + .Inc("TodaysAssigned", todaysAssigned); + + var result = await _taskCollection.UpdateOneAsync(filter, updates); + if (result.ModifiedCount > 0) + { + return true; + } + return false; + } } } diff --git a/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs index 37218b7..cc77d96 100644 --- a/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs @@ -2,7 +2,7 @@ { public class ActivityMasterMongoDB { - public string? Id { get; set; } + public string Id { get; set; } = string.Empty; public string? ActivityName { get; set; } public string? UnitOfMeasurement { get; set; } } diff --git a/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs b/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs index 87ccb8d..64ccbce 100644 --- a/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs @@ -7,7 +7,7 @@ public string? Description { get; set; } public double PlannedWork { get; set; } public double CompletedWork { get; set; } - public List? Floors { get; set; } + public List Floors { get; set; } = new List(); } public class BuildingMongoDBVM { diff --git a/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs b/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs index ae3975f..57257a4 100644 --- a/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs @@ -6,7 +6,7 @@ public string? FloorName { get; set; } public double PlannedWork { get; set; } public double CompletedWork { get; set; } - public List? WorkAreas { get; set; } + public List WorkAreas { get; set; } = new List(); } public class FloorMongoDBVM diff --git a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs index 8b1612c..7f3a557 100644 --- a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs @@ -7,7 +7,7 @@ public string? ShortName { get; set; } public string? ProjectAddress { get; set; } public string? ContactPerson { get; set; } - public List? Buildings { get; set; } + public List Buildings { get; set; } = new List(); public DateTime? StartDate { get; set; } public DateTime? EndDate { get; set; } public StatusMasterMongoDB? ProjectStatus { get; set; } diff --git a/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs index aef0ada..4ea4682 100644 --- a/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs @@ -2,7 +2,7 @@ { public class WorkCategoryMasterMongoDB { - public string? Id { get; set; } + public string Id { get; set; } = string.Empty; public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; } diff --git a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs index 71638a3..850300d 100644 --- a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs @@ -6,7 +6,7 @@ public string WorkAreaId { get; set; } = string.Empty; public ActivityMasterMongoDB? ActivityMaster { get; set; } public WorkCategoryMasterMongoDB? WorkCategoryMaster { get; set; } - public string? ParentTaskId { get; set; } + public string? ParentTaskId { get; set; } = null; public double PlannedWork { get; set; } = 0; public double TodaysAssigned { get; set; } = 0; public double CompletedWork { get; set; } = 0; diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index a440c21..3ae76ed 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -734,16 +734,45 @@ namespace MarcoBMS.Services.Controllers } // Step 4: Fetch WorkItems with related Activity and Work Category data - var workItems = await _context.WorkItems - .Include(wi => wi.ActivityMaster) - .Include(wi => wi.WorkCategoryMaster) - .Where(wi => wi.WorkAreaId == workAreaId) - .ToListAsync(); + var workItemVMs = await _cache.GetWorkItemDetailsByWorkArea(workAreaId); + if (workItemVMs == null) + { + var workItems = await _context.WorkItems + .Include(wi => wi.ActivityMaster) + .Include(wi => wi.WorkCategoryMaster) + .Where(wi => wi.WorkAreaId == workAreaId) + .ToListAsync(); - _logger.LogInfo("{Count} work items fetched successfully for WorkAreaId: {WorkAreaId}", workItems.Count, workAreaId); + workItemVMs = workItems.Select(wi => new WorkItemMongoDB + { + Id = wi.Id.ToString(), + WorkAreaId = wi.WorkAreaId.ToString(), + ParentTaskId = wi.ParentTaskId.ToString(), + ActivityMaster = new ActivityMasterMongoDB + { + Id = wi.ActivityId.ToString(), + ActivityName = wi.ActivityMaster != null ? wi.ActivityMaster.ActivityName : null, + UnitOfMeasurement = wi.ActivityMaster != null ? wi.ActivityMaster.UnitOfMeasurement : null + }, + WorkCategoryMaster = new WorkCategoryMasterMongoDB + { + Id = wi.ActivityId.ToString(), + Name = wi.WorkCategoryMaster != null ? wi.WorkCategoryMaster.Name : "", + Description = wi.WorkCategoryMaster != null ? wi.WorkCategoryMaster.Description : "" + }, + PlannedWork = wi.PlannedWork, + CompletedWork = wi.CompletedWork, + Description = wi.Description, + TaskDate = wi.TaskDate, + }).ToList(); + + await _cache.ManageWorkItemDetails(workItems); + } + + _logger.LogInfo("{Count} work items fetched successfully for WorkAreaId: {WorkAreaId}", workItemVMs.Count, workAreaId); // Step 5: Return result - return Ok(ApiResponse.SuccessResponse(workItems, $"{workItems.Count} records of tasks fetched successfully", 200)); + return Ok(ApiResponse.SuccessResponse(workItemVMs, $"{workItemVMs.Count} records of tasks fetched successfully", 200)); } [HttpPost("task")] @@ -765,6 +794,8 @@ namespace MarcoBMS.Services.Controllers var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); string message = ""; List projectIds = new List(); + var workItemIds = workItemDtos.Where(wi => wi.Id != null && wi.Id != Guid.Empty).Select(wi => wi.Id).ToList(); + var workItems = await _context.WorkItems.AsNoTracking().Where(wi => workItemIds.Contains(wi.Id)).ToListAsync(); foreach (var itemDto in workItemDtos) { @@ -778,6 +809,28 @@ namespace MarcoBMS.Services.Controllers // Update existing workItemsToUpdate.Add(workItem); message = $"Task Updated in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}"; + var existingWorkItem = workItems.FirstOrDefault(wi => wi.Id == workItem.Id); + double plannedWork = 0; + double completedWork = 0; + if (existingWorkItem != null) + { + if (existingWorkItem.PlannedWork != workItem.PlannedWork && existingWorkItem.CompletedWork != workItem.CompletedWork) + { + plannedWork = workItem.PlannedWork - existingWorkItem.PlannedWork; + completedWork = workItem.CompletedWork - existingWorkItem.CompletedWork; + } + else if (existingWorkItem.PlannedWork == workItem.PlannedWork && existingWorkItem.CompletedWork != workItem.CompletedWork) + { + plannedWork = 0; + completedWork = workItem.CompletedWork - existingWorkItem.CompletedWork; + } + else if (existingWorkItem.PlannedWork != workItem.PlannedWork && existingWorkItem.CompletedWork == workItem.CompletedWork) + { + plannedWork = workItem.PlannedWork - existingWorkItem.PlannedWork; + completedWork = 0; + } + await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, plannedWork, completedWork); + } } else { @@ -785,6 +838,7 @@ namespace MarcoBMS.Services.Controllers workItem.Id = Guid.NewGuid(); workItemsToCreate.Add(workItem); message = $"Task Added in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}"; + await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, workItem.PlannedWork, workItem.CompletedWork); } responseList.Add(new WorkItemVM @@ -793,6 +847,7 @@ namespace MarcoBMS.Services.Controllers WorkItem = workItem }); projectIds.Add(building.ProjectId); + } string responseMessage = ""; // Apply DB changes @@ -801,7 +856,7 @@ namespace MarcoBMS.Services.Controllers _logger.LogInfo("Adding {Count} new work items", workItemsToCreate.Count); await _context.WorkItems.AddRangeAsync(workItemsToCreate); responseMessage = "Task Added Successfully"; - + await _cache.ManageWorkItemDetails(workItemsToCreate); } if (workItemsToUpdate.Any()) @@ -809,7 +864,7 @@ namespace MarcoBMS.Services.Controllers _logger.LogInfo("Updating {Count} existing work items", workItemsToUpdate.Count); _context.WorkItems.UpdateRange(workItemsToUpdate); responseMessage = "Task Updated Successfully"; - + await _cache.ManageWorkItemDetails(workItemsToUpdate); } await _context.SaveChangesAsync(); diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 6ff9cfe..ecce8ab 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -109,6 +109,71 @@ namespace Marco.Pms.Services.Helpers return null; } } + public async Task UpdatePlannedAndCompleteWorksInBuilding(Guid workAreaId, double plannedWork = 0, double completedWork = 0) + { + try + { + await _projectCache.UpdatePlannedAndCompleteWorksInBuildingFromCache(workAreaId, plannedWork, completedWork); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while updating planned work and completed work in building infra form Cache: {Error}", ex.Message); + } + } + + // ------------------------------------------------------- WorkItem ------------------------------------------------------- + + public async Task ManageWorkItemDetails(List workItems) + { + try + { + await _projectCache.ManageWorkItemDetailsToCache(workItems); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while saving workItems form Cache: {Error}", ex.Message); + } + } + public async Task?> GetWorkItemDetailsByWorkArea(Guid workAreaId) + { + try + { + var workItems = await _projectCache.GetWorkItemDetailsByWorkAreaFromCache(workAreaId); + if (workItems.Count > 0) + { + return workItems; + } + else + { + return null; + } + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while fetching list of workItems form Cache: {Error}", ex.Message); + return null; + } + } + public async Task GetWorkItemDetailsById(Guid id) + { + try + { + var workItem = await _projectCache.GetWorkItemDetailsByIdFromCache(id); + if (workItem.Id != "") + { + return workItem; + } + else + { + return null; + } + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while fetching list of workItems form Cache: {Error}", ex.Message); + return null; + } + } // ------------------------------------ Employee Profile Cache --------------------------------------- diff --git a/Marco.Pms.Services/appsettings.Development.json b/Marco.Pms.Services/appsettings.Development.json index 5f5e19d..030c450 100644 --- a/Marco.Pms.Services/appsettings.Development.json +++ b/Marco.Pms.Services/appsettings.Development.json @@ -48,6 +48,6 @@ }, "MongoDB": { "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs", - "ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches" + "ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches?socketTimeoutMS=500&serverSelectionTimeoutMS=500&connectTimeoutMS=500" } } From 364616359336fbda224d109b8373ab05256ca30d Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 8 Jul 2025 12:20:54 +0530 Subject: [PATCH 054/124] Implemented the cache in task allocation --- Marco.Pms.CacheHelper/ProjectCache.cs | 4 +--- Marco.Pms.Services/Controllers/ProjectController.cs | 4 ++-- Marco.Pms.Services/Controllers/TaskController.cs | 10 +++++++++- Marco.Pms.Services/Helpers/CacheUpdateHelper.cs | 11 +++++++++++ 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index 6f5a3d3..23df64c 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -26,8 +26,6 @@ namespace Marco.Pms.CacheHelper } public async Task AddProjectDetailsToCache(Project project) { - - //_logger.LogInfo("[AddProjectDetails] Initiated for ProjectId: {ProjectId}", project.Id); var projectDetails = new ProjectMongoDB @@ -544,7 +542,7 @@ namespace Marco.Pms.CacheHelper .FirstOrDefaultAsync(); return workItem; } - public async Task UpdatePlannedAndCompleteWorksInWorkItem(Guid id, double plannedWork = 0, double completedWork = 0, double todaysAssigned = 0) + public async Task UpdatePlannedAndCompleteWorksInWorkItemToCache(Guid id, double plannedWork, double completedWork, double todaysAssigned) { var filter = Builders.Filter.Eq(p => p.Id, id.ToString()); var updates = Builders.Update diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 3ae76ed..e12d2ad 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -29,7 +29,7 @@ namespace MarcoBMS.Services.Controllers private readonly ApplicationDbContext _context; private readonly UserHelper _userHelper; private readonly ILoggingService _logger; - private readonly RolesHelper _rolesHelper; + //private readonly RolesHelper _rolesHelper; private readonly ProjectsHelper _projectsHelper; private readonly IHubContext _signalR; private readonly PermissionServices _permission; @@ -47,7 +47,7 @@ namespace MarcoBMS.Services.Controllers _context = context; _userHelper = userHelper; _logger = logger; - _rolesHelper = rolesHelper; + //_rolesHelper = rolesHelper; _projectsHelper = projectHelper; _signalR = signalR; _cache = cache; diff --git a/Marco.Pms.Services/Controllers/TaskController.cs b/Marco.Pms.Services/Controllers/TaskController.cs index ca24f1a..40d31f8 100644 --- a/Marco.Pms.Services/Controllers/TaskController.cs +++ b/Marco.Pms.Services/Controllers/TaskController.cs @@ -6,6 +6,7 @@ using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Activities; +using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Service; using MarcoBMS.Services.Helpers; @@ -33,7 +34,7 @@ namespace MarcoBMS.Services.Controllers private readonly PermissionServices _permissionServices; public TaskController(ApplicationDbContext context, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permissionServices, - IHubContext signalR) + IHubContext signalR, CacheUpdateHelper cache) { _context = context; _userHelper = userHelper; @@ -82,6 +83,8 @@ namespace MarcoBMS.Services.Controllers _context.TaskAllocations.Add(taskAllocation); await _context.SaveChangesAsync(); + await _cache.UpdatePlannedAndCompleteWorksInWorkItem(taskAllocation.WorkItemId, todaysAssigned: taskAllocation.PlannedTask); + _logger.LogInfo("Task {TaskId} assigned by Employee {EmployeeId}", taskAllocation.Id, employee.Id); var response = taskAllocation.ToAssignTaskVMFromTaskAllocation(); @@ -255,6 +258,10 @@ namespace MarcoBMS.Services.Controllers } await _context.SaveChangesAsync(); + var selectedWorkAreaId = taskAllocation.WorkItem?.WorkAreaId ?? Guid.Empty; + + await _cache.UpdatePlannedAndCompleteWorksInWorkItem(taskAllocation.WorkItemId, completedWork: taskAllocation.CompletedTask); + await _cache.UpdatePlannedAndCompleteWorksInBuilding(selectedWorkAreaId, completedWork: taskAllocation.CompletedTask); var response = taskAllocation.ToReportTaskVMFromTaskAllocation(); var comments = await _context.TaskComments @@ -675,6 +682,7 @@ namespace MarcoBMS.Services.Controllers /// /// DTO containing task approval details. /// IActionResult indicating success or failure. + [HttpPost("approve")] public async Task ApproveTask(ApproveTaskDto approveTask) { diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index ecce8ab..03fd397 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -174,6 +174,17 @@ namespace Marco.Pms.Services.Helpers return null; } } + public async Task UpdatePlannedAndCompleteWorksInWorkItem(Guid id, double plannedWork = 0, double completedWork = 0, double todaysAssigned = 0) + { + try + { + var response = await _projectCache.UpdatePlannedAndCompleteWorksInWorkItemToCache(id, plannedWork, completedWork, todaysAssigned); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while updating planned work, completed work, and today's assigned work in workItems in Cache: {Error}", ex.Message); + } + } // ------------------------------------ Employee Profile Cache --------------------------------------- From 0be200e77aa2bd3efd06d510ab2eef929a93e6d9 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 8 Jul 2025 12:48:13 +0530 Subject: [PATCH 055/124] In Project Report Email only sending data of job role assigned to that project --- Marco.Pms.Services/Controllers/ReportController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Marco.Pms.Services/Controllers/ReportController.cs b/Marco.Pms.Services/Controllers/ReportController.cs index 893c16b..8f8a790 100644 --- a/Marco.Pms.Services/Controllers/ReportController.cs +++ b/Marco.Pms.Services/Controllers/ReportController.cs @@ -232,9 +232,9 @@ namespace Marco.Pms.Services.Controllers double totalPlannedTask = todayAssignedTasks.Sum(t => t.PlannedTask); double totalCompletedTask = todayAssignedTasks.Sum(t => t.CompletedTask); - + var jobRoleIds = projectAllocations.Select(pa => pa.JobRoleId).ToList(); var jobRoles = await _context.JobRoles - .Where(j => j.TenantId == project.TenantId) + .Where(j => j.TenantId == project.TenantId && jobRoleIds.Contains(j.Id)) .ToListAsync(); // Team on site From 3ec4bd762f574e37c02dbcba154c595bf92656da Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 10 Jul 2025 09:52:18 +0530 Subject: [PATCH 056/124] Added old project Details API --- .../ViewModels/Projects/OldProjectVM.cs | 10 ++ .../Controllers/ProjectController.cs | 134 ++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 Marco.Pms.Model/ViewModels/Projects/OldProjectVM.cs diff --git a/Marco.Pms.Model/ViewModels/Projects/OldProjectVM.cs b/Marco.Pms.Model/ViewModels/Projects/OldProjectVM.cs new file mode 100644 index 0000000..cb38dfc --- /dev/null +++ b/Marco.Pms.Model/ViewModels/Projects/OldProjectVM.cs @@ -0,0 +1,10 @@ +using Marco.Pms.Model.Dtos.Project; + +namespace Marco.Pms.Model.ViewModels.Projects +{ + public class OldProjectVM : ProjectDto + { + public List? Buildings { get; set; } + + } +} diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index e12d2ad..fbc9bf6 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -1,6 +1,8 @@ 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; @@ -288,6 +290,138 @@ namespace MarcoBMS.Services.Controllers }; } + [HttpGet("details-old/{id}")] + public async Task DetailsOld([FromRoute] Guid id) + { + // ProjectDetailsVM vm = new ProjectDetailsVM(); + + if (!ModelState.IsValid) + { + var errors = ModelState.Values + .SelectMany(v => v.Errors) + .Select(e => e.ErrorMessage) + .ToList(); + return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); + + } + + var project = await _context.Projects.Where(c => c.TenantId == _userHelper.GetTenantId() && c.Id == id).Include(c => c.ProjectStatus).SingleOrDefaultAsync(); // includeProperties: "ProjectStatus,Tenant"); //_context.Stock.FindAsync(id); + + if (project == null) + { + return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); + + } + else + { + //var project = projects.Where(c => c.Id == id).SingleOrDefault(); + ProjectDetailsVM vm = await GetProjectViewModel(id, project); + + OldProjectVM projectVM = new OldProjectVM(); + if (vm.project != null) + { + projectVM.Id = vm.project.Id; + projectVM.Name = vm.project.Name; + projectVM.ShortName = vm.project.ShortName; + projectVM.ProjectAddress = vm.project.ProjectAddress; + projectVM.ContactPerson = vm.project.ContactPerson; + projectVM.StartDate = vm.project.StartDate; + projectVM.EndDate = vm.project.EndDate; + projectVM.ProjectStatusId = vm.project.ProjectStatusId; + } + projectVM.Buildings = new List(); + if (vm.buildings != null) + { + foreach (Building build in vm.buildings) + { + BuildingVM buildVM = new BuildingVM() { Id = build.Id, Description = build.Description, Name = build.Name }; + buildVM.Floors = new List(); + if (vm.floors != null) + { + foreach (Floor floorDto in vm.floors.Where(c => c.BuildingId == build.Id).ToList()) + { + FloorsVM floorVM = new FloorsVM() { FloorName = floorDto.FloorName, Id = floorDto.Id }; + floorVM.WorkAreas = new List(); + + if (vm.workAreas != null) + { + foreach (WorkArea workAreaDto in vm.workAreas.Where(c => c.FloorId == floorVM.Id).ToList()) + { + WorkAreaVM workAreaVM = new WorkAreaVM() { Id = workAreaDto.Id, AreaName = workAreaDto.AreaName, WorkItems = new List() }; + + if (vm.workItems != null) + { + foreach (WorkItem workItemDto in vm.workItems.Where(c => c.WorkAreaId == workAreaDto.Id).ToList()) + { + WorkItemVM workItemVM = new WorkItemVM() { WorkItemId = workItemDto.Id, WorkItem = workItemDto }; + + workItemVM.WorkItem.WorkArea = new WorkArea(); + + if (workItemVM.WorkItem.ActivityMaster != null) + { + workItemVM.WorkItem.ActivityMaster.Tenant = new Tenant(); + } + workItemVM.WorkItem.Tenant = new Tenant(); + + double todaysAssigned = 0; + if (vm.Tasks != null) + { + var tasks = vm.Tasks.Where(t => t.WorkItemId == workItemDto.Id).ToList(); + foreach (TaskAllocation task in tasks) + { + todaysAssigned += task.PlannedTask; + } + } + workItemVM.TodaysAssigned = todaysAssigned; + + workAreaVM.WorkItems.Add(workItemVM); + } + } + + floorVM.WorkAreas.Add(workAreaVM); + } + } + + buildVM.Floors.Add(floorVM); + } + } + projectVM.Buildings.Add(buildVM); + } + } + return Ok(ApiResponse.SuccessResponse(projectVM, "Success.", 200)); + } + + + } + + private async Task GetProjectViewModel(Guid? id, Project project) + { + ProjectDetailsVM vm = new ProjectDetailsVM(); + + // List buildings = _unitOfWork.Building.GetAll(c => c.ProjectId == id).ToList(); + List buildings = await _context.Buildings.Where(c => c.ProjectId == id).ToListAsync(); + List idList = buildings.Select(o => o.Id).ToList(); + // List floors = _unitOfWork.Floor.GetAll(c => idList.Contains(c.Id)).ToList(); + List floors = await _context.Floor.Where(c => idList.Contains(c.BuildingId)).ToListAsync(); + idList = floors.Select(o => o.Id).ToList(); + //List workAreas = _unitOfWork.WorkArea.GetAll(c => idList.Contains(c.Id), includeProperties: "WorkItems,WorkItems.ActivityMaster").ToList(); + + List workAreas = await _context.WorkAreas.Where(c => idList.Contains(c.FloorId)).ToListAsync(); + + idList = workAreas.Select(o => o.Id).ToList(); + List workItems = await _context.WorkItems.Include(c => c.WorkCategoryMaster).Where(c => idList.Contains(c.WorkAreaId)).Include(c => c.ActivityMaster).ToListAsync(); + // List workItems = _unitOfWork.WorkItem.GetAll(c => idList.Contains(c.WorkAreaId), includeProperties: "ActivityMaster").ToList(); + idList = workItems.Select(t => t.Id).ToList(); + List tasks = await _context.TaskAllocations.Where(t => idList.Contains(t.WorkItemId) && t.AssignmentDate.Date == DateTime.UtcNow.Date).ToListAsync(); + vm.project = project; + vm.buildings = buildings; + vm.floors = floors; + vm.workAreas = workAreas; + vm.workItems = workItems; + vm.Tasks = tasks; + return vm; + } + private Guid GetTenantId() { return _userHelper.GetTenantId(); From 3e316ef388afe1254933ad98260780be85d4ba20 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 9 Jul 2025 10:35:35 +0530 Subject: [PATCH 057/124] Changed the signalR keyword for work item --- Marco.Pms.Services/Controllers/ProjectController.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index fbc9bf6..8453db2 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -927,7 +927,7 @@ namespace MarcoBMS.Services.Controllers var responseList = new List(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); string message = ""; - List projectIds = new List(); + List workAreaIds = new List(); var workItemIds = workItemDtos.Where(wi => wi.Id != null && wi.Id != Guid.Empty).Select(wi => wi.Id).ToList(); var workItems = await _context.WorkItems.AsNoTracking().Where(wi => workItemIds.Contains(wi.Id)).ToListAsync(); @@ -980,7 +980,7 @@ namespace MarcoBMS.Services.Controllers WorkItemId = workItem.Id, WorkItem = workItem }); - projectIds.Add(building.ProjectId); + workAreaIds.Add(workItem.WorkAreaId); } string responseMessage = ""; @@ -1007,7 +1007,7 @@ namespace MarcoBMS.Services.Controllers - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = message }; + var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = message }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); @@ -1019,7 +1019,7 @@ namespace MarcoBMS.Services.Controllers { Guid tenantId = _userHelper.GetTenantId(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - List projectIds = new List(); + List workAreaIds = new List(); WorkItem? task = await _context.WorkItems.AsNoTracking().Include(t => t.WorkArea).FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId); if (task != null) { @@ -1036,9 +1036,9 @@ namespace MarcoBMS.Services.Controllers var floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == floorId); - projectIds.Add(floor?.Building?.ProjectId ?? Guid.Empty); + workAreaIds.Add(task.WorkAreaId); - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = $"Task Deleted in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}, in Area: {task.WorkArea?.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}" }; + var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = $"Task Deleted in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}, in Area: {task.WorkArea?.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}" }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); } else From 669500e57ec5fe35f10a653e331b51df976bfda7 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 9 Jul 2025 12:39:27 +0530 Subject: [PATCH 058/124] Added the caching project report API and added expiry in workItems in cache --- Marco.Pms.CacheHelper/ProjectCache.cs | 72 ++++- .../MongoDBModels/BuildingMongoDB.cs | 6 +- Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs | 4 + .../MongoDBModels/WorkAreaInfoMongoDB.cs | 13 + .../MongoDBModels/WorkAreaMongoDB.cs | 1 + .../MongoDBModels/WorkItemMongoDB.cs | 1 + .../Controllers/ProjectController.cs | 4 + .../Controllers/ReportController.cs | 161 +--------- Marco.Pms.Services/Dockerfile | 2 +- .../Helpers/CacheUpdateHelper.cs | 30 ++ Marco.Pms.Services/Helpers/ReportHelper.cs | 274 ++++++++++++++++++ Marco.Pms.Services/Program.cs | 1 + 12 files changed, 411 insertions(+), 158 deletions(-) create mode 100644 Marco.Pms.Model/MongoDBModels/WorkAreaInfoMongoDB.cs create mode 100644 Marco.Pms.Services/Helpers/ReportHelper.cs diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index 23df64c..9b2036d 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -106,6 +106,7 @@ namespace Marco.Pms.CacheHelper workAreaMongoList.Add(new WorkAreaMongoDB { Id = wa.Id.ToString(), + FloorId = wa.FloorId.ToString(), AreaName = wa.AreaName, PlannedWork = waPlanned, CompletedWork = waCompleted @@ -118,6 +119,7 @@ namespace Marco.Pms.CacheHelper floorMongoList.Add(new FloorMongoDB { Id = floor.Id.ToString(), + BuildingId = floor.BuildingId.ToString(), FloorName = floor.FloorName, PlannedWork = floorPlanned, CompletedWork = floorCompleted, @@ -131,6 +133,7 @@ namespace Marco.Pms.CacheHelper buildingMongoList.Add(new BuildingMongoDB { Id = building.Id.ToString(), + ProjectId = building.ProjectId.ToString(), BuildingName = building.Name, Description = building.Description, PlannedWork = buildingPlanned, @@ -477,7 +480,59 @@ namespace Marco.Pms.CacheHelper var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); } + public async Task GetBuildingAndFloorByWorkAreaIdFromCache(Guid workAreaId) + { + var pipeline = new[] + { + new BsonDocument("$unwind", "$Buildings"), + new BsonDocument("$unwind", "$Buildings.Floors"), + new BsonDocument("$unwind", "$Buildings.Floors.WorkAreas"), + new BsonDocument("$match", new BsonDocument("Buildings.Floors.WorkAreas._id", workAreaId.ToString())), + new BsonDocument("$project", new BsonDocument + { + { "_id", 0 }, + { "ProjectId", "$_id" }, + { "ProjectName", "$Name" }, + { "PlannedWork", "$PlannedWork" }, + { "CompletedWork", "$CompletedWork" }, + { + "Building", new BsonDocument + { + { "_id", "$Buildings._id" }, + { "BuildingName", "$Buildings.BuildingName" }, + { "Description", "$Buildings.Description" }, + { "PlannedWork", "$Buildings.PlannedWork" }, + { "CompletedWork", "$Buildings.CompletedWork" } + } + }, + { + "Floor", new BsonDocument + { + { "_id", "$Buildings.Floors._id" }, + { "FloorName", "$Buildings.Floors.FloorName" }, + { "PlannedWork", "$Buildings.Floors.PlannedWork" }, + { "CompletedWork", "$Buildings.Floors.CompletedWork" } + } + }, + { "WorkArea", "$Buildings.Floors.WorkAreas" } + }) + }; + var result = await _projetCollection.Aggregate(pipeline).FirstOrDefaultAsync(); + if (result == null) + return null; + return result; + } + public async Task> GetWorkItemsByWorkAreaIdsFromCache(List workAreaIds) + { + var stringWorkAreaIds = workAreaIds.Select(wa => wa.ToString()).ToList(); + var filter = Builders.Filter.In(w => w.WorkAreaId, stringWorkAreaIds); + var workItems = await _taskCollection // replace with your actual collection name + .Find(filter) + .ToListAsync(); + + return workItems; + } // ------------------------------------------------------- WorkItem ------------------------------------------------------- @@ -485,12 +540,14 @@ namespace Marco.Pms.CacheHelper { var activityIds = workItems.Select(wi => wi.ActivityId).ToList(); var workCategoryIds = workItems.Select(wi => wi.WorkCategoryId).ToList(); + var workItemIds = workItems.Select(wi => wi.Id).ToList(); // fetching Activity master var activities = await _context.ActivityMasters.Where(a => activityIds.Contains(a.Id)).ToListAsync() ?? new List(); // Fetching Work Category var workCategories = await _context.WorkCategoryMasters.Where(wc => workCategoryIds.Contains(wc.Id)).ToListAsync() ?? new List(); - + var task = await _context.TaskAllocations.Where(t => workItemIds.Contains(t.WorkItemId) && t.AssignmentDate == DateTime.UtcNow).ToListAsync(); + var todaysAssign = task.Sum(t => t.PlannedTask); foreach (WorkItem workItem in workItems) { var activity = activities.FirstOrDefault(a => a.Id == workItem.ActivityId) ?? new ActivityMaster(); @@ -501,10 +558,11 @@ namespace Marco.Pms.CacheHelper Builders.Update.Set(r => r.WorkAreaId, workItem.WorkAreaId.ToString()), Builders.Update.Set(r => r.ParentTaskId, (workItem.ParentTaskId != null ? workItem.ParentTaskId.ToString() : null)), Builders.Update.Set(r => r.PlannedWork, workItem.PlannedWork), - Builders.Update.Set(r => r.TodaysAssigned, 0), + Builders.Update.Set(r => r.TodaysAssigned, todaysAssign), Builders.Update.Set(r => r.CompletedWork, workItem.CompletedWork), Builders.Update.Set(r => r.Description, workItem.Description), Builders.Update.Set(r => r.TaskDate, workItem.TaskDate), + Builders.Update.Set(r => r.ExpireAt, DateTime.UtcNow.Date.AddDays(1)), Builders.Update.Set(r => r.ActivityMaster, new ActivityMasterMongoDB { Id = activity.Id.ToString(), @@ -520,6 +578,16 @@ namespace Marco.Pms.CacheHelper ); var options = new UpdateOptions { IsUpsert = true }; var result = await _taskCollection.UpdateOneAsync(filter, updates, options); + if (result.UpsertedId != null) + { + var indexKeys = Builders.IndexKeys.Ascending(x => x.ExpireAt); + var indexOptions = new CreateIndexOptions + { + ExpireAfter = TimeSpan.Zero // required for fixed expiration time + }; + var indexModel = new CreateIndexModel(indexKeys, indexOptions); + await _taskCollection.Indexes.CreateOneAsync(indexModel); + } } } public async Task> GetWorkItemDetailsByWorkAreaFromCache(Guid workAreaId) diff --git a/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs b/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs index 64ccbce..786ceb5 100644 --- a/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs @@ -7,12 +7,16 @@ public string? Description { get; set; } public double PlannedWork { get; set; } public double CompletedWork { get; set; } + public string ProjectId { get; set; } = string.Empty; public List Floors { get; set; } = new List(); } public class BuildingMongoDBVM { public string Id { get; set; } = string.Empty; - public string? Name { get; set; } + public string? BuildingName { get; set; } public string? Description { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } + public string ProjectId { get; set; } = string.Empty; } } diff --git a/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs b/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs index 57257a4..15d3060 100644 --- a/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs @@ -3,6 +3,7 @@ public class FloorMongoDB { public string Id { get; set; } = string.Empty; + public string BuildingId { get; set; } = string.Empty; public string? FloorName { get; set; } public double PlannedWork { get; set; } public double CompletedWork { get; set; } @@ -12,6 +13,9 @@ public class FloorMongoDBVM { public string Id { get; set; } = string.Empty; + public string BuildingId { get; set; } = string.Empty; public string? FloorName { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } } } diff --git a/Marco.Pms.Model/MongoDBModels/WorkAreaInfoMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkAreaInfoMongoDB.cs new file mode 100644 index 0000000..da1001b --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/WorkAreaInfoMongoDB.cs @@ -0,0 +1,13 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class WorkAreaInfoMongoDB + { + public string ProjectId { get; set; } = string.Empty; + public string? ProjectName { get; set; } + public BuildingMongoDBVM? Building { get; set; } + public FloorMongoDBVM? Floor { get; set; } + public WorkAreaMongoDB? WorkArea { get; set; } + public double CompletedWork { get; set; } + public double PlannedWork { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs index d17f52c..412c940 100644 --- a/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs @@ -3,6 +3,7 @@ public class WorkAreaMongoDB { public string Id { get; set; } = string.Empty; + public string FloorId { get; set; } = string.Empty; public string? AreaName { get; set; } public double PlannedWork { get; set; } public double CompletedWork { get; set; } diff --git a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs index 850300d..cf798f3 100644 --- a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs @@ -12,5 +12,6 @@ public double CompletedWork { get; set; } = 0; public string? Description { get; set; } public DateTime TaskDate { get; set; } + public DateTime ExpireAt { get; set; } = DateTime.UtcNow.Date.AddDays(1); } } diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 8453db2..fde715f 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -237,6 +237,10 @@ namespace MarcoBMS.Services.Controllers .Include(c => c.ProjectStatus) .FirstOrDefaultAsync(c => c.TenantId == tenantId && c.Id == id); projectVM = GetProjectViewModel(project); + if (project != null) + { + await _cache.AddProjectDetails(project); + } } else { diff --git a/Marco.Pms.Services/Controllers/ReportController.cs b/Marco.Pms.Services/Controllers/ReportController.cs index 8f8a790..11dec58 100644 --- a/Marco.Pms.Services/Controllers/ReportController.cs +++ b/Marco.Pms.Services/Controllers/ReportController.cs @@ -1,12 +1,10 @@ using System.Data; -using System.Globalization; using Marco.Pms.DataAccess.Data; -using Marco.Pms.Model.Dtos.Attendance; using Marco.Pms.Model.Dtos.Mail; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Mail; using Marco.Pms.Model.Utilities; -using Marco.Pms.Model.ViewModels.Report; +using Marco.Pms.Services.Helpers; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; @@ -26,13 +24,15 @@ namespace Marco.Pms.Services.Controllers private readonly ILoggingService _logger; private readonly UserHelper _userHelper; private readonly IWebHostEnvironment _env; - public ReportController(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, UserHelper userHelper, IWebHostEnvironment env) + private readonly ReportHelper _reportHelper; + public ReportController(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, UserHelper userHelper, IWebHostEnvironment env, ReportHelper reportHelper) { _context = context; _emailSender = emailSender; _logger = logger; _userHelper = userHelper; _env = env; + _reportHelper = reportHelper; } [HttpPost("set-mail")] @@ -151,7 +151,6 @@ namespace Marco.Pms.Services.Controllers /// An ApiResponse indicating the success or failure of retrieving statistics and sending the email. private async Task> GetProjectStatistics(Guid projectId, List recipientEmails, string body, string subject, Guid tenantId) { - DateTime reportDate = DateTime.UtcNow.AddDays(-1).Date; if (projectId == Guid.Empty) { @@ -159,161 +158,15 @@ namespace Marco.Pms.Services.Controllers return ApiResponse.ErrorResponse("Provided empty Project ID.", "Provided empty Project ID.", 400); } - var project = await _context.Projects - .AsNoTracking() - .FirstOrDefaultAsync(p => p.Id == projectId); - if (project == null) + var statisticReport = await _reportHelper.GetDailyProjectReport(projectId, tenantId); + + if (statisticReport == null) { _logger.LogWarning("User attempted to fetch project progress for project ID {ProjectId} but not found.", projectId); return ApiResponse.ErrorResponse("Project not found.", "Project not found.", 404); } - var statisticReport = new ProjectStatisticReport - { - Date = reportDate, - ProjectName = project.Name ?? "", - TimeStamp = DateTime.Now.ToString("dd-MMM-yyyy HH:mm:ss", CultureInfo.InvariantCulture) - }; - - // Preload relevant data - var projectAllocations = await _context.ProjectAllocations - .Include(p => p.Employee) - .Where(p => p.ProjectId == project.Id && p.IsActive) - .ToListAsync(); - - var assignedEmployeeIds = projectAllocations.Select(p => p.EmployeeId).ToHashSet(); - - var attendances = await _context.Attendes - .AsNoTracking() - .Where(a => a.ProjectID == project.Id && a.InTime != null && a.InTime.Value.Date == reportDate) - .ToListAsync(); - - var checkedInEmployeeIds = attendances.Select(a => a.EmployeeID).Distinct().ToHashSet(); - var checkoutPendingIds = attendances.Where(a => a.OutTime == null).Select(a => a.EmployeeID).Distinct().ToHashSet(); - var regularizationIds = attendances - .Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE) - .Select(a => a.EmployeeID).Distinct().ToHashSet(); - - // Preload buildings, floors, areas - var buildings = await _context.Buildings.Where(b => b.ProjectId == project.Id).ToListAsync(); - var buildingIds = buildings.Select(b => b.Id).ToList(); - - var floors = await _context.Floor.Where(f => buildingIds.Contains(f.BuildingId)).ToListAsync(); - var floorIds = floors.Select(f => f.Id).ToList(); - - var areas = await _context.WorkAreas.Where(a => floorIds.Contains(a.FloorId)).ToListAsync(); - var areaIds = areas.Select(a => a.Id).ToList(); - - var workItems = await _context.WorkItems - .Include(w => w.ActivityMaster) - .Where(w => areaIds.Contains(w.WorkAreaId)) - .ToListAsync(); - - var itemIds = workItems.Select(i => i.Id).ToList(); - - var tasks = await _context.TaskAllocations - .Where(t => itemIds.Contains(t.WorkItemId)) - .ToListAsync(); - - var taskIds = tasks.Select(t => t.Id).ToList(); - - var taskMembers = await _context.TaskMembers - .Include(m => m.Employee) - .Where(m => taskIds.Contains(m.TaskAllocationId)) - .ToListAsync(); - - // Aggregate data - double totalPlannedWork = workItems.Sum(w => w.PlannedWork); - double totalCompletedWork = workItems.Sum(w => w.CompletedWork); - - var todayAssignedTasks = tasks.Where(t => t.AssignmentDate.Date == reportDate).ToList(); - var reportPending = tasks.Where(t => t.ReportedDate == null).ToList(); - - double totalPlannedTask = todayAssignedTasks.Sum(t => t.PlannedTask); - double totalCompletedTask = todayAssignedTasks.Sum(t => t.CompletedTask); - var jobRoleIds = projectAllocations.Select(pa => pa.JobRoleId).ToList(); - var jobRoles = await _context.JobRoles - .Where(j => j.TenantId == project.TenantId && jobRoleIds.Contains(j.Id)) - .ToListAsync(); - - // Team on site - var teamOnSite = jobRoles - .Select(role => - { - var count = projectAllocations.Count(p => p.JobRoleId == role.Id && checkedInEmployeeIds.Contains(p.EmployeeId)); - return new TeamOnSite { RoleName = role.Name, NumberofEmployees = count }; - }) - .OrderByDescending(t => t.NumberofEmployees) - .ToList(); - - // Task details - var performedTasks = todayAssignedTasks.Select(task => - { - var workItem = workItems.FirstOrDefault(w => w.Id == task.WorkItemId); - var area = areas.FirstOrDefault(a => a.Id == workItem?.WorkAreaId); - var floor = floors.FirstOrDefault(f => f.Id == area?.FloorId); - var building = buildings.FirstOrDefault(b => b.Id == floor?.BuildingId); - - string activityName = workItem?.ActivityMaster?.ActivityName ?? ""; - string location = $"{building?.Name} > {floor?.FloorName}
{floor?.FloorName}-{area?.AreaName}"; - double pending = (workItem?.PlannedWork ?? 0) - (workItem?.CompletedWork ?? 0); - - var taskTeam = taskMembers - .Where(m => m.TaskAllocationId == task.Id) - .Select(m => - { - string name = $"{m.Employee?.FirstName ?? ""} {m.Employee?.LastName ?? ""}"; - var role = jobRoles.FirstOrDefault(r => r.Id == m.Employee?.JobRoleId); - return new TaskTeam { Name = name, RoleName = role?.Name ?? "" }; - }) - .ToList(); - - return new PerformedTask - { - Activity = activityName, - Location = location, - AssignedToday = task.PlannedTask, - CompletedToday = task.CompletedTask, - Pending = pending, - Comment = task.Description, - Team = taskTeam - }; - }).ToList(); - - // Attendance details - var performedAttendance = attendances.Select(att => - { - var alloc = projectAllocations.FirstOrDefault(p => p.EmployeeId == att.EmployeeID); - var role = jobRoles.FirstOrDefault(r => r.Id == alloc?.JobRoleId); - string name = $"{alloc?.Employee?.FirstName ?? ""} {alloc?.Employee?.LastName ?? ""}"; - - return new PerformedAttendance - { - Name = name, - RoleName = role?.Name ?? "", - InTime = att.InTime ?? DateTime.UtcNow, - OutTime = att.OutTime, - Comment = att.Comment - }; - }).ToList(); - - // Fill report - statisticReport.TodaysAttendances = checkedInEmployeeIds.Count; - statisticReport.TotalEmployees = assignedEmployeeIds.Count; - statisticReport.RegularizationPending = regularizationIds.Count; - statisticReport.CheckoutPending = checkoutPendingIds.Count; - statisticReport.TotalPlannedWork = totalPlannedWork; - statisticReport.TotalCompletedWork = totalCompletedWork; - statisticReport.TotalPlannedTask = totalPlannedTask; - statisticReport.TotalCompletedTask = totalCompletedTask; - statisticReport.CompletionStatus = totalPlannedWork > 0 ? totalCompletedWork / totalPlannedWork : 0; - statisticReport.TodaysAssignTasks = todayAssignedTasks.Count; - statisticReport.ReportPending = reportPending.Count; - statisticReport.TeamOnSite = teamOnSite; - statisticReport.PerformedTasks = performedTasks; - statisticReport.PerformedAttendance = performedAttendance; - // Send Email var emailBody = await _emailSender.SendProjectStatisticsEmail(recipientEmails, body, subject, statisticReport); var employee = await _context.Employees.FirstOrDefaultAsync(e => e.Email != null && recipientEmails.Contains(e.Email)) ?? new Employee(); diff --git a/Marco.Pms.Services/Dockerfile b/Marco.Pms.Services/Dockerfile index 77311ee..2aa24ea 100644 --- a/Marco.Pms.Services/Dockerfile +++ b/Marco.Pms.Services/Dockerfile @@ -19,7 +19,7 @@ COPY ["Marco.Pms.Services/Marco.Pms.Services.csproj", "Marco.Pms.Services/"] COPY ["Marco.Pms.DataAccess/Marco.Pms.DataAccess.csproj", "Marco.Pms.DataAccess/"] COPY ["Marco.Pms.Model/Marco.Pms.Model.csproj", "Marco.Pms.Model/"] COPY ["Marco.Pms.Utility/Marco.Pms.Utility.csproj", "Marco.Pms.Utility/"] -COPY ["Marco.Pms.Utility/Marco.Pms.CacheHelper.csproj", "Marco.Pms.CacheHelper/"] +COPY ["Marco.Pms.CacheHelper/Marco.Pms.CacheHelper.csproj", "Marco.Pms.CacheHelper/"] RUN dotnet restore "./Marco.Pms.Services/Marco.Pms.Services.csproj" COPY . . WORKDIR "/src/Marco.Pms.Services" diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 03fd397..216ec6e 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -120,6 +120,36 @@ namespace Marco.Pms.Services.Helpers _logger.LogWarning("Error occured while updating planned work and completed work in building infra form Cache: {Error}", ex.Message); } } + public async Task GetBuildingAndFloorByWorkAreaId(Guid workAreaId) + { + try + { + var response = await _projectCache.GetBuildingAndFloorByWorkAreaIdFromCache(workAreaId); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while fetching workArea Details using its ID form Cache: {Error}", ex.Message); + return null; + } + } + public async Task?> GetWorkItemsByWorkAreaIds(List workAreaIds) + { + try + { + var response = await _projectCache.GetWorkItemsByWorkAreaIdsFromCache(workAreaIds); + if (response.Count > 0) + { + return response; + } + return null; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while fetching workItems list using workArea IDs list form Cache: {Error}", ex.Message); + return null; + } + } // ------------------------------------------------------- WorkItem ------------------------------------------------------- diff --git a/Marco.Pms.Services/Helpers/ReportHelper.cs b/Marco.Pms.Services/Helpers/ReportHelper.cs new file mode 100644 index 0000000..e7632fd --- /dev/null +++ b/Marco.Pms.Services/Helpers/ReportHelper.cs @@ -0,0 +1,274 @@ +using System.Globalization; +using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.Dtos.Attendance; +using Marco.Pms.Model.MongoDBModels; +using Marco.Pms.Model.ViewModels.Report; +using Microsoft.EntityFrameworkCore; + +namespace Marco.Pms.Services.Helpers +{ + public class ReportHelper + { + private readonly ApplicationDbContext _context; + private readonly CacheUpdateHelper _cache; + public ReportHelper(CacheUpdateHelper cache, ApplicationDbContext context) + { + _cache = cache; + _context = context; + } + public async Task GetDailyProjectReport(Guid projectId, Guid tenantId) + { + // await _cache.GetBuildingAndFloorByWorkAreaId(); + DateTime reportDate = DateTime.UtcNow.AddDays(-1).Date; + var project = await _cache.GetProjectDetails(projectId); + if (project == null) + { + var projectSQL = await _context.Projects + .AsNoTracking() + .FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId); + if (projectSQL != null) + { + project = new ProjectMongoDB + { + Id = projectSQL.Id.ToString(), + Name = projectSQL.Name, + ShortName = projectSQL.ShortName, + ProjectAddress = projectSQL.ProjectAddress, + ContactPerson = projectSQL.ContactPerson + }; + await _cache.AddProjectDetails(projectSQL); + } + } + if (project != null) + { + + var statisticReport = new ProjectStatisticReport + { + Date = reportDate, + ProjectName = project.Name ?? "", + TimeStamp = DateTime.Now.ToString("dd-MMM-yyyy HH:mm:ss", CultureInfo.InvariantCulture) + }; + + // Preload relevant data + var projectAllocations = await _context.ProjectAllocations + .Include(p => p.Employee) + .Where(p => p.ProjectId == projectId && p.IsActive) + .ToListAsync(); + + var assignedEmployeeIds = projectAllocations.Select(p => p.EmployeeId).ToHashSet(); + + var attendances = await _context.Attendes + .AsNoTracking() + .Where(a => a.ProjectID == projectId && a.InTime != null && a.InTime.Value.Date == reportDate) + .ToListAsync(); + + var checkedInEmployeeIds = attendances.Select(a => a.EmployeeID).Distinct().ToHashSet(); + var checkoutPendingIds = attendances.Where(a => a.OutTime == null).Select(a => a.EmployeeID).Distinct().ToHashSet(); + var regularizationIds = attendances + .Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE) + .Select(a => a.EmployeeID).Distinct().ToHashSet(); + + // Preload buildings, floors, areas + List? buildings = null; + List? floors = null; + List? areas = null; + List? workItems = null; + + // Fetch Buildings + buildings = project.Buildings + .Select(b => new BuildingMongoDBVM + { + Id = b.Id, + ProjectId = b.ProjectId, + BuildingName = b.BuildingName, + Description = b.Description + }).ToList(); + if (buildings == null) + { + buildings = await _context.Buildings + .Where(b => b.ProjectId == projectId) + .Select(b => new BuildingMongoDBVM + { + Id = b.Id.ToString(), + ProjectId = b.ProjectId.ToString(), + BuildingName = b.Name, + Description = b.Description + }) + .ToListAsync(); + } + + // fetch Floors + floors = project.Buildings + .SelectMany(b => b.Floors.Select(f => new FloorMongoDBVM + { + Id = f.Id.ToString(), + BuildingId = f.BuildingId, + FloorName = f.FloorName + })).ToList(); + if (floors == null) + { + var buildingIds = buildings.Select(b => Guid.Parse(b.Id)).ToList(); + floors = await _context.Floor + .Where(f => buildingIds.Contains(f.BuildingId)) + .Select(f => new FloorMongoDBVM + { + Id = f.Id.ToString(), + BuildingId = f.BuildingId.ToString(), + FloorName = f.FloorName + }) + .ToListAsync(); + } + + // fetch Work Areas + areas = project.Buildings + .SelectMany(b => b.Floors) + .SelectMany(f => f.WorkAreas).ToList(); + if (areas == null) + { + var floorIds = floors.Select(f => Guid.Parse(f.Id)).ToList(); + areas = await _context.WorkAreas + .Where(a => floorIds.Contains(a.FloorId)) + .Select(wa => new WorkAreaMongoDB + { + Id = wa.Id.ToString(), + FloorId = wa.FloorId.ToString(), + AreaName = wa.AreaName, + }) + .ToListAsync(); + } + + var areaIds = areas.Select(a => Guid.Parse(a.Id)).ToList(); + + // fetch Work Items + workItems = await _cache.GetWorkItemsByWorkAreaIds(areaIds); + if (workItems == null) + { + workItems = await _context.WorkItems + .Include(w => w.ActivityMaster) + .Where(w => areaIds.Contains(w.WorkAreaId)) + .Select(wi => new WorkItemMongoDB + { + Id = wi.Id.ToString(), + WorkAreaId = wi.WorkAreaId.ToString(), + PlannedWork = wi.PlannedWork, + CompletedWork = wi.CompletedWork, + Description = wi.Description, + TaskDate = wi.TaskDate, + ActivityMaster = new ActivityMasterMongoDB + { + ActivityName = wi.ActivityMaster != null ? wi.ActivityMaster.ActivityName : null, + UnitOfMeasurement = wi.ActivityMaster != null ? wi.ActivityMaster.UnitOfMeasurement : null + } + }) + .ToListAsync(); + } + + var itemIds = workItems.Select(i => Guid.Parse(i.Id)).ToList(); + + var tasks = await _context.TaskAllocations + .Where(t => itemIds.Contains(t.WorkItemId)) + .ToListAsync(); + + var taskIds = tasks.Select(t => t.Id).ToList(); + + var taskMembers = await _context.TaskMembers + .Include(m => m.Employee) + .Where(m => taskIds.Contains(m.TaskAllocationId)) + .ToListAsync(); + + // Aggregate data + double totalPlannedWork = workItems.Sum(w => w.PlannedWork); + double totalCompletedWork = workItems.Sum(w => w.CompletedWork); + + var todayAssignedTasks = tasks.Where(t => t.AssignmentDate.Date == reportDate).ToList(); + var reportPending = tasks.Where(t => t.ReportedDate == null).ToList(); + + double totalPlannedTask = todayAssignedTasks.Sum(t => t.PlannedTask); + double totalCompletedTask = todayAssignedTasks.Sum(t => t.CompletedTask); + var jobRoleIds = projectAllocations.Select(pa => pa.JobRoleId).ToList(); + var jobRoles = await _context.JobRoles + .Where(j => j.TenantId == tenantId && jobRoleIds.Contains(j.Id)) + .ToListAsync(); + + // Team on site + var teamOnSite = jobRoles + .Select(role => + { + var count = projectAllocations.Count(p => p.JobRoleId == role.Id && checkedInEmployeeIds.Contains(p.EmployeeId)); + return new TeamOnSite { RoleName = role.Name, NumberofEmployees = count }; + }) + .OrderByDescending(t => t.NumberofEmployees) + .ToList(); + + // Task details + var performedTasks = todayAssignedTasks.Select(task => + { + var workItem = workItems.FirstOrDefault(w => w.Id == task.WorkItemId.ToString()); + var area = areas.FirstOrDefault(a => a.Id == workItem?.WorkAreaId); + var floor = floors.FirstOrDefault(f => f.Id == area?.FloorId); + var building = buildings.FirstOrDefault(b => b.Id == floor?.BuildingId); + + string activityName = workItem?.ActivityMaster?.ActivityName ?? ""; + string location = $"{building?.BuildingName} > {floor?.FloorName}
{floor?.FloorName}-{area?.AreaName}"; + double pending = (workItem?.PlannedWork ?? 0) - (workItem?.CompletedWork ?? 0); + + var taskTeam = taskMembers + .Where(m => m.TaskAllocationId == task.Id) + .Select(m => + { + string name = $"{m.Employee?.FirstName ?? ""} {m.Employee?.LastName ?? ""}"; + var role = jobRoles.FirstOrDefault(r => r.Id == m.Employee?.JobRoleId); + return new TaskTeam { Name = name, RoleName = role?.Name ?? "" }; + }) + .ToList(); + + return new PerformedTask + { + Activity = activityName, + Location = location, + AssignedToday = task.PlannedTask, + CompletedToday = task.CompletedTask, + Pending = pending, + Comment = task.Description, + Team = taskTeam + }; + }).ToList(); + + // Attendance details + var performedAttendance = attendances.Select(att => + { + var alloc = projectAllocations.FirstOrDefault(p => p.EmployeeId == att.EmployeeID); + var role = jobRoles.FirstOrDefault(r => r.Id == alloc?.JobRoleId); + string name = $"{alloc?.Employee?.FirstName ?? ""} {alloc?.Employee?.LastName ?? ""}"; + + return new PerformedAttendance + { + Name = name, + RoleName = role?.Name ?? "", + InTime = att.InTime ?? DateTime.UtcNow, + OutTime = att.OutTime, + Comment = att.Comment + }; + }).ToList(); + + // Fill report + statisticReport.TodaysAttendances = checkedInEmployeeIds.Count; + statisticReport.TotalEmployees = assignedEmployeeIds.Count; + statisticReport.RegularizationPending = regularizationIds.Count; + statisticReport.CheckoutPending = checkoutPendingIds.Count; + statisticReport.TotalPlannedWork = totalPlannedWork; + statisticReport.TotalCompletedWork = totalCompletedWork; + statisticReport.TotalPlannedTask = totalPlannedTask; + statisticReport.TotalCompletedTask = totalCompletedTask; + statisticReport.CompletionStatus = totalPlannedWork > 0 ? totalCompletedWork / totalPlannedWork : 0; + statisticReport.TodaysAssignTasks = todayAssignedTasks.Count; + statisticReport.ReportPending = reportPending.Count; + statisticReport.TeamOnSite = teamOnSite; + statisticReport.PerformedTasks = performedTasks; + statisticReport.PerformedAttendance = performedAttendance; + return statisticReport; + } + return null; + } + } +} diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index 1d9b4b3..30831c6 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -137,6 +137,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); From ff722503d508599acd701a36594e9cf3699c0ff7 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 9 Jul 2025 15:11:08 +0530 Subject: [PATCH 059/124] Added new parameter in log "Origin" --- Marco.Pms.Services/Middleware/LoggingMiddleware.cs | 4 +++- Marco.Pms.Services/Service/RefreshTokenService.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Marco.Pms.Services/Middleware/LoggingMiddleware.cs b/Marco.Pms.Services/Middleware/LoggingMiddleware.cs index dd10d7d..c57f05c 100644 --- a/Marco.Pms.Services/Middleware/LoggingMiddleware.cs +++ b/Marco.Pms.Services/Middleware/LoggingMiddleware.cs @@ -24,7 +24,7 @@ namespace MarcoBMS.Services.Middleware var response = context.Response; var request = context.Request; var tenantId = context.User.FindFirst("TenantId")?.Value; - + string origin = request.Headers["Origin"].FirstOrDefault() ?? ""; using (LogContext.PushProperty("TenantId", tenantId)) using (LogContext.PushProperty("TraceId", context.TraceIdentifier)) @@ -33,6 +33,8 @@ namespace MarcoBMS.Services.Middleware using (LogContext.PushProperty("Timestamp", DateTime.UtcNow)) using (LogContext.PushProperty("IpAddress", context.Connection.RemoteIpAddress?.ToString())) using (LogContext.PushProperty("RequestPath", request.Path)) + using (LogContext.PushProperty("Origin", origin)) + try diff --git a/Marco.Pms.Services/Service/RefreshTokenService.cs b/Marco.Pms.Services/Service/RefreshTokenService.cs index 018de68..231e27c 100644 --- a/Marco.Pms.Services/Service/RefreshTokenService.cs +++ b/Marco.Pms.Services/Service/RefreshTokenService.cs @@ -218,7 +218,7 @@ namespace MarcoBMS.Services.Service catch (Exception ex) { // Token is invalid - Console.WriteLine($"Token validation failed: {ex.Message}"); + _logger.LogError($"Token validation failed: {ex.Message}"); return null; } } From 3c8a044d6682b4af8aba6585f6c50f964c7a959d Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 10 Jul 2025 14:59:28 +0530 Subject: [PATCH 060/124] Added the workcategory in WorkItem --- Marco.Pms.Services/Controllers/ProjectController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index fde715f..09858d5 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -894,7 +894,7 @@ namespace MarcoBMS.Services.Controllers }, WorkCategoryMaster = new WorkCategoryMasterMongoDB { - Id = wi.ActivityId.ToString(), + Id = wi.WorkCategoryId.ToString() ?? "", Name = wi.WorkCategoryMaster != null ? wi.WorkCategoryMaster.Name : "", Description = wi.WorkCategoryMaster != null ? wi.WorkCategoryMaster.Description : "" }, From 8e3eedbfa7317ac44729d1960718d033b188d92e Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 10 Jul 2025 15:57:08 +0530 Subject: [PATCH 061/124] Removing the project stored in cache for employee who have the project manage permission --- Marco.Pms.CacheHelper/EmployeeCache.cs | 14 +++++ .../Controllers/ProjectController.cs | 59 ++++++++++++++----- .../Helpers/CacheUpdateHelper.cs | 11 ++++ 3 files changed, 68 insertions(+), 16 deletions(-) diff --git a/Marco.Pms.CacheHelper/EmployeeCache.cs b/Marco.Pms.CacheHelper/EmployeeCache.cs index 5c86e6f..c2a1f7b 100644 --- a/Marco.Pms.CacheHelper/EmployeeCache.cs +++ b/Marco.Pms.CacheHelper/EmployeeCache.cs @@ -137,6 +137,20 @@ namespace Marco.Pms.CacheHelper return true; } + public async Task ClearAllProjectIdsByPermissionIdFromCache(Guid permissionId) + { + var filter = Builders.Filter.AnyEq(e => e.PermissionIds, permissionId.ToString()); + + var update = Builders.Update + .Set(e => e.ProjectIds, new List()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + return true; + } public async Task RemoveRoleIdFromCache(Guid employeeId, Guid roleId) { var filter = Builders.Filter diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 09858d5..07ddbfd 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -36,6 +36,7 @@ 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; @@ -44,7 +45,7 @@ namespace MarcoBMS.Services.Controllers public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, ProjectsHelper projectHelper, - IHubContext signalR, PermissionServices permission, CacheUpdateHelper cache) + IHubContext signalR, PermissionServices permission, CacheUpdateHelper cache, IServiceScopeFactory serviceScopeFactory) { _context = context; _userHelper = userHelper; @@ -59,6 +60,7 @@ namespace MarcoBMS.Services.Controllers ViewInfra = Guid.Parse("8d7cc6e3-9147-41f7-aaa7-fa507e450bd4"); ManageInfra = Guid.Parse("f2aee20a-b754-4537-8166-f9507b44585b"); tenantId = _userHelper.GetTenantId(); + _serviceScopeFactory = serviceScopeFactory; } [HttpGet("list/basic")] @@ -436,31 +438,56 @@ namespace MarcoBMS.Services.Controllers [HttpPost] public async Task Create([FromBody] CreateProjectDto projectDto) { - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + // 1. Validate input first (early exit) if (!ModelState.IsValid) { - var errors = ModelState.Values - .SelectMany(v => v.Errors) - .Select(e => e.ErrorMessage) - .ToList(); + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); - } - Guid TenantId = GetTenantId(); - var project = projectDto.ToProjectFromCreateProjectDto(TenantId); + // 2. Prepare data without I/O + Guid tenantId = _userHelper.GetTenantId(); // Assuming this is fast and from claims + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var loggedInUserId = loggedInEmployee.Id; + var project = projectDto.ToProjectFromCreateProjectDto(tenantId); - _context.Projects.Add(project); + // 3. Store it to database + try + { + _context.Projects.Add(project); + await _context.SaveChangesAsync(); + } + catch (Exception ex) + { + // Log the detailed exception + _logger.LogError("Failed to create project in database. Rolling back transaction. : {Error}", ex.Message); + // Return a server error as the primary operation failed + return StatusCode(500, ApiResponse.ErrorResponse("An error occurred while saving the project.", ex.Message, 500)); + } - await _context.SaveChangesAsync(); + // 4. Perform non-critical side-effects (caching, notifications) concurrently + try + { + // These operations do not depend on each other, so they can run in parallel. + Task cacheAddDetailsTask = _cache.AddProjectDetails(project); + Task cacheClearListTask = _cache.ClearAllProjectIdsByPermissionId(ManageProject); - await _cache.AddProjectDetails(project); + 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) + Task notificationTask = _signalR.Clients.Group(tenantId.ToString()).SendAsync("NotificationEventHandler", notification); - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Create_Project", Response = project.ToProjectDto() }; + // Await all side-effect tasks to complete in parallel + await Task.WhenAll(cacheAddDetailsTask, cacheClearListTask, notificationTask); + } + catch (Exception ex) + { + // The project was created successfully, but a side-effect failed. + // Log this as a warning, as the primary operation succeeded. Don't return an error to the user. + _logger.LogWarning("Project {ProjectId} was created, but a post-creation side-effect (caching/notification) failed. : {Error}", project.Id, ex.Message); + } - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); - - return Ok(ApiResponse.SuccessResponse(project.ToProjectDto(), "Success.", 200)); + // 5. Return a success response to the user as soon as the critical data is saved. + return Ok(ApiResponse.SuccessResponse(project.ToProjectDto(), "Project created successfully.", 200)); } [HttpPut] diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 216ec6e..ae6264e 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -298,6 +298,17 @@ namespace Marco.Pms.Services.Helpers _logger.LogWarning("Error occured while deleting projectIds from Cache for Application Role {RoleId}: {Error}", roleId, ex.Message); } } + public async Task ClearAllProjectIdsByPermissionId(Guid permissionId) + { + try + { + await _employeeCache.ClearAllProjectIdsByPermissionIdFromCache(permissionId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting projectIds from Cache for Permission {PermissionId}: {Error}", permissionId, ex.Message); + } + } public async Task ClearAllPermissionIdsByEmployeeID(Guid employeeId) { try From 5cb56b7a10e04f4cf51992817b0f50f708f2945c Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Fri, 11 Jul 2025 11:06:29 +0530 Subject: [PATCH 062/124] Sovled the rebase code errors --- Marco.Pms.Services/Controllers/TaskController.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Marco.Pms.Services/Controllers/TaskController.cs b/Marco.Pms.Services/Controllers/TaskController.cs index 40d31f8..b764f00 100644 --- a/Marco.Pms.Services/Controllers/TaskController.cs +++ b/Marco.Pms.Services/Controllers/TaskController.cs @@ -31,6 +31,7 @@ namespace MarcoBMS.Services.Controllers private readonly S3UploadService _s3Service; private readonly ILoggingService _logger; private readonly IHubContext _signalR; + private readonly CacheUpdateHelper _cache; private readonly PermissionServices _permissionServices; public TaskController(ApplicationDbContext context, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permissionServices, @@ -41,6 +42,7 @@ namespace MarcoBMS.Services.Controllers _s3Service = s3Service; _logger = logger; _signalR = signalR; + _cache = cache; _permissionServices = permissionServices; } From d27cdee72d5b8d73f36a51bc239056cef3b6ae47 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Sat, 12 Jul 2025 13:13:29 +0530 Subject: [PATCH 063/124] Refactor project report APIs to improve performance and readability --- Marco.Pms.CacheHelper/ReportCache.cs | 45 ++ .../ProjectReportEmailMongoDB.cs | 16 + .../Controllers/ReportController.cs | 530 +++++++++++++++--- Marco.Pms.Services/Service/EmailSender.cs | 26 +- 4 files changed, 523 insertions(+), 94 deletions(-) create mode 100644 Marco.Pms.CacheHelper/ReportCache.cs create mode 100644 Marco.Pms.Model/MongoDBModels/ProjectReportEmailMongoDB.cs diff --git a/Marco.Pms.CacheHelper/ReportCache.cs b/Marco.Pms.CacheHelper/ReportCache.cs new file mode 100644 index 0000000..76009a4 --- /dev/null +++ b/Marco.Pms.CacheHelper/ReportCache.cs @@ -0,0 +1,45 @@ +using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.MongoDBModels; +using Microsoft.Extensions.Configuration; +using MongoDB.Driver; + +namespace Marco.Pms.CacheHelper +{ + public class ReportCache + { + private readonly ApplicationDbContext _context; + private readonly IMongoCollection _projectReportCollection; + public ReportCache(ApplicationDbContext context, IConfiguration configuration) + { + var connectionString = configuration["MongoDB:ConnectionString"]; + _context = context; + var mongoUrl = new MongoUrl(connectionString); + var client = new MongoClient(mongoUrl); // Your MongoDB connection string + var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name + _projectReportCollection = mongoDB.GetCollection("ProjectReportMail"); + } + + /// + /// Retrieves project report emails from the cache based on their sent status. + /// + /// True to get sent reports, false to get unsent reports. + /// A list of ProjectReportEmailMongoDB objects. + public async Task> GetProjectReportMailFromCache(bool isSent) + { + var filter = Builders.Filter.Eq(p => p.IsSent, isSent); + var reports = await _projectReportCollection.Find(filter).ToListAsync(); + return reports; + } + + /// + /// Adds a project report email to the cache. + /// + /// The ProjectReportEmailMongoDB object to add. + /// A Task representing the asynchronous operation. + public async Task AddProjectReportMailToCache(ProjectReportEmailMongoDB report) + { + // Consider adding validation or logging here. + await _projectReportCollection.InsertOneAsync(report); + } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/ProjectReportEmailMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ProjectReportEmailMongoDB.cs new file mode 100644 index 0000000..519ea4f --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/ProjectReportEmailMongoDB.cs @@ -0,0 +1,16 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace Marco.Pms.Model.MongoDBModels +{ + public class ProjectReportEmailMongoDB + { + [BsonId] // Tells MongoDB this is the primary key (_id) + [BsonRepresentation(BsonType.ObjectId)] // Optional: if your _id is ObjectId + public string Id { get; set; } = string.Empty; + public string? Body { get; set; } + public string? Subject { get; set; } + public List? Receivers { get; set; } + public bool IsSent { get; set; } = false; + } +} diff --git a/Marco.Pms.Services/Controllers/ReportController.cs b/Marco.Pms.Services/Controllers/ReportController.cs index 11dec58..717a273 100644 --- a/Marco.Pms.Services/Controllers/ReportController.cs +++ b/Marco.Pms.Services/Controllers/ReportController.cs @@ -1,16 +1,19 @@ -using System.Data; -using Marco.Pms.DataAccess.Data; +using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Dtos.Mail; -using Marco.Pms.Model.Employees; using Marco.Pms.Model.Mail; +using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.Utilities; using Marco.Pms.Services.Helpers; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.CodeAnalysis; using Microsoft.EntityFrameworkCore; using MongoDB.Driver; +using System.Data; +using System.Globalization; +using System.Net.Mail; namespace Marco.Pms.Services.Controllers { @@ -25,7 +28,11 @@ namespace Marco.Pms.Services.Controllers private readonly UserHelper _userHelper; private readonly IWebHostEnvironment _env; private readonly ReportHelper _reportHelper; - public ReportController(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, UserHelper userHelper, IWebHostEnvironment env, ReportHelper reportHelper) + private readonly IConfiguration _configuration; + private readonly CacheUpdateHelper _cache; + private readonly IServiceScopeFactory _serviceScopeFactory; + public ReportController(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, UserHelper userHelper, + IWebHostEnvironment env, ReportHelper reportHelper, IConfiguration configuration, CacheUpdateHelper cache, IServiceScopeFactory serviceScopeFactory) { _context = context; _emailSender = emailSender; @@ -33,27 +40,122 @@ namespace Marco.Pms.Services.Controllers _userHelper = userHelper; _env = env; _reportHelper = reportHelper; + _configuration = configuration; + _cache = cache; + _serviceScopeFactory = serviceScopeFactory; } - [HttpPost("set-mail")] + /// + /// Adds new mail details for a project report. + /// + /// The mail details data. + /// An API response indicating success or failure. + [HttpPost("mail-details")] // More specific route for adding mail details public async Task AddMailDetails([FromBody] MailDetailsDto mailDetailsDto) { + // 1. Get Tenant ID and Basic Authorization Check Guid tenantId = _userHelper.GetTenantId(); - MailDetails mailDetails = new MailDetails + if (tenantId == Guid.Empty) + { + _logger.LogWarning("Authorization Error: Attempt to add mail details with an empty or invalid tenant ID."); + return Unauthorized(ApiResponse.ErrorResponse("Unauthorized", "Tenant ID not found or invalid.", 401)); + } + + // 2. Input Validation (Leverage Model Validation attributes on DTO) + if (mailDetailsDto == null) + { + _logger.LogWarning("Validation Error: MailDetails DTO is null. TenantId: {TenantId}", tenantId); + return BadRequest(ApiResponse.ErrorResponse("Invalid Data", "Request body is empty.", 400)); + } + + // Ensure ProjectId and Recipient are not empty + if (mailDetailsDto.ProjectId == Guid.Empty) + { + _logger.LogWarning("Validation Error: Project ID is empty. TenantId: {TenantId}", tenantId); + return BadRequest(ApiResponse.ErrorResponse("Validation Failed", "Project ID cannot be empty.", 400)); + } + + if (string.IsNullOrWhiteSpace(mailDetailsDto.Recipient)) + { + _logger.LogWarning("Validation Error: Recipient email is empty. ProjectId: {ProjectId}, TenantId: {TenantId}", mailDetailsDto.ProjectId, tenantId); + return BadRequest(ApiResponse.ErrorResponse("Validation Failed", "Recipient email cannot be empty.", 400)); + } + + // Optional: Validate email format using regex or System.Net.Mail.MailAddress + try + { + var mailAddress = new MailAddress(mailDetailsDto.Recipient); + _logger.LogInfo("nothing"); + } + catch (FormatException) + { + _logger.LogWarning("Validation Error: Invalid recipient email format '{Recipient}'. ProjectId: {ProjectId}, TenantId: {TenantId}", mailDetailsDto.Recipient, mailDetailsDto.ProjectId, tenantId); + return BadRequest(ApiResponse.ErrorResponse("Validation Failed", "Invalid recipient email format.", 400)); + } + + // 3. Validate MailListId (Foreign Key Check) + // Ensure the MailListId refers to an existing MailBody (template) within the same tenant. + if (mailDetailsDto.MailListId != Guid.Empty) // Only validate if a MailListId is provided + { + bool mailTemplateExists; + try + { + mailTemplateExists = await _context.MailingList + .AsNoTracking() + .AnyAsync(m => m.Id == mailDetailsDto.MailListId && m.TenantId == tenantId); + } + catch (Exception ex) + { + _logger.LogError("Database Error: Failed to check existence of MailListId '{MailListId}' for TenantId: {TenantId}. : {Error}", mailDetailsDto.MailListId, tenantId, ex.Message); + return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An error occurred while validating mail template.", 500)); + } + + if (!mailTemplateExists) + { + _logger.LogWarning("Validation Error: Provided MailListId '{MailListId}' does not exist or does not belong to TenantId: {TenantId}.", mailDetailsDto.MailListId, tenantId); + return NotFound(ApiResponse.ErrorResponse("Invalid Mail Template", "The specified mail template (MailListId) was not found or accessible.", 404)); + } + } + // If MailListId can be null/empty and implies no specific template, adjust logic accordingly. + // Currently assumes it must exist if provided. + + // 4. Create and Add New Mail Details + var newMailDetails = new MailDetails { ProjectId = mailDetailsDto.ProjectId, Recipient = mailDetailsDto.Recipient, Schedule = mailDetailsDto.Schedule, MailListId = mailDetailsDto.MailListId, - TenantId = tenantId + TenantId = tenantId, }; - _context.MailDetails.Add(mailDetails); - await _context.SaveChangesAsync(); - return Ok("Success"); + + try + { + _context.MailDetails.Add(newMailDetails); + await _context.SaveChangesAsync(); + _logger.LogInfo("Successfully added new mail details with ID {MailDetailsId} for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}.", newMailDetails.Id, newMailDetails.ProjectId, newMailDetails.Recipient, tenantId); + + // 5. Return Success Response (201 Created is ideal for resource creation) + return StatusCode(201, ApiResponse.SuccessResponse( + newMailDetails, // Return the newly created object (or a DTO of it) + "Mail details added successfully.", + 201)); + } + catch (DbUpdateException dbEx) + { + _logger.LogError("Database Error: Failed to save new mail details for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}. : {Error}", newMailDetails.ProjectId, newMailDetails.Recipient, tenantId, dbEx.Message); + // Check for specific constraint violations if applicable (e.g., duplicate recipient for a project) + return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An error occurred while saving the mail details.", 500)); + } + catch (Exception ex) + { + _logger.LogError("Unexpected Error: An unhandled exception occurred while adding mail details for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}. : {Error}", newMailDetails.ProjectId, newMailDetails.Recipient, tenantId, ex.Message); + return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500)); + } } - [HttpPost("mail-template")] - public async Task AddMailTemplate([FromBody] MailTemeplateDto mailTemeplateDto) + [HttpPost("mail-template1")] + public async Task AddMailTemplate1([FromBody] MailTemeplateDto mailTemeplateDto) { Guid tenantId = _userHelper.GetTenantId(); if (string.IsNullOrWhiteSpace(mailTemeplateDto.Body) && string.IsNullOrWhiteSpace(mailTemeplateDto.Title)) @@ -80,116 +182,376 @@ namespace Marco.Pms.Services.Controllers return Ok("Success"); } + /// + /// Adds a new mail template. + /// + /// The mail template data. + /// An API response indicating success or failure. + [HttpPost("mail-template")] // More specific route for adding a template + public async Task AddMailTemplate([FromBody] MailTemeplateDto mailTemplateDto) // Renamed parameter for consistency + { + // 1. Get Tenant ID and Basic Authorization Check + Guid tenantId = _userHelper.GetTenantId(); + if (tenantId == Guid.Empty) + { + _logger.LogWarning("Authorization Error: Attempt to add mail template with an empty or invalid tenant ID."); + return Unauthorized(ApiResponse.ErrorResponse("Unauthorized", "Tenant ID not found or invalid.", 401)); + } + + // 2. Input Validation (Moved to model validation if possible, or keep explicit) + // Use proper model validation attributes ([Required], [StringLength]) on MailTemeplateDto + // and rely on ASP.NET Core's automatic model validation if possible. + // If not, these checks are good. + if (mailTemplateDto == null) + { + _logger.LogWarning("Validation Error: Mail template DTO is null."); + return BadRequest(ApiResponse.ErrorResponse("Invalid Data", "Request body is empty.", 400)); + } + + if (string.IsNullOrWhiteSpace(mailTemplateDto.Title)) + { + _logger.LogWarning("Validation Error: Mail template title is empty or whitespace. TenantId: {TenantId}", tenantId); + return BadRequest(ApiResponse.ErrorResponse("Validation Failed", "Mail template title cannot be empty.", 400)); + } + + // The original logic checked both body and title, but often a template needs at least a title. + // Re-evalute if body can be empty. If so, remove the body check. Assuming title is always mandatory. + // If both body and title are empty, it's definitely invalid. + if (string.IsNullOrWhiteSpace(mailTemplateDto.Body) && string.IsNullOrWhiteSpace(mailTemplateDto.Subject)) + { + _logger.LogWarning("Validation Error: Mail template body and subject are both empty or whitespace for title '{Title}'. TenantId: {TenantId}", mailTemplateDto.Title, tenantId); + return BadRequest(ApiResponse.ErrorResponse("Validation Failed", "Mail template body or subject must be provided.", 400)); + } + + // 3. Check for Existing Template Title (Case-Insensitive) + // Use AsNoTracking() for read-only query + MailingList? existingTemplate; + try + { + existingTemplate = await _context.MailingList + .AsNoTracking() // Important for read-only checks + .FirstOrDefaultAsync(t => t.Title.ToLower() == mailTemplateDto.Title.ToLower() && t.TenantId == tenantId); // IMPORTANT: Filter by TenantId! + } + catch (Exception ex) + { + _logger.LogError("Database Error: Failed to check for existing mail template with title '{Title}' for TenantId: {TenantId}.: {Error}", mailTemplateDto.Title, tenantId, ex.Message); + return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An error occurred while checking for existing templates.", 500)); + } + + + if (existingTemplate != null) + { + _logger.LogWarning("Conflict Error: User tries to add email template with title '{Title}' which already exists for TenantId: {TenantId}.", mailTemplateDto.Title, tenantId); + return Conflict(ApiResponse.ErrorResponse("Conflict", $"Email template with title '{mailTemplateDto.Title}' already exists.", 409)); + } + + // 4. Create and Add New Template + var newMailingList = new MailingList + { + Title = mailTemplateDto.Title, + Body = mailTemplateDto.Body, + Subject = mailTemplateDto.Subject, + Keywords = mailTemplateDto.Keywords, + TenantId = tenantId, + }; + + try + { + _context.MailingList.Add(newMailingList); + await _context.SaveChangesAsync(); + _logger.LogInfo("Successfully added new mail template with ID {TemplateId} and title '{Title}' for TenantId: {TenantId}.", newMailingList.Id, newMailingList.Title, tenantId); + + // 5. Return Success Response (201 Created is ideal for resource creation) + // It's good practice to return the created resource or its ID. + return StatusCode(201, ApiResponse.SuccessResponse( + newMailingList, // Return the newly created object (or a DTO of it) + "Mail template added successfully.", + 201)); + } + catch (DbUpdateException dbEx) + { + _logger.LogError("Database Error: Failed to save new mail template '{Title}' for TenantId: {TenantId}. : {Error}", mailTemplateDto.Title, tenantId, dbEx.Message); + return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An error occurred while saving the mail template.", 500)); + } + catch (Exception ex) + { + _logger.LogError("Unexpected Error: An unhandled exception occurred while adding mail template '{Title}' for TenantId: {TenantId}. : {Error}", mailTemplateDto.Title, tenantId, ex.Message); + return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500)); + } + } + [HttpGet("project-statistics")] public async Task SendProjectReport() { Guid tenantId = _userHelper.GetTenantId(); - // Use AsNoTracking() for read-only queries to improve performance - List mailDetails = await _context.MailDetails + // 1. OPTIMIZATION: Perform grouping and projection on the database server. + // This is far more efficient than loading all entities into memory. + var projectMailGroups = await _context.MailDetails .AsNoTracking() - .Include(m => m.MailBody) .Where(m => m.TenantId == tenantId) - .ToListAsync(); - - int successCount = 0; - int notFoundCount = 0; - int invalidIdCount = 0; - - var groupedMails = mailDetails .GroupBy(m => new { m.ProjectId, m.MailListId }) .Select(g => new { ProjectId = g.Key.ProjectId, - MailListId = g.Key.MailListId, Recipients = g.Select(m => m.Recipient).Distinct().ToList(), - MailBody = g.FirstOrDefault()?.MailBody?.Body ?? "", - Subject = g.FirstOrDefault()?.MailBody?.Subject ?? string.Empty, + // Project the mail body and subject from the first record in the group + MailInfo = g.Select(m => new { Body = m.MailBody != null ? m.MailBody.Body : "", Subject = m.MailBody != null ? m.MailBody.Subject : "" }).FirstOrDefault() }) - .ToList(); + .ToListAsync(); - var semaphore = new SemaphoreSlim(1); - - // Using Task.WhenAll to send reports concurrently for better performance - var sendTasks = groupedMails.Select(async mailDetail => + if (!projectMailGroups.Any()) { - await semaphore.WaitAsync(); - try + return Ok(ApiResponse.SuccessResponse(new { }, "No projects found to send reports for.", 200)); + } + + int successCount = 0; + int notFoundCount = 0; + int invalidIdCount = 0; + int failureCount = 0; + + // 2. OPTIMIZATION: Use true concurrency by removing SemaphoreSlim(1) + // and giving each task its own isolated set of services (including DbContext). + var sendTasks = projectMailGroups.Select(async mailGroup => + { + // SOLUTION: Create a new Dependency Injection scope for each parallel task. + using (var scope = _serviceScopeFactory.CreateScope()) { - var response = await GetProjectStatistics(mailDetail.ProjectId, mailDetail.Recipients, mailDetail.MailBody, mailDetail.Subject, tenantId); - if (response.StatusCode == 200) - Interlocked.Increment(ref successCount); - else if (response.StatusCode == 404) - Interlocked.Increment(ref notFoundCount); - else if (response.StatusCode == 400) - Interlocked.Increment(ref invalidIdCount); - } - finally - { - semaphore.Release(); + // Resolve a new instance of the helper from this isolated scope. + // This ensures each task gets its own thread-safe DbContext. + var reportHelper = scope.ServiceProvider.GetRequiredService(); + + try + { + // Ensure MailInfo and ProjectId are valid before proceeding + if (mailGroup.MailInfo == null || mailGroup.ProjectId == Guid.Empty) + { + Interlocked.Increment(ref invalidIdCount); + return; + } + + var response = await reportHelper.GetProjectStatistics( + mailGroup.ProjectId, + mailGroup.Recipients, + mailGroup.MailInfo.Body, + mailGroup.MailInfo.Subject, + tenantId); + + // Use a switch expression for cleaner counting + switch (response.StatusCode) + { + case 200: Interlocked.Increment(ref successCount); break; + case 404: Interlocked.Increment(ref notFoundCount); break; + case 400: Interlocked.Increment(ref invalidIdCount); break; + default: Interlocked.Increment(ref failureCount); break; + } + } + catch (Exception ex) + { + // 3. OPTIMIZATION: Make the process resilient. + // If one task fails unexpectedly, log it and continue with others. + _logger.LogError("Failed to send report for project {ProjectId} : {Error}", mailGroup.ProjectId, ex.Message); + Interlocked.Increment(ref failureCount); + } } }).ToList(); await Task.WhenAll(sendTasks); - //var response = await GetProjectStatistics(Guid.Parse("2618eb89-2823-11f0-9d9e-bc241163f504"), "ashutosh.nehete@marcoaiot.com", tenantId); + var summaryMessage = $"Processing complete. Success: {successCount}, Not Found: {notFoundCount}, Invalid ID: {invalidIdCount}, Failures: {failureCount}."; _logger.LogInfo( - "Emails of project reports sent for tenant {TenantId}. Successfully sent: {SuccessCount}, Projects not found: {NotFoundCount}, Invalid IDs: {InvalidIdsCount}", - tenantId, successCount, notFoundCount, invalidIdCount); + "Project report sending complete for tenant {TenantId}. Success: {SuccessCount}, Not Found: {NotFoundCount}, Invalid ID: {InvalidIdCount}, Failures: {FailureCount}", + tenantId, successCount, notFoundCount, invalidIdCount, failureCount); return Ok(ApiResponse.SuccessResponse( - new { }, - $"Reports sent successfully: {successCount}. Projects not found: {notFoundCount}. Invalid IDs: {invalidIdCount}.", + new { successCount, notFoundCount, invalidIdCount, failureCount }, + summaryMessage, 200)); } - /// - /// 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. - private async Task> GetProjectStatistics(Guid projectId, List recipientEmails, string body, string subject, Guid tenantId) + + //[HttpPost("add-report-mail1")] + //public async Task StoreProjectStatistics1() + //{ + + // Guid tenantId = _userHelper.GetTenantId(); + + // // Use AsNoTracking() for read-only queries to improve performance + // List mailDetails = await _context.MailDetails + // .AsNoTracking() + // .Include(m => m.MailBody) + // .Where(m => m.TenantId == tenantId) + // .ToListAsync(); + + // var groupedMails = mailDetails + // .GroupBy(m => new { m.ProjectId, m.MailListId }) + // .Select(g => new + // { + // ProjectId = g.Key.ProjectId, + // MailListId = g.Key.MailListId, + // Recipients = g.Select(m => m.Recipient).Distinct().ToList(), + // MailBody = g.FirstOrDefault()?.MailBody?.Body ?? "", + // Subject = g.FirstOrDefault()?.MailBody?.Subject ?? string.Empty, + // }) + // .ToList(); + // foreach (var groupMail in groupedMails) + // { + // var projectId = groupMail.ProjectId; + // var body = groupMail.MailBody; + // var subject = groupMail.Subject; + // var receivers = groupMail.Recipients; + // if (projectId == Guid.Empty) + // { + // _logger.LogError("Provided empty project ID while fetching project report."); + // return NotFound(ApiResponse.ErrorResponse("Provided empty Project ID.", "Provided empty Project ID.", 400)); + // } + + + // var statisticReport = await _reportHelper.GetDailyProjectReport(projectId, tenantId); + + // if (statisticReport == null) + // { + // _logger.LogWarning("User attempted to fetch project progress for project ID {ProjectId} but not found.", projectId); + // return NotFound(ApiResponse.ErrorResponse("Project not found.", "Project not found.", 404)); + // } + // var date = statisticReport.Date.ToString("dd-MMM-yyyy", CultureInfo.InvariantCulture); + + // // Send Email + // var emailBody = await _emailSender.SendProjectStatisticsEmail(new List(), body, subject, statisticReport); + // var subjectReplacements = new Dictionary + // { + // {"DATE", date }, + // {"PROJECT_NAME", statisticReport.ProjectName} + // }; + // foreach (var item in subjectReplacements) + // { + // subject = subject.Replace($"{{{{{item.Key}}}}}", item.Value); + // } + // string env = _configuration["environment:Title"] ?? string.Empty; + // if (string.IsNullOrWhiteSpace(env)) + // { + // subject = $"{subject}"; + // } + // else + // { + // subject = $"({env}) {subject}"; + // } + // var mail = new ProjectReportEmailMongoDB + // { + // IsSent = false, + // Body = emailBody, + // Receivers = receivers, + // Subject = subject, + // }; + // await _cache.AddProjectReportMail(mail); + // } + // return Ok(ApiResponse.SuccessResponse("Project Report Mail is stored in MongoDB", "Project Report Mail is stored in MongoDB", 200)); + //} + + [HttpPost("add-report-mail")] + public async Task StoreProjectStatistics() { + Guid tenantId = _userHelper.GetTenantId(); - if (projectId == Guid.Empty) + // 1. Database-Side Grouping (Still the most efficient way to get initial data) + var projectMailGroups = await _context.MailDetails + .AsNoTracking() + .Where(m => m.TenantId == tenantId && m.ProjectId != Guid.Empty) + .GroupBy(m => new { m.ProjectId, m.MailListId }) + .Select(g => new + { + g.Key.ProjectId, + Recipients = g.Select(m => m.Recipient).Distinct().ToList(), + MailInfo = g.Select(m => new { Body = m.MailBody != null ? m.MailBody.Body : "", Subject = m.MailBody != null ? m.MailBody.Subject : "" }).FirstOrDefault() + }) + .ToListAsync(); + + if (!projectMailGroups.Any()) { - _logger.LogError("Provided empty project ID while fetching project report."); - return ApiResponse.ErrorResponse("Provided empty Project ID.", "Provided empty Project ID.", 400); + _logger.LogInfo("No project mail details found for tenant {TenantId} to process.", tenantId); + return Ok(ApiResponse.SuccessResponse("No project reports to generate.", "No project reports to generate.", 200)); } + string env = _configuration["environment:Title"] ?? string.Empty; - var statisticReport = await _reportHelper.GetDailyProjectReport(projectId, tenantId); - - if (statisticReport == null) + // 2. Process each group concurrently, but with isolated DBContexts. + var processingTasks = projectMailGroups.Select(async group => { - _logger.LogWarning("User attempted to fetch project progress for project ID {ProjectId} but not found.", projectId); - return ApiResponse.ErrorResponse("Project not found.", "Project not found.", 404); - } + // SOLUTION: Create a new DI scope for each parallel task. + using (var scope = _serviceScopeFactory.CreateScope()) + { + // Resolve services from this new, isolated scope. + // These helpers will get their own fresh DbContext instance. + var reportHelper = scope.ServiceProvider.GetRequiredService(); + var emailSender = scope.ServiceProvider.GetRequiredService(); + var cache = scope.ServiceProvider.GetRequiredService(); // e.g., IProjectReportCache - // Send Email - var emailBody = await _emailSender.SendProjectStatisticsEmail(recipientEmails, body, subject, statisticReport); - var employee = await _context.Employees.FirstOrDefaultAsync(e => e.Email != null && recipientEmails.Contains(e.Email)) ?? new Employee(); - - List mailLogs = new List(); - foreach (var recipientEmail in recipientEmails) - { - mailLogs.Add( - new MailLog + // The rest of the logic is the same, but now it's thread-safe. + try { - ProjectId = projectId, - EmailId = recipientEmail, - Body = emailBody, - EmployeeId = employee.Id, - TimeStamp = DateTime.UtcNow, - TenantId = tenantId - }); + var projectId = group.ProjectId; + var statisticReport = await reportHelper.GetDailyProjectReport(projectId, tenantId); + + if (statisticReport == null) + { + _logger.LogWarning("Statistic report for project ID {ProjectId} not found. Skipping.", projectId); + return; + } + + if (group.MailInfo == null) + { + _logger.LogWarning("MailBody info for project ID {ProjectId} not found. Skipping.", projectId); + return; + } + + var date = statisticReport.Date.ToString("dd-MMM-yyyy", CultureInfo.InvariantCulture); + // Assuming the first param to SendProjectStatisticsEmail was just a placeholder + var emailBody = await emailSender.SendProjectStatisticsEmail(new List(), group.MailInfo.Body, string.Empty, statisticReport); + + string subject = group.MailInfo.Subject + .Replace("{{DATE}}", date) + .Replace("{{PROJECT_NAME}}", statisticReport.ProjectName); + + subject = string.IsNullOrWhiteSpace(env) ? subject : $"({env}) {subject}"; + + var mail = new ProjectReportEmailMongoDB + { + IsSent = false, + Body = emailBody, + Receivers = group.Recipients, + Subject = subject, + }; + + await cache.AddProjectReportMail(mail); + } + catch (Exception ex) + { + // It's good practice to log any unexpected errors within a concurrent task. + _logger.LogError("Failed to process project report for ProjectId {ProjectId} : {Error}", group.ProjectId, ex.Message); + } + } + }); + + // Await all the concurrent, now thread-safe, tasks. + await Task.WhenAll(processingTasks); + + return Ok(ApiResponse.SuccessResponse( + $"{projectMailGroups.Count} Project Report Mail(s) are queued for storage.", + "Project Report Mail processing initiated.", + 200)); + } + + + [HttpGet("report-mail")] + public async Task GetProjectStatisticsFromCache() + { + var mailList = await _cache.GetProjectReportMail(false); + if (mailList == null) + { + return NotFound(ApiResponse.ErrorResponse("Not mail found", "Not mail found", 404)); } - _context.MailLogs.AddRange(mailLogs); - - await _context.SaveChangesAsync(); - return ApiResponse.SuccessResponse(statisticReport, "Email sent successfully", 200); + return Ok(ApiResponse.SuccessResponse(mailList, "Fetched list of mail body successfully", 200)); } } } diff --git a/Marco.Pms.Services/Service/EmailSender.cs b/Marco.Pms.Services/Service/EmailSender.cs index 568510a..4d66a4f 100644 --- a/Marco.Pms.Services/Service/EmailSender.cs +++ b/Marco.Pms.Services/Service/EmailSender.cs @@ -150,18 +150,24 @@ namespace MarcoBMS.Services.Service emailBody = emailBody.Replace("{{TEAM_ON_SITE}}", BuildTeamOnSiteHtml(report.TeamOnSite)); emailBody = emailBody.Replace("{{PERFORMED_TASK}}", BuildPerformedTaskHtml(report.PerformedTasks, report.Date)); emailBody = emailBody.Replace("{{PERFORMED_ATTENDANCE}}", BuildPerformedAttendanceHtml(report.PerformedAttendance)); - var subjectReplacements = new Dictionary + if (!string.IsNullOrWhiteSpace(subject)) { - {"DATE", date }, - {"PROJECT_NAME", report.ProjectName} - }; - foreach (var item in subjectReplacements) - { - subject = subject.Replace($"{{{{{item.Key}}}}}", item.Value); + var subjectReplacements = new Dictionary + { + {"DATE", date }, + {"PROJECT_NAME", report.ProjectName} + }; + foreach (var item in subjectReplacements) + { + subject = subject.Replace($"{{{{{item.Key}}}}}", item.Value); + } + string env = _configuration["environment:Title"] ?? string.Empty; + subject = CheckSubject(subject); + } + if (toEmails.Count > 0) + { + await SendEmailAsync(toEmails, subject, emailBody); } - string env = _configuration["environment:Title"] ?? string.Empty; - subject = CheckSubject(subject); - await SendEmailAsync(toEmails, subject, emailBody); return emailBody; } public async Task SendOTP(List toEmails, string emailBody, string name, string otp, string subject) From 8bb8b3643f72cdf60da3c1bdef59326e9e15f504 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Sat, 12 Jul 2025 13:14:15 +0530 Subject: [PATCH 064/124] Refactored the function to add project in cache and added auto Mapper --- Marco.Pms.CacheHelper/ProjectCache.cs | 135 +----- .../Controllers/AttendanceController.cs | 28 +- .../Controllers/DashboardController.cs | 2 +- .../Controllers/EmployeeController.cs | 9 +- .../Controllers/ImageController.cs | 6 +- .../Controllers/ProjectController.cs | 286 ++++++++---- .../Helpers/CacheUpdateHelper.cs | 432 +++++++++++++++++- Marco.Pms.Services/Helpers/ProjectsHelper.cs | 77 +--- Marco.Pms.Services/Helpers/ReportHelper.cs | 99 +++- .../MappingProfiles/ProjectMappingProfile.cs | 30 ++ Marco.Pms.Services/Marco.Pms.Services.csproj | 1 + Marco.Pms.Services/Program.cs | 269 ++++++----- Marco.Pms.Services/Service/ILoggingService.cs | 5 +- Marco.Pms.Services/Service/LoggingServices.cs | 18 +- .../Service/PermissionServices.cs | 40 +- 15 files changed, 958 insertions(+), 479 deletions(-) create mode 100644 Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs 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 8ed0ba0..bdb965c 100644 --- a/Marco.Pms.Services/Controllers/DashboardController.cs +++ b/Marco.Pms.Services/Controllers/DashboardController.cs @@ -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) { 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); } } } From 4ba533f64791cd2800f063f391899b048b693759 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 14 Jul 2025 12:02:45 +0530 Subject: [PATCH 065/124] Optimized both get Project list API and get Project list basic API --- Marco.Pms.CacheHelper/ProjectCache.cs | 43 +- .../Controllers/ProjectController.cs | 727 +++++++++--------- .../Controllers/UserController.cs | 2 +- .../Helpers/CacheUpdateHelper.cs | 43 +- Marco.Pms.Services/Helpers/ProjectsHelper.cs | 2 +- Marco.Pms.Services/Helpers/RolesHelper.cs | 121 ++- .../Service/PermissionServices.cs | 2 +- 7 files changed, 513 insertions(+), 427 deletions(-) diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index 1fd36f4..183bbc4 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -32,20 +32,9 @@ namespace Marco.Pms.CacheHelper 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) + public async Task UpdateProjectDetailsOnlyToCache(Project project, StatusMaster projectStatus) { - //_logger.LogInfo("Starting update for project: {ProjectId}", project.Id); - - var projectStatus = await _context.StatusMasters - .FirstOrDefaultAsync(s => s.Id == project.ProjectStatusId); - - if (projectStatus == null) - { - //_logger.LogWarning("StatusMaster not found for ProjectStatusId: {StatusId}", project.ProjectStatusId); - } - // Build the update definition var updates = Builders.Update.Combine( Builders.Update.Set(r => r.Name, project.Name), @@ -69,11 +58,9 @@ namespace Marco.Pms.CacheHelper if (result.MatchedCount == 0) { - //_logger.LogWarning("No project matched in MongoDB for update. ProjectId: {ProjectId}", project.Id); return false; } - //_logger.LogInfo("Project {ProjectId} successfully updated in MongoDB", project.Id); return true; } public async Task GetProjectDetailsFromCache(Guid projectId) @@ -83,21 +70,12 @@ namespace Marco.Pms.CacheHelper var filter = Builders.Filter.Eq(p => p.Id, projectId.ToString()); var projection = Builders.Projection.Exclude(p => p.Buildings); - //_logger.LogInfo("Fetching project details for ProjectId: {ProjectId} from MongoDB", projectId); - // Perform query var project = await _projetCollection .Find(filter) .Project(projection) .FirstOrDefaultAsync(); - if (project == null) - { - //_logger.LogWarning("No project found in MongoDB for ProjectId: {ProjectId}", projectId); - return null; - } - - //_logger.LogInfo("Successfully fetched project details (excluding Buildings) for ProjectId: {ProjectId}", projectId); return project; } public async Task> GetProjectDetailsListFromCache(List projectIds) @@ -111,6 +89,12 @@ namespace Marco.Pms.CacheHelper .ToListAsync(); return projects; } + public async Task DeleteProjectByIdFromCacheAsync(Guid projectId) + { + var filter = Builders.Filter.Eq(e => e.Id, projectId.ToString()); + var result = await _projetCollection.DeleteOneAsync(filter); + return result.DeletedCount > 0; + } // ------------------------------------------------------- Project InfraStructure ------------------------------------------------------- @@ -407,6 +391,10 @@ namespace Marco.Pms.CacheHelper return null; return result; } + + + // ------------------------------------------------------- WorkItem ------------------------------------------------------- + public async Task> GetWorkItemsByWorkAreaIdsFromCache(List workAreaIds) { var stringWorkAreaIds = workAreaIds.Select(wa => wa.ToString()).ToList(); @@ -418,9 +406,6 @@ namespace Marco.Pms.CacheHelper return workItems; } - - // ------------------------------------------------------- WorkItem ------------------------------------------------------- - public async Task ManageWorkItemDetailsToCache(List workItems) { var activityIds = workItems.Select(wi => wi.ActivityId).ToList(); @@ -510,5 +495,11 @@ namespace Marco.Pms.CacheHelper } return false; } + public async Task DeleteWorkItemByIdFromCacheAsync(Guid workItemId) + { + var filter = Builders.Filter.Eq(e => e.Id, workItemId.ToString()); + var result = await _taskCollection.DeleteOneAsync(filter); + return result.DeletedCount > 0; + } } } diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 29f9d04..adb5887 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -28,10 +28,10 @@ namespace MarcoBMS.Services.Controllers [Authorize] public class ProjectController : ControllerBase { + private readonly IDbContextFactory _dbContextFactory; private readonly ApplicationDbContext _context; private readonly UserHelper _userHelper; private readonly ILoggingService _logger; - //private readonly RolesHelper _rolesHelper; private readonly ProjectsHelper _projectsHelper; private readonly IHubContext _signalR; private readonly PermissionServices _permission; @@ -40,13 +40,13 @@ namespace MarcoBMS.Services.Controllers private readonly Guid tenantId; - public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, ProjectsHelper projectHelper, - IHubContext signalR, PermissionServices permission, CacheUpdateHelper cache, IMapper mapper) + public ProjectController(IDbContextFactory dbContextFactory, ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, + ProjectsHelper projectHelper, IHubContext signalR, PermissionServices permission, CacheUpdateHelper cache, IMapper mapper) { + _dbContextFactory = dbContextFactory; _context = context; _userHelper = userHelper; _logger = logger; - //_rolesHelper = rolesHelper; _projectsHelper = projectHelper; _signalR = signalR; _cache = cache; @@ -55,55 +55,10 @@ namespace MarcoBMS.Services.Controllers tenantId = _userHelper.GetTenantId(); } - [HttpGet("list/basic1")] - public async Task GetAllProjects1() - { - if (!ModelState.IsValid) - { - var errors = ModelState.Values - .SelectMany(v => v.Errors) - .Select(e => e.ErrorMessage) - .ToList(); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); - - } - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - // Defensive check for null employee (important for robust APIs) - if (LoggedInEmployee == null) - { - return Unauthorized(ApiResponse.ErrorResponse("Employee not found.", null, 401)); - } - - List response = new List(); - List projectIds = 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(); - - //} - 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)); - } + #region =================================================================== Project Get APIs =================================================================== [HttpGet("list/basic")] - public async Task GetAllProjects() // Renamed for clarity + public async Task GetAllProjectsBasic() { // Step 1: Get the current user var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); @@ -133,146 +88,82 @@ namespace MarcoBMS.Services.Controllers } /// - /// 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. + /// Retrieves a list of projects accessible to the current user, including aggregated details. + /// This method is optimized to use a cache-first approach. If data is not in the cache, + /// it fetches and aggregates data efficiently from the database in parallel. /// - /// 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; - } + /// An ApiResponse containing a list of projects or an error. [HttpGet("list")] - public async Task GetAll() + public async Task GetAllProjects() { + // --- Step 1: Input Validation and Initial Setup --- if (!ModelState.IsValid) { var errors = ModelState.Values .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); - + _logger.LogWarning("GetAllProjects called with invalid model state. Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - //List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(LoggedInEmployee.Id); - //string[] projectsId = []; - //List projects = new List(); - ///* User with permission manage project can see all projects */ - //if (featurePermission != null && featurePermission.Exists(c => c.Id.ToString() == "172fc9b6-755b-4f62-ab26-55c34a330614")) - //{ - // projects = await _projectsHelper.GetAllProjectByTanentID(LoggedInEmployee.TenantId); - //} - //else - //{ - // List allocation = await _projectsHelper.GetProjectByEmployeeID(LoggedInEmployee.Id); - // projectsId = allocation.Select(c => c.ProjectId.ToString()).ToArray(); - // projects = await _context.Projects.Where(c => projectsId.Contains(c.Id.ToString()) && c.TenantId == tenantId).ToListAsync(); - //} - - //List projects = await _projectsHelper.GetMyProjects(tenantId, LoggedInEmployee); - ////List projects = new List(); - /// - List response = new List(); - List projectIds = await _projectsHelper.GetMyProjects(tenantId, LoggedInEmployee); - - var projectsDetails = await _cache.GetProjectDetailsList(projectIds); - if (projectsDetails == null) + try { - List projects = await _context.Projects.Where(p => projectIds.Contains(p.Id)).ToListAsync(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + _logger.LogInfo("Starting GetAllProjects for TenantId: {TenantId}, User: {UserId}", tenantId, loggedInEmployee.Id); - var teams = await _context.ProjectAllocations.Where(p => p.TenantId == tenantId && projectIds.Contains(p.ProjectId) && p.IsActive == true).ToListAsync(); - - - List allBuildings = await _context.Buildings.Where(b => projectIds.Contains(b.ProjectId) && b.TenantId == tenantId).ToListAsync(); - List idList = allBuildings.Select(b => b.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 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) + // --- Step 2: Get a list of project IDs the user can access --- + List projectIds = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); + if (!projectIds.Any()) { - 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); + _logger.LogInfo("User has no assigned projects. Returning empty list."); + return Ok(ApiResponse>.SuccessResponse(new List(), "No projects found for the current user.", 200)); } - } - else - { - response = projectsDetails.Select(p => _mapper.Map(p)).ToList(); - } - return Ok(ApiResponse.SuccessResponse(response, "Success.", 200)); + // --- Step 3: Efficiently handle partial cache hits --- + _logger.LogInfo("Attempting to fetch details for {ProjectCount} projects from cache.", projectIds.Count); + + // Fetch what we can from the cache. + var cachedDetails = await _cache.GetProjectDetailsList(projectIds) ?? new List(); + var cachedDictionary = cachedDetails.ToDictionary(p => Guid.Parse(p.Id)); + + // Identify which projects are missing from the cache. + var missingIds = projectIds.Where(id => !cachedDictionary.ContainsKey(id)).ToList(); + + // Start building the response with the items we found in the cache. + var responseVms = _mapper.Map>(cachedDictionary.Values); + + if (missingIds.Any()) + { + // --- Step 4: Fetch ONLY the missing items from the database --- + _logger.LogInfo("Cache partial MISS. Found {CachedCount}, fetching {MissingCount} projects from DB.", + cachedDictionary.Count, missingIds.Count); + + // Call our dedicated data-fetching method for the missing IDs. + var newMongoDetails = await FetchAndBuildProjectDetails(missingIds, tenantId); + + if (newMongoDetails.Any()) + { + // Map the newly fetched items and add them to our response list. + responseVms.AddRange(newMongoDetails); + } + } + else + { + _logger.LogInfo("Cache HIT. All {ProjectCount} projects found in cache.", projectIds.Count); + } + + // --- Step 5: Return the combined result --- + _logger.LogInfo("Successfully retrieved a total of {ProjectCount} projects.", responseVms.Count); + return Ok(ApiResponse>.SuccessResponse(responseVms, "Projects retrieved successfully.", 200)); + } + catch (Exception ex) + { + // --- Step 6: Graceful Error Handling --- + _logger.LogError("An unexpected error occurred in GetAllProjects for tenant {TenantId}. : {Error}", tenantId, ex.Message); + return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred. Please try again later.", null, 500)); + } } [HttpGet("get/{id}")] @@ -351,23 +242,6 @@ namespace MarcoBMS.Services.Controllers { 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) @@ -486,40 +360,9 @@ namespace MarcoBMS.Services.Controllers } - private async Task GetProjectViewModel(Guid? id, Project project) - { - ProjectDetailsVM vm = new ProjectDetailsVM(); + #endregion - // List buildings = _unitOfWork.Building.GetAll(c => c.ProjectId == id).ToList(); - List buildings = await _context.Buildings.Where(c => c.ProjectId == id).ToListAsync(); - List idList = buildings.Select(o => o.Id).ToList(); - // List floors = _unitOfWork.Floor.GetAll(c => idList.Contains(c.Id)).ToList(); - List floors = await _context.Floor.Where(c => idList.Contains(c.BuildingId)).ToListAsync(); - idList = floors.Select(o => o.Id).ToList(); - //List workAreas = _unitOfWork.WorkArea.GetAll(c => idList.Contains(c.Id), includeProperties: "WorkItems,WorkItems.ActivityMaster").ToList(); - - List workAreas = await _context.WorkAreas.Where(c => idList.Contains(c.FloorId)).ToListAsync(); - - idList = workAreas.Select(o => o.Id).ToList(); - List workItems = await _context.WorkItems.Include(c => c.WorkCategoryMaster).Where(c => idList.Contains(c.WorkAreaId)).Include(c => c.ActivityMaster).ToListAsync(); - // List workItems = _unitOfWork.WorkItem.GetAll(c => idList.Contains(c.WorkAreaId), includeProperties: "ActivityMaster").ToList(); - idList = workItems.Select(t => t.Id).ToList(); - List tasks = await _context.TaskAllocations.Where(t => idList.Contains(t.WorkItemId) && t.AssignmentDate.Date == DateTime.UtcNow.Date).ToListAsync(); - vm.project = project; - vm.buildings = buildings; - vm.floors = floors; - vm.workAreas = workAreas; - vm.workItems = workItems; - vm.Tasks = tasks; - return vm; - } - - private Guid GetTenantId() - { - return _userHelper.GetTenantId(); - //var tenant = User.FindFirst("TenantId")?.Value; - //return (tenant != null ? Convert.ToInt32(tenant) : 1); - } + #region =================================================================== Project Manage APIs =================================================================== [HttpPost] public async Task Create([FromBody] CreateProjectDto projectDto) @@ -619,50 +462,9 @@ namespace MarcoBMS.Services.Controllers } } - //[HttpPost("assign-employee")] - //public async Task AssignEmployee(int? allocationid, int employeeId, int projectId) - //{ - // var employee = await _context.Employees.FindAsync(employeeId); - // var project = _projectrepo.Get(c => c.Id == projectId); - // if (employee == null || project == null) - // { - // return NotFound(); - // } + #endregion - // // Logic to add the product to a new table (e.g., selected products) - - // if (allocationid == null) - // { - // // Add allocation - // ProjectAllocation allocation = new ProjectAllocation() - // { - // EmployeeId = employeeId, - // ProjectId = project.Id, - // AllocationDate = DateTime.UtcNow, - // //EmployeeRole = employee.Rol - // TenantId = project.TenantId - // }; - - // _unitOfWork.ProjectAllocation.CreateAsync(allocation); - // } - // else - // { - // //remove allocation - // var allocation = await _context.ProjectAllocations.FindAsync(allocationid); - // if (allocation != null) - // { - // allocation.ReAllocationDate = DateTime.UtcNow; - - // _unitOfWork.ProjectAllocation.UpdateAsync(allocation.Id, allocation); - // } - // else - // { - // return NotFound(); - // } - // } - - // return Ok(); - //} + #region =================================================================== Project Allocation APIs =================================================================== [HttpGet] [Route("employees/get/{projectid?}/{includeInactive?}")] @@ -838,6 +640,134 @@ namespace MarcoBMS.Services.Controllers } + [HttpGet("assigned-projects/{employeeId}")] + public async Task GetProjectsByEmployee([FromRoute] Guid employeeId) + { + + Guid tenantId = _userHelper.GetTenantId(); + if (employeeId == Guid.Empty) + { + return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Employee id not valid.", 400)); + } + + List projectList = await _context.ProjectAllocations + .Where(c => c.TenantId == tenantId && c.EmployeeId == employeeId && c.IsActive) + .Select(c => c.ProjectId).Distinct() + .ToListAsync(); + + if (!projectList.Any()) + { + return NotFound(ApiResponse.SuccessResponse(new List(), "No projects found.", 200)); + } + + + List projectlist = await _context.Projects + .Where(p => projectList.Contains(p.Id)) + .ToListAsync(); + + List projects = new List(); + + + foreach (var project in projectlist) + { + + projects.Add(project.ToProjectListVMFromProject()); + } + + + + return Ok(ApiResponse.SuccessResponse(projects, "Success.", 200)); + } + + [HttpPost("assign-projects/{employeeId}")] + public async Task AssigneProjectsToEmployee([FromBody] List projectAllocationDtos, [FromRoute] Guid employeeId) + { + if (projectAllocationDtos != null && employeeId != Guid.Empty) + { + Guid TenentID = GetTenantId(); + var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + List? result = new List(); + List projectIds = new List(); + + foreach (var projectAllocationDto in projectAllocationDtos) + { + try + { + ProjectAllocation projectAllocation = projectAllocationDto.ToProjectAllocationFromProjectsAllocationDto(TenentID, employeeId); + ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.ProjectId == projectAllocationDto.ProjectId && c.ReAllocationDate == null && c.TenantId == TenentID).SingleOrDefaultAsync(); + + if (projectAllocationFromDb != null) + { + + + _context.ProjectAllocations.Attach(projectAllocationFromDb); + + if (projectAllocationDto.Status) + { + projectAllocationFromDb.JobRoleId = projectAllocation.JobRoleId; ; + projectAllocationFromDb.IsActive = true; + _context.Entry(projectAllocationFromDb).Property(e => e.JobRoleId).IsModified = true; + _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; + } + else + { + projectAllocationFromDb.ReAllocationDate = DateTime.UtcNow; + projectAllocationFromDb.IsActive = false; + _context.Entry(projectAllocationFromDb).Property(e => e.ReAllocationDate).IsModified = true; + _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; + + projectIds.Add(projectAllocation.ProjectId); + } + await _context.SaveChangesAsync(); + var result1 = new + { + Id = projectAllocationFromDb.Id, + EmployeeId = projectAllocation.EmployeeId, + JobRoleId = projectAllocation.JobRoleId, + IsActive = projectAllocation.IsActive, + ProjectId = projectAllocation.ProjectId, + AllocationDate = projectAllocation.AllocationDate, + ReAllocationDate = projectAllocation.ReAllocationDate, + TenantId = projectAllocation.TenantId + }; + result.Add(result1); + } + else + { + projectAllocation.AllocationDate = DateTime.Now; + projectAllocation.IsActive = true; + _context.ProjectAllocations.Add(projectAllocation); + await _context.SaveChangesAsync(); + + projectIds.Add(projectAllocation.ProjectId); + + } + + + } + catch (Exception ex) + { + + return Ok(ApiResponse.ErrorResponse(ex.Message, ex, 400)); + } + } + await _cache.ClearAllProjectIds(employeeId); + var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeId = employeeId }; + + await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + + return Ok(ApiResponse.SuccessResponse(result, "Data saved successfully", 200)); + } + else + { + return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "All Field is required", 400)); + } + + } + + #endregion + + #region =================================================================== Project InfraStructure Get APIs =================================================================== [HttpGet("infra-details/{projectId}")] public async Task GetInfraDetails(Guid projectId) @@ -1026,6 +956,10 @@ namespace MarcoBMS.Services.Controllers return Ok(ApiResponse.SuccessResponse(workItemVMs, $"{workItemVMs.Count} records of tasks fetched successfully", 200)); } + #endregion + + #region =================================================================== Project Infrastructre Manage APIs =================================================================== + [HttpPost("task")] public async Task CreateProjectTask(List workItemDtos) { @@ -1309,131 +1243,172 @@ namespace MarcoBMS.Services.Controllers } - [HttpGet("assigned-projects/{employeeId}")] - public async Task GetProjectsByEmployee([FromRoute] Guid employeeId) + #endregion + + #region =================================================================== Helper Functions =================================================================== + + /// + /// 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); - Guid tenantId = _userHelper.GetTenantId(); - if (employeeId == Guid.Empty) + _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 BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Employee id not valid.", 400)); + return finalViewModels; } - List projectList = await _context.ProjectAllocations - .Where(c => c.TenantId == tenantId && c.EmployeeId == employeeId && c.IsActive) - .Select(c => c.ProjectId).Distinct() - .ToListAsync(); + var cachedIds = cachedMongoDocs.Select(p => p.Id).ToHashSet(); // Assuming ProjectMongoDB has an Id + var missingIds = projectIds.Where(id => !cachedIds.Contains(id.ToString())).ToList(); - if (!projectList.Any()) + // --- Step 3: Fetch Missing from Database --- + if (missingIds.Any()) { - return NotFound(ApiResponse.SuccessResponse(new List(), "No projects found.", 200)); - } + _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(); - List projectlist = await _context.Projects - .Where(p => projectList.Contains(p.Id)) - .ToListAsync(); - - List projects = new List(); - - - foreach (var project in projectlist) - { - - projects.Add(project.ToProjectListVMFromProject()); - } - - - - return Ok(ApiResponse.SuccessResponse(projects, "Success.", 200)); - } - - [HttpPost("assign-projects/{employeeId}")] - public async Task AssigneProjectsToEmployee([FromBody] List projectAllocationDtos, [FromRoute] Guid employeeId) - { - if (projectAllocationDtos != null && employeeId != Guid.Empty) - { - Guid TenentID = GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - List? result = new List(); - List projectIds = new List(); - - foreach (var projectAllocationDto in projectAllocationDtos) + if (projectsFromDb.Any()) { - try - { - ProjectAllocation projectAllocation = projectAllocationDto.ToProjectAllocationFromProjectsAllocationDto(TenentID, employeeId); - ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.ProjectId == projectAllocationDto.ProjectId && c.ReAllocationDate == null && c.TenantId == TenentID).SingleOrDefaultAsync(); + // Map the newly fetched projects (from SQL) to their ViewModel + var vmsFromDb = _mapper.Map>(projectsFromDb); + finalViewModels.AddRange(vmsFromDb); - if (projectAllocationFromDb != null) - { - - - _context.ProjectAllocations.Attach(projectAllocationFromDb); - - if (projectAllocationDto.Status) - { - projectAllocationFromDb.JobRoleId = projectAllocation.JobRoleId; ; - projectAllocationFromDb.IsActive = true; - _context.Entry(projectAllocationFromDb).Property(e => e.JobRoleId).IsModified = true; - _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; - } - else - { - projectAllocationFromDb.ReAllocationDate = DateTime.UtcNow; - projectAllocationFromDb.IsActive = false; - _context.Entry(projectAllocationFromDb).Property(e => e.ReAllocationDate).IsModified = true; - _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; - - projectIds.Add(projectAllocation.ProjectId); - } - await _context.SaveChangesAsync(); - var result1 = new - { - Id = projectAllocationFromDb.Id, - EmployeeId = projectAllocation.EmployeeId, - JobRoleId = projectAllocation.JobRoleId, - IsActive = projectAllocation.IsActive, - ProjectId = projectAllocation.ProjectId, - AllocationDate = projectAllocation.AllocationDate, - ReAllocationDate = projectAllocation.ReAllocationDate, - TenantId = projectAllocation.TenantId - }; - result.Add(result1); - } - else - { - projectAllocation.AllocationDate = DateTime.Now; - projectAllocation.IsActive = true; - _context.ProjectAllocations.Add(projectAllocation); - await _context.SaveChangesAsync(); - - projectIds.Add(projectAllocation.ProjectId); - - } - - - } - catch (Exception ex) - { - - return Ok(ApiResponse.ErrorResponse(ex.Message, ex, 400)); - } + // --- 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); } - await _cache.ClearAllProjectIds(employeeId); - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeId = employeeId }; - - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); - - return Ok(ApiResponse.SuccessResponse(result, "Data saved successfully", 200)); - } - else - { - return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "All Field is required", 400)); } + return finalViewModels; + } + + private Guid GetTenantId() + { + return _userHelper.GetTenantId(); + } + + private async Task GetProjectViewModel(Guid? id, Project project) + { + ProjectDetailsVM vm = new ProjectDetailsVM(); + + // List buildings = _unitOfWork.Building.GetAll(c => c.ProjectId == id).ToList(); + List buildings = await _context.Buildings.Where(c => c.ProjectId == id).ToListAsync(); + List idList = buildings.Select(o => o.Id).ToList(); + // List floors = _unitOfWork.Floor.GetAll(c => idList.Contains(c.Id)).ToList(); + List floors = await _context.Floor.Where(c => idList.Contains(c.BuildingId)).ToListAsync(); + idList = floors.Select(o => o.Id).ToList(); + //List workAreas = _unitOfWork.WorkArea.GetAll(c => idList.Contains(c.Id), includeProperties: "WorkItems,WorkItems.ActivityMaster").ToList(); + + List workAreas = await _context.WorkAreas.Where(c => idList.Contains(c.FloorId)).ToListAsync(); + + idList = workAreas.Select(o => o.Id).ToList(); + List workItems = await _context.WorkItems.Include(c => c.WorkCategoryMaster).Where(c => idList.Contains(c.WorkAreaId)).Include(c => c.ActivityMaster).ToListAsync(); + // List workItems = _unitOfWork.WorkItem.GetAll(c => idList.Contains(c.WorkAreaId), includeProperties: "ActivityMaster").ToList(); + idList = workItems.Select(t => t.Id).ToList(); + List tasks = await _context.TaskAllocations.Where(t => idList.Contains(t.WorkItemId) && t.AssignmentDate.Date == DateTime.UtcNow.Date).ToListAsync(); + vm.project = project; + vm.buildings = buildings; + vm.floors = floors; + vm.workAreas = workAreas; + vm.workItems = workItems; + vm.Tasks = tasks; + return vm; } + /// + /// Fetches project details from the database for a given list of project IDs and assembles them into MongoDB models. + /// This method encapsulates the optimized, parallel database queries. + /// + /// The list of project IDs to fetch. + /// The current tenant ID for filtering. + /// A list of fully populated ProjectMongoDB objects. + private async Task> FetchAndBuildProjectDetails(List projectIdsToFetch, Guid tenantId) + { + // Task to get base project details for the MISSING projects + var projectsTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + return await context.Projects.AsNoTracking() + .Where(p => projectIdsToFetch.Contains(p.Id) && p.TenantId == tenantId) + .ToListAsync(); + }); + + // Task to get team sizes for the MISSING projects + var teamSizesTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + return await context.ProjectAllocations.AsNoTracking() + .Where(pa => pa.TenantId == tenantId && projectIdsToFetch.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); + }); + + // Task to get work summaries for the MISSING projects + var workSummariesTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + return await context.WorkItems.AsNoTracking() + .Where(wi => wi.TenantId == tenantId && + wi.WorkArea != null && + wi.WorkArea.Floor != null && + wi.WorkArea.Floor.Building != null && + projectIdsToFetch.Contains(wi.WorkArea.Floor.Building.ProjectId)) + .GroupBy(wi => wi.WorkArea!.Floor!.Building!.ProjectId) + .Select(g => new { ProjectId = g.Key, PlannedWork = g.Sum(i => i.PlannedWork), CompletedWork = g.Sum(i => i.CompletedWork) }) + .ToDictionaryAsync(x => x.ProjectId); + }); + + // Await all parallel tasks to complete + await Task.WhenAll(projectsTask, teamSizesTask, workSummariesTask); + + var projects = await projectsTask; + var teamSizes = await teamSizesTask; + var workSummaries = await workSummariesTask; + + // Proactively update the cache with the items we just fetched. + _logger.LogInfo("Updating cache with {NewItemCount} newly fetched projects.", projects.Count); + await _cache.AddProjectDetailsList(projects); + + // This section would build the full ProjectMongoDB objects, similar to your AddProjectDetailsList method. + // For brevity, assuming you have a mapper or a builder for this. Here's a simplified representation: + var mongoDetailsList = new List(); + foreach (var project in projects) + { + // This is a placeholder for the full build logic from your other methods. + // In a real scenario, you would fetch all hierarchy levels (buildings, floors, etc.) + // for the `projectIdsToFetch` and build the complete MongoDB object. + var mongoDetail = _mapper.Map(project); + mongoDetail.Id = project.Id; + mongoDetail.TeamSize = teamSizes.GetValueOrDefault(project.Id, 0); + if (workSummaries.TryGetValue(project.Id, out var summary)) + { + mongoDetail.PlannedWork = summary.PlannedWork; + mongoDetail.CompletedWork = summary.CompletedWork; + } + mongoDetailsList.Add(mongoDetail); + } + + return mongoDetailsList; + } + + #endregion } } \ No newline at end of file diff --git a/Marco.Pms.Services/Controllers/UserController.cs b/Marco.Pms.Services/Controllers/UserController.cs index 2aeb208..4bb4432 100644 --- a/Marco.Pms.Services/Controllers/UserController.cs +++ b/Marco.Pms.Services/Controllers/UserController.cs @@ -50,7 +50,7 @@ namespace MarcoBMS.Services.Controllers emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id); } - List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(emp.Id); + List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeId(emp.Id); string[] projectsId = []; /* User with permission manage project can see all projects */ diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 589ab52..4369b5b 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -1,5 +1,6 @@ using Marco.Pms.CacheHelper; using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.Master; using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.Projects; using MarcoBMS.Services.Service; @@ -15,20 +16,20 @@ namespace Marco.Pms.Services.Helpers private readonly ReportCache _reportCache; private readonly ILoggingService _logger; private readonly IDbContextFactory _dbContextFactory; + private readonly ApplicationDbContext _context; public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache, ReportCache reportCache, ILoggingService logger, - IDbContextFactory dbContextFactory) + IDbContextFactory dbContextFactory, ApplicationDbContext context) { _projectCache = projectCache; _employeeCache = employeeCache; _reportCache = reportCache; _logger = logger; _dbContextFactory = dbContextFactory; + _context = context; } // ------------------------------------ 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) { @@ -417,9 +418,11 @@ namespace Marco.Pms.Services.Helpers } public async Task UpdateProjectDetailsOnly(Project project) { + StatusMaster projectStatus = await _context.StatusMasters + .FirstOrDefaultAsync(s => s.Id == project.ProjectStatusId) ?? new StatusMaster(); try { - bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project); + bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project, projectStatus); return response; } catch (Exception ex) @@ -457,10 +460,22 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while getting list od project details from to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while getting list of project details from to Cache: {Error}", ex.Message); return null; } } + public async Task DeleteProjectByIdAsync(Guid projectId) + { + try + { + var response = await _projectCache.DeleteProjectByIdFromCacheAsync(projectId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting project from to Cache: {Error}", ex.Message); + + } + } // ------------------------------------ Project Infrastructure Cache --------------------------------------- @@ -527,6 +542,9 @@ namespace Marco.Pms.Services.Helpers return null; } } + + // ------------------------------------------------------- WorkItem ------------------------------------------------------- + public async Task?> GetWorkItemsByWorkAreaIds(List workAreaIds) { try @@ -544,9 +562,6 @@ namespace Marco.Pms.Services.Helpers return null; } } - - // ------------------------------------------------------- WorkItem ------------------------------------------------------- - public async Task ManageWorkItemDetails(List workItems) { try @@ -609,6 +624,18 @@ namespace Marco.Pms.Services.Helpers _logger.LogWarning("Error occured while updating planned work, completed work, and today's assigned work in workItems in Cache: {Error}", ex.Message); } } + public async Task DeleteWorkItemByIdAsync(Guid workItemId) + { + try + { + var response = await _projectCache.DeleteWorkItemByIdFromCacheAsync(workItemId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting work item from to Cache: {Error}", ex.Message); + + } + } // ------------------------------------ Employee Profile Cache --------------------------------------- diff --git a/Marco.Pms.Services/Helpers/ProjectsHelper.cs b/Marco.Pms.Services/Helpers/ProjectsHelper.cs index fb5b6f2..6c1cab1 100644 --- a/Marco.Pms.Services/Helpers/ProjectsHelper.cs +++ b/Marco.Pms.Services/Helpers/ProjectsHelper.cs @@ -58,7 +58,7 @@ namespace MarcoBMS.Services.Helpers if (projectIds == null) { - var hasPermission = await _permission.HasPermission(LoggedInEmployee.Id, PermissionsMaster.ManageProject); + var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageProject, LoggedInEmployee.Id); if (hasPermission) { var projects = await _context.Projects.Where(c => c.TenantId == tenantId).ToListAsync(); diff --git a/Marco.Pms.Services/Helpers/RolesHelper.cs b/Marco.Pms.Services/Helpers/RolesHelper.cs index 15bf0b1..1688dce 100644 --- a/Marco.Pms.Services/Helpers/RolesHelper.cs +++ b/Marco.Pms.Services/Helpers/RolesHelper.cs @@ -3,6 +3,7 @@ using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Entitlements; using Marco.Pms.Services.Helpers; +using MarcoBMS.Services.Service; using Microsoft.EntityFrameworkCore; namespace MarcoBMS.Services.Helpers @@ -11,33 +12,81 @@ namespace MarcoBMS.Services.Helpers { private readonly ApplicationDbContext _context; private readonly CacheUpdateHelper _cache; - public RolesHelper(ApplicationDbContext context, CacheUpdateHelper cache) + private readonly ILoggingService _logger; + public RolesHelper(ApplicationDbContext context, CacheUpdateHelper cache, ILoggingService logger) { _context = context; _cache = cache; + _logger = logger; } - public async Task> GetFeaturePermissionByEmployeeID(Guid EmployeeID) + /// + /// Retrieves a unique list of enabled feature permissions for a given employee. + /// This method is optimized to use a single, composed database query. + /// + /// The ID of the employee. + /// A distinct list of FeaturePermission objects the employee is granted. + public async Task> GetFeaturePermissionByEmployeeId(Guid EmployeeId) { - List roleMappings = await _context.EmployeeRoleMappings.Where(c => c.EmployeeId == EmployeeID && c.IsEnabled == true).Select(c => c.RoleId).ToListAsync(); + _logger.LogInfo("Fetching feature permissions for EmployeeId: {EmployeeId}", EmployeeId); - await _cache.AddApplicationRole(EmployeeID, roleMappings); + try + { + // --- Step 1: Define the subquery for the employee's roles --- + // This is an IQueryable, not a list. It will be composed directly into the main query + // by Entity Framework, avoiding a separate database call. + var employeeRoleIdsQuery = _context.EmployeeRoleMappings + .Where(erm => erm.EmployeeId == EmployeeId && erm.IsEnabled == true) + .Select(erm => erm.RoleId); - // _context.RolePermissionMappings + // --- Step 2: Asynchronously update the cache in the background (Fire and Forget) --- + // This task is started but not awaited. The main function continues immediately, + // reducing latency. The cache will be updated eventually without blocking the user. + _ = Task.Run(async () => + { + try + { + var roleIds = await employeeRoleIdsQuery.ToListAsync(); // Execute the query for the cache + if (roleIds.Any()) + { + await _cache.AddApplicationRole(EmployeeId, roleIds); + _logger.LogInfo("Successfully queued cache update for EmployeeId: {EmployeeId}", EmployeeId); + } + } + catch (Exception ex) + { + // Log errors from the background task so they are not lost. + _logger.LogWarning("Background cache update failed for EmployeeId {EmployeeId} : {Error}", EmployeeId, ex.Message); + } + }); - var result = await (from rpm in _context.RolePermissionMappings - join fp in _context.FeaturePermissions.Where(c => c.IsEnabled == true).Include(fp => fp.Feature) // Include Feature - on rpm.FeaturePermissionId equals fp.Id - where roleMappings.Contains(rpm.ApplicationRoleId) - select fp) - .ToListAsync(); + // --- Step 3: Execute the main query to get permissions in a single database call --- + // This single, efficient query gets all the required data at once. + var permissions = await ( + from rpm in _context.RolePermissionMappings + join fp in _context.FeaturePermissions.Include(f => f.Feature) // Include related Feature data + on rpm.FeaturePermissionId equals fp.Id + // The 'employeeRoleIdsQuery' subquery is seamlessly integrated here by EF Core, + // resulting in a SQL "IN (SELECT ...)" clause. + where employeeRoleIdsQuery.Contains(rpm.ApplicationRoleId) && fp.IsEnabled == true + select fp) + .Distinct() // Ensures each permission is returned only once + .ToListAsync(); - return result; + _logger.LogInfo("Successfully retrieved {PermissionCount} unique permissions for EmployeeId: {EmployeeId}", permissions.Count, EmployeeId); - // return null; + return permissions; + } + catch (Exception ex) + { + _logger.LogError("An error occurred while fetching permissions for EmployeeId {EmployeeId} :{Error}", EmployeeId, ex.Message); + // Depending on your application's error handling strategy, you might re-throw, + // or return an empty list to prevent downstream failures. + return new List(); + } } - public async Task> GetFeaturePermissionByRoleID(Guid roleId) + public async Task> GetFeaturePermissionByRoleID1(Guid roleId) { List roleMappings = await _context.RolePermissionMappings.Where(c => c.ApplicationRoleId == roleId).Select(c => c.ApplicationRoleId).ToListAsync(); @@ -54,5 +103,49 @@ namespace MarcoBMS.Services.Helpers // return null; } + /// + /// Retrieves a unique list of enabled feature permissions for a given role. + /// This method is optimized to fetch all data in a single, efficient database query. + /// + /// The ID of the role. + /// A distinct list of FeaturePermission objects granted to the role. + public async Task> GetFeaturePermissionByRoleID(Guid roleId) + { + _logger.LogInfo("Fetching feature permissions for RoleID: {RoleId}", roleId); + + try + { + // This single, efficient query gets all the required data at once. + // It joins the mapping table to the permissions table and filters by the given roleId. + var permissions = await ( + // 1. Start with the linking table. + from rpm in _context.RolePermissionMappings + + // 2. Join to the FeaturePermissions table on the foreign key. + join fp in _context.FeaturePermissions on rpm.FeaturePermissionId equals fp.Id + + // 3. Apply all filters in one 'where' clause for clarity and efficiency. + where + rpm.ApplicationRoleId == roleId // Filter by the specific role + && fp.IsEnabled == true // And only get enabled permissions + + // 4. Select the final FeaturePermission object. + select fp) + .Include(fp => fp.Feature) + .Distinct() + .ToListAsync(); + + _logger.LogInfo("Successfully retrieved {PermissionCount} unique permissions for RoleID: {RoleId}", permissions.Count, roleId); + + return permissions; + } + catch (Exception ex) + { + _logger.LogError("An error occurred while fetching permissions for RoleId {RoleId}: {Error}", roleId, ex.Message); + // Return an empty list as a safe default to prevent downstream failures. + return new List(); + } + } + } } diff --git a/Marco.Pms.Services/Service/PermissionServices.cs b/Marco.Pms.Services/Service/PermissionServices.cs index 7162dc5..f20a768 100644 --- a/Marco.Pms.Services/Service/PermissionServices.cs +++ b/Marco.Pms.Services/Service/PermissionServices.cs @@ -24,7 +24,7 @@ namespace Marco.Pms.Services.Service var featurePermissionIds = await _cache.GetPermissions(employeeId); if (featurePermissionIds == null) { - List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(employeeId); + List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeId(employeeId); featurePermissionIds = featurePermission.Select(fp => fp.Id).ToList(); } var hasPermission = featurePermissionIds.Contains(featurePermissionId); From 0c84bb11a3f3bfec39e7a31446cc9fca2f528b65 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 14 Jul 2025 15:08:31 +0530 Subject: [PATCH 066/124] Solved Concurrency Issue --- Marco.Pms.CacheHelper/EmployeeCache.cs | 19 +------- .../Helpers/CacheUpdateHelper.cs | 23 +++++++++- Marco.Pms.Services/Helpers/RolesHelper.cs | 43 ++++++++++--------- 3 files changed, 46 insertions(+), 39 deletions(-) diff --git a/Marco.Pms.CacheHelper/EmployeeCache.cs b/Marco.Pms.CacheHelper/EmployeeCache.cs index c2a1f7b..4a668f0 100644 --- a/Marco.Pms.CacheHelper/EmployeeCache.cs +++ b/Marco.Pms.CacheHelper/EmployeeCache.cs @@ -20,29 +20,12 @@ namespace Marco.Pms.CacheHelper var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name _collection = mongoDB.GetCollection("EmployeeProfile"); } - public async Task AddApplicationRoleToCache(Guid employeeId, List roleIds) + public async Task AddApplicationRoleToCache(Guid employeeId, List newRoleIds, List newPermissionIds) { - // 1. Guard Clause: Avoid unnecessary database work if there are no roles to add. - if (roleIds == null || !roleIds.Any()) - { - return false; // Nothing to add, so the operation did not result in a change. - } // 2. Perform database queries concurrently for better performance. var employeeIdString = employeeId.ToString(); - Task> getPermissionIdsTask = _context.RolePermissionMappings - .Where(rp => roleIds.Contains(rp.ApplicationRoleId)) - .Select(p => p.FeaturePermissionId.ToString()) - .Distinct() - .ToListAsync(); - - // 3. Prepare role IDs in parallel with the database query. - var newRoleIds = roleIds.Select(r => r.ToString()).ToList(); - - // 4. Await the database query result. - var newPermissionIds = await getPermissionIdsTask; - // 5. Build a single, efficient update operation. var filter = Builders.Filter.Eq(e => e.Id, employeeIdString); diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 4369b5b..5bae90f 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -641,9 +641,30 @@ namespace Marco.Pms.Services.Helpers // ------------------------------------ Employee Profile Cache --------------------------------------- public async Task AddApplicationRole(Guid employeeId, List roleIds) { + // 1. Guard Clause: Avoid unnecessary database work if there are no roles to add. + if (roleIds == null || !roleIds.Any()) + { + return; // Nothing to add, so the operation did not result in a change. + } + Task> getPermissionIdsTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + + return await context.RolePermissionMappings + .Where(rp => roleIds.Contains(rp.ApplicationRoleId)) + .Select(p => p.FeaturePermissionId.ToString()) + .Distinct() + .ToListAsync(); + }); + + // 3. Prepare role IDs in parallel with the database query. + var newRoleIds = roleIds.Select(r => r.ToString()).ToList(); + + // 4. Await the database query result. + var newPermissionIds = await getPermissionIdsTask; try { - var response = await _employeeCache.AddApplicationRoleToCache(employeeId, roleIds); + var response = await _employeeCache.AddApplicationRoleToCache(employeeId, newRoleIds, newPermissionIds); } catch (Exception ex) { diff --git a/Marco.Pms.Services/Helpers/RolesHelper.cs b/Marco.Pms.Services/Helpers/RolesHelper.cs index 1688dce..cd73c0f 100644 --- a/Marco.Pms.Services/Helpers/RolesHelper.cs +++ b/Marco.Pms.Services/Helpers/RolesHelper.cs @@ -10,14 +10,16 @@ namespace MarcoBMS.Services.Helpers { public class RolesHelper { + private readonly IDbContextFactory _dbContextFactory; private readonly ApplicationDbContext _context; private readonly CacheUpdateHelper _cache; private readonly ILoggingService _logger; - public RolesHelper(ApplicationDbContext context, CacheUpdateHelper cache, ILoggingService logger) + public RolesHelper(ApplicationDbContext context, CacheUpdateHelper cache, ILoggingService logger, IDbContextFactory dbContextFactory) { _context = context; _cache = cache; _logger = logger; + _dbContextFactory = dbContextFactory; } /// @@ -32,56 +34,57 @@ namespace MarcoBMS.Services.Helpers try { - // --- Step 1: Define the subquery for the employee's roles --- - // This is an IQueryable, not a list. It will be composed directly into the main query - // by Entity Framework, avoiding a separate database call. + // --- Step 1: Define the subquery using the main thread's context --- + // This is safe because the query is not executed yet. var employeeRoleIdsQuery = _context.EmployeeRoleMappings - .Where(erm => erm.EmployeeId == EmployeeId && erm.IsEnabled == true) + .Where(erm => erm.EmployeeId == EmployeeId && erm.IsEnabled) .Select(erm => erm.RoleId); - // --- Step 2: Asynchronously update the cache in the background (Fire and Forget) --- - // This task is started but not awaited. The main function continues immediately, - // reducing latency. The cache will be updated eventually without blocking the user. + // --- Step 2: Asynchronously update the cache using the DbContextFactory --- _ = Task.Run(async () => { try { - var roleIds = await employeeRoleIdsQuery.ToListAsync(); // Execute the query for the cache + // Create a NEW, short-lived DbContext instance for this background task. + await using var contextForCache = await _dbContextFactory.CreateDbContextAsync(); + + // Now, re-create and execute the query using this new, isolated context. + var roleIds = await contextForCache.EmployeeRoleMappings + .Where(erm => erm.EmployeeId == EmployeeId && erm.IsEnabled) + .Select(erm => erm.RoleId) + .ToListAsync(); + if (roleIds.Any()) { + // The cache service might also need its own context, or you can pass the data directly. + // Assuming AddApplicationRole takes the data, not a context. await _cache.AddApplicationRole(EmployeeId, roleIds); _logger.LogInfo("Successfully queued cache update for EmployeeId: {EmployeeId}", EmployeeId); } } catch (Exception ex) { - // Log errors from the background task so they are not lost. _logger.LogWarning("Background cache update failed for EmployeeId {EmployeeId} : {Error}", EmployeeId, ex.Message); } }); - // --- Step 3: Execute the main query to get permissions in a single database call --- - // This single, efficient query gets all the required data at once. + // --- Step 3: Execute the main query on the main thread using its original context --- + // This is now safe because the background task is using a different DbContext instance. var permissions = await ( from rpm in _context.RolePermissionMappings - join fp in _context.FeaturePermissions.Include(f => f.Feature) // Include related Feature data + join fp in _context.FeaturePermissions.Include(f => f.Feature) on rpm.FeaturePermissionId equals fp.Id - // The 'employeeRoleIdsQuery' subquery is seamlessly integrated here by EF Core, - // resulting in a SQL "IN (SELECT ...)" clause. where employeeRoleIdsQuery.Contains(rpm.ApplicationRoleId) && fp.IsEnabled == true select fp) - .Distinct() // Ensures each permission is returned only once + .Distinct() .ToListAsync(); _logger.LogInfo("Successfully retrieved {PermissionCount} unique permissions for EmployeeId: {EmployeeId}", permissions.Count, EmployeeId); - return permissions; } catch (Exception ex) { - _logger.LogError("An error occurred while fetching permissions for EmployeeId {EmployeeId} :{Error}", EmployeeId, ex.Message); - // Depending on your application's error handling strategy, you might re-throw, - // or return an empty list to prevent downstream failures. + _logger.LogError("An error occurred while fetching permissions for EmployeeId {EmployeeId} : {Error}", EmployeeId, ex.Message); return new List(); } } From c5d9beec04403afda3c809138351dfb117c9f8b7 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 14 Jul 2025 15:57:52 +0530 Subject: [PATCH 067/124] Optimized the Get project By ID API --- .../MongoDBModels/StatusMasterMongoDB.cs | 2 +- .../Controllers/ProjectController.cs | 117 +++++++++++++++--- .../MappingProfiles/ProjectMappingProfile.cs | 13 +- 3 files changed, 116 insertions(+), 16 deletions(-) diff --git a/Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs index 01a0552..77e8eb5 100644 --- a/Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs @@ -2,7 +2,7 @@ { public class StatusMasterMongoDB { - public string? Id { get; set; } + public string Id { get; set; } = string.Empty; public string? Status { get; set; } } } diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index adb5887..acc97d2 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -40,8 +40,8 @@ namespace MarcoBMS.Services.Controllers private readonly Guid tenantId; - public ProjectController(IDbContextFactory dbContextFactory, ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, - ProjectsHelper projectHelper, IHubContext signalR, PermissionServices permission, CacheUpdateHelper cache, IMapper mapper) + public ProjectController(IDbContextFactory dbContextFactory, ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, + ProjectsHelper projectHelper, IHubContext signalR, CacheUpdateHelper cache, PermissionServices permission, IMapper mapper) { _dbContextFactory = dbContextFactory; _context = context; @@ -52,7 +52,7 @@ namespace MarcoBMS.Services.Controllers _cache = cache; _permission = permission; _mapper = mapper; - tenantId = _userHelper.GetTenantId(); + tenantId = userHelper.GetTenantId(); } #region =================================================================== Project Get APIs =================================================================== @@ -161,29 +161,74 @@ namespace MarcoBMS.Services.Controllers catch (Exception ex) { // --- Step 6: Graceful Error Handling --- - _logger.LogError("An unexpected error occurred in GetAllProjects for tenant {TenantId}. : {Error}", tenantId, ex.Message); + _logger.LogError("An unexpected error occurred in GetAllProjects for tenant {TenantId}. \n {Error}", tenantId, ex.Message); return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred. Please try again later.", null, 500)); } } + /// + /// Retrieves details for a specific project by its ID. + /// This endpoint is optimized with a cache-first strategy and parallel permission checks. + /// + /// The unique identifier of the project. + /// An ApiResponse containing the project details or an appropriate error. + [HttpGet("get/{id}")] public async Task Get([FromRoute] Guid id) { + // --- Step 1: Input Validation --- if (!ModelState.IsValid) { - var errors = ModelState.Values - .SelectMany(v => v.Errors) - .Select(e => e.ErrorMessage) - .ToList(); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); - + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("Get project called with invalid model state for ID {ProjectId}. Errors: {Errors}", id, string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - var project = await _context.Projects.Where(c => c.TenantId == _userHelper.GetTenantId() && c.Id == id).SingleOrDefaultAsync(); - if (project == null) return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); - return Ok(ApiResponse.SuccessResponse(project, "Success.", 200)); + try + { + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // --- Step 2: Run independent operations in PARALLEL --- + // We can check permissions and fetch data at the same time to reduce latency. + var permissionTask = _permission.HasProjectPermission(loggedInEmployee, id); + + // This helper method encapsulates the "cache-first, then database" logic. + var projectDataTask = GetProjectDataAsync(id); + + // Await both tasks to complete. + await Task.WhenAll(permissionTask, projectDataTask); + + var hasPermission = await permissionTask; + var projectVm = await projectDataTask; + + // --- Step 3: Process results sequentially --- + + // 3a. Check for permission first. Forbid() is the idiomatic way to return 403. + if (!hasPermission) + { + _logger.LogWarning("Access denied for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, id); + return StatusCode(403, (ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to access this project.", 403))); + } + + // 3b. Check if the project was found (either in cache or DB). + if (projectVm == null) + { + _logger.LogInfo("Project with ID {ProjectId} not found.", id); + return NotFound(ApiResponse.ErrorResponse("Project not found.", $"No project found with ID {id}.", 404)); + } + + // 3c. Success. Return the consistent ViewModel. + _logger.LogInfo("Successfully retrieved project {ProjectId}.", id); + return Ok(ApiResponse.SuccessResponse(projectVm, "Project retrieved successfully.", 200)); + } + catch (Exception ex) + { + _logger.LogError("An unexpected error occurred while getting project {ProjectId} : \n {Error}", id, ex.Message); + return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred.", null, 500)); + } } + [HttpGet("details/{id}")] public async Task Details([FromRoute] Guid id) { @@ -1331,7 +1376,6 @@ namespace MarcoBMS.Services.Controllers return vm; } - /// /// Fetches project details from the database for a given list of project IDs and assembles them into MongoDB models. /// This method encapsulates the optimized, parallel database queries. @@ -1409,6 +1453,51 @@ namespace MarcoBMS.Services.Controllers return mongoDetailsList; } + /// + /// Private helper to encapsulate the cache-first data retrieval logic. + /// + /// A ProjectDetailVM if found, otherwise null. + private async Task GetProjectDataAsync(Guid projectId) + { + // --- Cache First --- + _logger.LogDebug("Attempting to fetch project {ProjectId} from cache.", projectId); + var cachedProject = await _cache.GetProjectDetails(projectId); + if (cachedProject != null) + { + _logger.LogInfo("Cache HIT for project {ProjectId}.", projectId); + // Map from the cache model (e.g., ProjectMongoDB) to the response ViewModel. + return _mapper.Map(cachedProject); + } + + // --- Database Second (on Cache Miss) --- + _logger.LogInfo("Cache MISS for project {ProjectId}. Fetching from database.", projectId); + var dbProject = await _context.Projects + .AsNoTracking() // Use AsNoTracking for read-only queries. + .Where(p => p.Id == projectId && p.TenantId == tenantId) + .SingleOrDefaultAsync(); + + if (dbProject == null) + { + return null; // The project doesn't exist. + } + + // --- Proactively Update Cache --- + // The next request for this project will now be a cache hit. + try + { + // Map the DB entity to the cache model (e.g., ProjectMongoDB) before caching. + await _cache.AddProjectDetails(dbProject); + _logger.LogInfo("Updated cache with project {ProjectId}.", projectId); + } + catch (Exception ex) + { + _logger.LogWarning("Failed to update cache for project {ProjectId} : \n {Error}", projectId, ex.Message); + } + + // Map from the database entity to the response ViewModel. + return dbProject; + } + #endregion } } \ No newline at end of file diff --git a/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs b/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs index c7ec4af..f527f67 100644 --- a/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs @@ -20,7 +20,18 @@ namespace Marco.Pms.Services.MappingProfiles .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)) + opt => opt.MapFrom(src => new Guid(src.Id)) + ); + + CreateMap() + .ForMember( + dest => dest.Id, + // Explicitly and safely convert string Id to Guid Id + opt => opt.MapFrom(src => new Guid(src.Id)) + ).ForMember( + dest => dest.ProjectStatusId, + // Explicitly and safely convert string ProjectStatusId to Guid ProjectStatusId + opt => opt.MapFrom(src => src.ProjectStatus == null ? Guid.Empty : new Guid(src.ProjectStatus.Id)) ); CreateMap(); From e769c161f47b8dc8c0ae10e17a7656ba1906e470 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 14 Jul 2025 17:00:28 +0530 Subject: [PATCH 068/124] Optimized the Update project API --- .../Controllers/ProjectController.cs | 168 ++++++++++++++---- Marco.Pms.Services/Helpers/ProjectsHelper.cs | 6 +- .../MappingProfiles/ProjectMappingProfile.cs | 3 + 3 files changed, 142 insertions(+), 35 deletions(-) diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index acc97d2..3d5558f 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -70,7 +70,6 @@ namespace MarcoBMS.Services.Controllers _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()) @@ -316,7 +315,7 @@ namespace MarcoBMS.Services.Controllers } - var project = await _context.Projects.Where(c => c.TenantId == _userHelper.GetTenantId() && c.Id == id).Include(c => c.ProjectStatus).SingleOrDefaultAsync(); // includeProperties: "ProjectStatus,Tenant"); //_context.Stock.FindAsync(id); + var project = await _context.Projects.Where(c => c.TenantId == tenantId && c.Id == id).Include(c => c.ProjectStatus).SingleOrDefaultAsync(); // includeProperties: "ProjectStatus,Tenant"); //_context.Stock.FindAsync(id); if (project == null) { @@ -420,7 +419,6 @@ namespace MarcoBMS.Services.Controllers } // 2. Prepare data without I/O - Guid tenantId = _userHelper.GetTenantId(); // Assuming this is fast and from claims Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInUserId = loggedInEmployee.Id; var project = projectDto.ToProjectFromCreateProjectDto(tenantId); @@ -465,7 +463,7 @@ namespace MarcoBMS.Services.Controllers } [HttpPut] - [Route("update/{id}")] + [Route("update1/{id}")] public async Task Update([FromRoute] Guid id, [FromBody] UpdateProjectDto updateProjectDto) { var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); @@ -480,9 +478,7 @@ namespace MarcoBMS.Services.Controllers } try { - Guid TenantId = GetTenantId(); - - Project project = updateProjectDto.ToProjectFromUpdateProjectDto(TenantId, id); + Project project = updateProjectDto.ToProjectFromUpdateProjectDto(tenantId, id); _context.Projects.Update(project); await _context.SaveChangesAsync(); @@ -507,6 +503,97 @@ namespace MarcoBMS.Services.Controllers } } + /// + /// Updates an existing project's details. + /// This endpoint is secure, handles concurrency, and performs non-essential tasks in the background. + /// + /// The ID of the project to update. + /// The data to update the project with. + /// An ApiResponse confirming the update or an appropriate error. + + [HttpPut("update/{id}")] + public async Task UpdateProject([FromRoute] Guid id, [FromBody] UpdateProjectDto updateProjectDto) + { + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) + { + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("Update project called with invalid model state for ID {ProjectId}. Errors: {Errors}", id, string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); + } + + try + { + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + // --- Step 2: Fetch the Existing Entity from the Database --- + // This is crucial to avoid the data loss bug. We only want to modify an existing record. + var existingProject = await _context.Projects + .Where(p => p.Id == id && p.TenantId == tenantId) + .SingleOrDefaultAsync(); + + // 2a. Existence Check + if (existingProject == null) + { + _logger.LogWarning("Attempt to update non-existent project with ID {ProjectId} by user {UserId}.", id, loggedInEmployee.Id); + return NotFound(ApiResponse.ErrorResponse("Project not found.", $"No project found with ID {id}.", 404)); + } + + // 2b. Security Check + var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, id); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} attempting to update project {ProjectId}.", loggedInEmployee.Id, id); + return StatusCode(403, (ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to modify this project.", 403))); + } + + // --- Step 3: Apply Changes and Save --- + // Map the changes from the DTO onto the entity we just fetched from the database. + // This only modifies the properties defined in the mapping, preventing data loss. + _mapper.Map(updateProjectDto, existingProject); + + // Mark the entity as modified (if your mapping doesn't do it automatically). + _context.Entry(existingProject).State = EntityState.Modified; + + try + { + await _context.SaveChangesAsync(); + _logger.LogInfo("Successfully updated project {ProjectId} by user {UserId}.", id, loggedInEmployee.Id); + } + catch (DbUpdateConcurrencyException ex) + { + // --- Step 4: Handle Concurrency Conflicts --- + // This happens if another user modified the project after we fetched it. + _logger.LogWarning("Concurrency conflict while updating project {ProjectId} \n {Error}", id, ex.Message); + return Conflict(ApiResponse.ErrorResponse("Conflict occurred.", "This project has been modified by someone else. Please refresh and try again.", 409)); + } + + // --- Step 5: Perform Side-Effects in the Background (Fire and Forget) --- + // The core database operation is done. Now, we perform non-blocking cache and notification updates. + _ = Task.Run(async () => + { + // Create a DTO of the updated project to pass to background tasks. + var projectDto = _mapper.Map(existingProject); + + // 5a. Update Cache + await UpdateCacheInBackground(existingProject); + + // 5b. Send Targeted SignalR Notification + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Update_Project", Response = projectDto }; + await SendNotificationInBackground(notification, projectDto.Id); + }); + + // --- Step 6: Return Success Response Immediately --- + // The client gets a fast response without waiting for caching or SignalR. + return Ok(ApiResponse.SuccessResponse(_mapper.Map(existingProject), "Project updated successfully.", 200)); + } + catch (Exception ex) + { + // --- Step 7: Graceful Error Handling for Unexpected Errors --- + _logger.LogError("An unexpected error occurred while updating project {ProjectId} \n {Error}", id, ex.Message); + return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred.", null, 500)); + } + } + #endregion #region =================================================================== Project Allocation APIs =================================================================== @@ -524,7 +611,6 @@ namespace MarcoBMS.Services.Controllers return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } - Guid TenantId = GetTenantId(); if (projectid != null) { @@ -535,14 +621,14 @@ namespace MarcoBMS.Services.Controllers { result = await (from rpm in _context.Employees.Include(c => c.JobRole) - join fp in _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == projectid) + join fp in _context.ProjectAllocations.Where(c => c.TenantId == tenantId && c.ProjectId == projectid) on rpm.Id equals fp.EmployeeId select rpm).ToListAsync(); } else { result = await (from rpm in _context.Employees.Include(c => c.JobRole) - join fp in _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == projectid && c.IsActive == true) + join fp in _context.ProjectAllocations.Where(c => c.TenantId == tenantId && c.ProjectId == projectid && c.IsActive) on rpm.Id equals fp.EmployeeId select rpm).ToListAsync(); } @@ -577,11 +663,9 @@ namespace MarcoBMS.Services.Controllers return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } - Guid TenantId = GetTenantId(); - var employees = await _context.ProjectAllocations - .Where(c => c.TenantId == TenantId && c.ProjectId == projectId && c.Employee != null) + .Where(c => c.TenantId == tenantId && c.ProjectId == projectId && c.Employee != null) .Include(e => e.Employee) .Select(e => new { @@ -605,7 +689,6 @@ namespace MarcoBMS.Services.Controllers { if (projectAllocationDot != null) { - Guid TenentID = GetTenantId(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); List? result = new List(); @@ -616,11 +699,11 @@ namespace MarcoBMS.Services.Controllers { try { - ProjectAllocation projectAllocation = item.ToProjectAllocationFromProjectAllocationDto(TenentID); + ProjectAllocation projectAllocation = item.ToProjectAllocationFromProjectAllocationDto(tenantId); ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == projectAllocation.EmployeeId && c.ProjectId == projectAllocation.ProjectId && c.ReAllocationDate == null - && c.TenantId == TenentID).SingleOrDefaultAsync(); + && c.TenantId == tenantId).SingleOrDefaultAsync(); if (projectAllocationFromDb != null) { @@ -688,8 +771,6 @@ namespace MarcoBMS.Services.Controllers [HttpGet("assigned-projects/{employeeId}")] public async Task GetProjectsByEmployee([FromRoute] Guid employeeId) { - - Guid tenantId = _userHelper.GetTenantId(); if (employeeId == Guid.Empty) { return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Employee id not valid.", 400)); @@ -729,7 +810,6 @@ namespace MarcoBMS.Services.Controllers { if (projectAllocationDtos != null && employeeId != Guid.Empty) { - Guid TenentID = GetTenantId(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); List? result = new List(); List projectIds = new List(); @@ -738,8 +818,8 @@ namespace MarcoBMS.Services.Controllers { try { - ProjectAllocation projectAllocation = projectAllocationDto.ToProjectAllocationFromProjectsAllocationDto(TenentID, employeeId); - ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.ProjectId == projectAllocationDto.ProjectId && c.ReAllocationDate == null && c.TenantId == TenentID).SingleOrDefaultAsync(); + ProjectAllocation projectAllocation = projectAllocationDto.ToProjectAllocationFromProjectsAllocationDto(tenantId, employeeId); + ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.ProjectId == projectAllocationDto.ProjectId && c.ReAllocationDate == null && c.TenantId == tenantId).SingleOrDefaultAsync(); if (projectAllocationFromDb != null) { @@ -1017,7 +1097,6 @@ namespace MarcoBMS.Services.Controllers return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Work Item details are not valid.", 400)); } - Guid tenantId = GetTenantId(); var workItemsToCreate = new List(); var workItemsToUpdate = new List(); var responseList = new List(); @@ -1113,7 +1192,6 @@ namespace MarcoBMS.Services.Controllers [HttpDelete("task/{id}")] public async Task DeleteProjectTask(Guid id) { - Guid tenantId = _userHelper.GetTenantId(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); List workAreaIds = new List(); WorkItem? task = await _context.WorkItems.AsNoTracking().Include(t => t.WorkArea).FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId); @@ -1162,7 +1240,6 @@ namespace MarcoBMS.Services.Controllers [HttpPost("manage-infra")] public async Task ManageProjectInfra(List infraDots) { - Guid tenantId = GetTenantId(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var responseData = new InfraVM { }; @@ -1177,7 +1254,7 @@ namespace MarcoBMS.Services.Controllers { Building building = item.Building.ToBuildingFromBuildingDto(tenantId); - building.TenantId = GetTenantId(); + building.TenantId = tenantId; if (item.Building.Id == null) { @@ -1204,7 +1281,7 @@ namespace MarcoBMS.Services.Controllers if (item.Floor != null) { Floor floor = item.Floor.ToFloorFromFloorDto(tenantId); - floor.TenantId = GetTenantId(); + floor.TenantId = tenantId; bool isCreated = false; if (item.Floor.Id == null) @@ -1242,7 +1319,7 @@ namespace MarcoBMS.Services.Controllers if (item.WorkArea != null) { WorkArea workArea = item.WorkArea.ToWorkAreaFromWorkAreaDto(tenantId); - workArea.TenantId = GetTenantId(); + workArea.TenantId = tenantId; bool isCreated = false; if (item.WorkArea.Id == null) @@ -1343,11 +1420,6 @@ namespace MarcoBMS.Services.Controllers return finalViewModels; } - private Guid GetTenantId() - { - return _userHelper.GetTenantId(); - } - private async Task GetProjectViewModel(Guid? id, Project project) { ProjectDetailsVM vm = new ProjectDetailsVM(); @@ -1498,6 +1570,38 @@ namespace MarcoBMS.Services.Controllers return dbProject; } + // Helper method for background cache update + private async Task UpdateCacheInBackground(Project project) + { + try + { + // This logic can be more complex, but the idea is to update or add. + if (!await _cache.UpdateProjectDetailsOnly(project)) + { + await _cache.AddProjectDetails(project); + } + _logger.LogInfo("Background cache update succeeded for project {ProjectId}.", project.Id); + } + catch (Exception ex) + { + _logger.LogError("Background cache update failed for project {ProjectId} \n {Error}", project.Id, ex.Message); + } + } + + // Helper method for background notification + private async Task SendNotificationInBackground(object notification, Guid projectId) + { + try + { + await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + _logger.LogInfo("Background SignalR notification sent for project {ProjectId}.", projectId); + } + catch (Exception ex) + { + _logger.LogError("Background SignalR notification failed for project {ProjectId} \n {Error}", projectId, ex.Message); + } + } + #endregion } } \ No newline at end of file diff --git a/Marco.Pms.Services/Helpers/ProjectsHelper.cs b/Marco.Pms.Services/Helpers/ProjectsHelper.cs index 6c1cab1..fe70a0a 100644 --- a/Marco.Pms.Services/Helpers/ProjectsHelper.cs +++ b/Marco.Pms.Services/Helpers/ProjectsHelper.cs @@ -67,11 +67,11 @@ namespace MarcoBMS.Services.Helpers else { var allocation = await GetProjectByEmployeeID(LoggedInEmployee.Id); - if (allocation.Any()) + if (!allocation.Any()) { - projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); + return new List(); } - return new List(); + projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); } await _cache.AddProjects(LoggedInEmployee.Id, projectIds); } diff --git a/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs b/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs index f527f67..18db7ff 100644 --- a/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs @@ -1,4 +1,5 @@ using AutoMapper; +using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Master; using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.Projects; @@ -14,7 +15,9 @@ namespace Marco.Pms.Services.MappingProfiles CreateMap(); CreateMap(); CreateMap(); + CreateMap(); CreateMap(); + CreateMap(); CreateMap(); CreateMap() .ForMember( From 5de59f0292c639cd75827511b983a18451758ec2 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 14 Jul 2025 18:45:23 +0530 Subject: [PATCH 069/124] Refactored: Moved business logic from ProjectController to ProjectService --- .../Controllers/ProjectController.cs | 693 +----------------- .../MappingProfiles/ProjectMappingProfile.cs | 1 + Marco.Pms.Services/Program.cs | 10 + .../Service/PermissionServices.cs | 10 +- Marco.Pms.Services/Service/ProjectServices.cs | 691 +++++++++++++++++ .../ServiceInterfaces/IProjectServices.cs | 17 + 6 files changed, 760 insertions(+), 662 deletions(-) create mode 100644 Marco.Pms.Services/Service/ProjectServices.cs create mode 100644 Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 3d5558f..e7d257f 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -1,6 +1,4 @@ -using AutoMapper; -using Marco.Pms.DataAccess.Data; -using Marco.Pms.Model.Activities; +using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Entitlements; @@ -13,6 +11,7 @@ using Marco.Pms.Model.ViewModels.Projects; using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Service; +using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; @@ -28,30 +27,26 @@ namespace MarcoBMS.Services.Controllers [Authorize] public class ProjectController : ControllerBase { - private readonly IDbContextFactory _dbContextFactory; + private readonly IProjectServices _projectServices; private readonly ApplicationDbContext _context; private readonly UserHelper _userHelper; private readonly ILoggingService _logger; - private readonly ProjectsHelper _projectsHelper; private readonly IHubContext _signalR; private readonly PermissionServices _permission; private readonly CacheUpdateHelper _cache; - private readonly IMapper _mapper; private readonly Guid tenantId; - public ProjectController(IDbContextFactory dbContextFactory, ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, - ProjectsHelper projectHelper, IHubContext signalR, CacheUpdateHelper cache, PermissionServices permission, IMapper mapper) + public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, + IHubContext signalR, CacheUpdateHelper cache, PermissionServices permission, IProjectServices projectServices) { - _dbContextFactory = dbContextFactory; _context = context; _userHelper = userHelper; _logger = logger; - _projectsHelper = projectHelper; _signalR = signalR; _cache = cache; _permission = permission; - _mapper = mapper; + _projectServices = projectServices; tenantId = userHelper.GetTenantId(); } @@ -60,30 +55,10 @@ namespace MarcoBMS.Services.Controllers [HttpGet("list/basic")] public async Task GetAllProjectsBasic() { - // Step 1: Get the current user + // 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 - 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)); + var response = await _projectServices.GetAllProjectsBasicAsync(tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } /// @@ -96,7 +71,7 @@ namespace MarcoBMS.Services.Controllers [HttpGet("list")] public async Task GetAllProjects() { - // --- Step 1: Input Validation and Initial Setup --- + // --- Input Validation and Initial Setup --- if (!ModelState.IsValid) { var errors = ModelState.Values @@ -106,63 +81,9 @@ namespace MarcoBMS.Services.Controllers _logger.LogWarning("GetAllProjects called with invalid model state. Errors: {Errors}", string.Join(", ", errors)); return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - - try - { - var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - _logger.LogInfo("Starting GetAllProjects for TenantId: {TenantId}, User: {UserId}", tenantId, loggedInEmployee.Id); - - // --- Step 2: Get a list of project IDs the user can access --- - List projectIds = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); - if (!projectIds.Any()) - { - _logger.LogInfo("User has no assigned projects. Returning empty list."); - return Ok(ApiResponse>.SuccessResponse(new List(), "No projects found for the current user.", 200)); - } - - // --- Step 3: Efficiently handle partial cache hits --- - _logger.LogInfo("Attempting to fetch details for {ProjectCount} projects from cache.", projectIds.Count); - - // Fetch what we can from the cache. - var cachedDetails = await _cache.GetProjectDetailsList(projectIds) ?? new List(); - var cachedDictionary = cachedDetails.ToDictionary(p => Guid.Parse(p.Id)); - - // Identify which projects are missing from the cache. - var missingIds = projectIds.Where(id => !cachedDictionary.ContainsKey(id)).ToList(); - - // Start building the response with the items we found in the cache. - var responseVms = _mapper.Map>(cachedDictionary.Values); - - if (missingIds.Any()) - { - // --- Step 4: Fetch ONLY the missing items from the database --- - _logger.LogInfo("Cache partial MISS. Found {CachedCount}, fetching {MissingCount} projects from DB.", - cachedDictionary.Count, missingIds.Count); - - // Call our dedicated data-fetching method for the missing IDs. - var newMongoDetails = await FetchAndBuildProjectDetails(missingIds, tenantId); - - if (newMongoDetails.Any()) - { - // Map the newly fetched items and add them to our response list. - responseVms.AddRange(newMongoDetails); - } - } - else - { - _logger.LogInfo("Cache HIT. All {ProjectCount} projects found in cache.", projectIds.Count); - } - - // --- Step 5: Return the combined result --- - _logger.LogInfo("Successfully retrieved a total of {ProjectCount} projects.", responseVms.Count); - return Ok(ApiResponse>.SuccessResponse(responseVms, "Projects retrieved successfully.", 200)); - } - catch (Exception ex) - { - // --- Step 6: Graceful Error Handling --- - _logger.LogError("An unexpected error occurred in GetAllProjects for tenant {TenantId}. \n {Error}", tenantId, ex.Message); - return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred. Please try again later.", null, 500)); - } + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetAllProjectsAsync(tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } /// @@ -173,7 +94,7 @@ namespace MarcoBMS.Services.Controllers /// An ApiResponse containing the project details or an appropriate error. [HttpGet("get/{id}")] - public async Task Get([FromRoute] Guid id) + public async Task GetProject([FromRoute] Guid id) { // --- Step 1: Input Validation --- if (!ModelState.IsValid) @@ -183,53 +104,14 @@ namespace MarcoBMS.Services.Controllers return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - try - { - var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - // --- Step 2: Run independent operations in PARALLEL --- - // We can check permissions and fetch data at the same time to reduce latency. - var permissionTask = _permission.HasProjectPermission(loggedInEmployee, id); - - // This helper method encapsulates the "cache-first, then database" logic. - var projectDataTask = GetProjectDataAsync(id); - - // Await both tasks to complete. - await Task.WhenAll(permissionTask, projectDataTask); - - var hasPermission = await permissionTask; - var projectVm = await projectDataTask; - - // --- Step 3: Process results sequentially --- - - // 3a. Check for permission first. Forbid() is the idiomatic way to return 403. - if (!hasPermission) - { - _logger.LogWarning("Access denied for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, id); - return StatusCode(403, (ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to access this project.", 403))); - } - - // 3b. Check if the project was found (either in cache or DB). - if (projectVm == null) - { - _logger.LogInfo("Project with ID {ProjectId} not found.", id); - return NotFound(ApiResponse.ErrorResponse("Project not found.", $"No project found with ID {id}.", 404)); - } - - // 3c. Success. Return the consistent ViewModel. - _logger.LogInfo("Successfully retrieved project {ProjectId}.", id); - return Ok(ApiResponse.SuccessResponse(projectVm, "Project retrieved successfully.", 200)); - } - catch (Exception ex) - { - _logger.LogError("An unexpected error occurred while getting project {ProjectId} : \n {Error}", id, ex.Message); - return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred.", null, 500)); - } + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetProjectAsync(id, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } [HttpGet("details/{id}")] - public async Task Details([FromRoute] Guid id) + public async Task GetProjectDetails([FromRoute] Guid id) { // Step 1: Validate model state if (!ModelState.IsValid) @@ -245,63 +127,13 @@ namespace MarcoBMS.Services.Controllers // Step 2: Get logged-in employee var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - _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(PermissionsMaster.ViewProject, loggedInEmployee.Id); - if (!hasViewProjectPermission) - { - _logger.LogWarning("ViewProjects permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); - return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have permission to view projects", 403)); - } - - // Step 4: Check permission for this specific project - var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, id); - if (!hasProjectPermission) - { - _logger.LogWarning("Project-specific access denied. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployee.Id, id); - return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have access to this project", 403)); - } - - // Step 5: Fetch project with status - var projectDetails = await _cache.GetProjectDetails(id); - ProjectVM? projectVM = null; - if (projectDetails == null) - { - var project = await _context.Projects - .Include(c => c.ProjectStatus) - .FirstOrDefaultAsync(c => c.TenantId == tenantId && c.Id == id); - - projectVM = _mapper.Map(project); - - if (project != null) - { - await _cache.AddProjectDetails(project); - } - } - else - { - projectVM = _mapper.Map(projectDetails); - if (projectVM.ProjectStatus != null) - { - projectVM.ProjectStatus.TenantId = tenantId; - } - } - - if (projectVM == null) - { - _logger.LogWarning("Project not found. ProjectId: {ProjectId}", id); - return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); - } - - // Step 6: Return result - - _logger.LogInfo("Project details fetched successfully. ProjectId: {ProjectId}", id); - return Ok(ApiResponse.SuccessResponse(projectVM, "Project details fetched successfully", 200)); + var response = await _projectServices.GetProjectDetailsAsync(id, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } [HttpGet("details-old/{id}")] - public async Task DetailsOld([FromRoute] Guid id) + public async Task GetProjectDetailsOld([FromRoute] Guid id) { // ProjectDetailsVM vm = new ProjectDetailsVM(); @@ -315,92 +147,10 @@ namespace MarcoBMS.Services.Controllers } - var project = await _context.Projects.Where(c => c.TenantId == tenantId && c.Id == id).Include(c => c.ProjectStatus).SingleOrDefaultAsync(); // includeProperties: "ProjectStatus,Tenant"); //_context.Stock.FindAsync(id); - - if (project == null) - { - return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); - - } - else - { - //var project = projects.Where(c => c.Id == id).SingleOrDefault(); - ProjectDetailsVM vm = await GetProjectViewModel(id, project); - - OldProjectVM projectVM = new OldProjectVM(); - if (vm.project != null) - { - projectVM.Id = vm.project.Id; - projectVM.Name = vm.project.Name; - projectVM.ShortName = vm.project.ShortName; - projectVM.ProjectAddress = vm.project.ProjectAddress; - projectVM.ContactPerson = vm.project.ContactPerson; - projectVM.StartDate = vm.project.StartDate; - projectVM.EndDate = vm.project.EndDate; - projectVM.ProjectStatusId = vm.project.ProjectStatusId; - } - projectVM.Buildings = new List(); - if (vm.buildings != null) - { - foreach (Building build in vm.buildings) - { - BuildingVM buildVM = new BuildingVM() { Id = build.Id, Description = build.Description, Name = build.Name }; - buildVM.Floors = new List(); - if (vm.floors != null) - { - foreach (Floor floorDto in vm.floors.Where(c => c.BuildingId == build.Id).ToList()) - { - FloorsVM floorVM = new FloorsVM() { FloorName = floorDto.FloorName, Id = floorDto.Id }; - floorVM.WorkAreas = new List(); - - if (vm.workAreas != null) - { - foreach (WorkArea workAreaDto in vm.workAreas.Where(c => c.FloorId == floorVM.Id).ToList()) - { - WorkAreaVM workAreaVM = new WorkAreaVM() { Id = workAreaDto.Id, AreaName = workAreaDto.AreaName, WorkItems = new List() }; - - if (vm.workItems != null) - { - foreach (WorkItem workItemDto in vm.workItems.Where(c => c.WorkAreaId == workAreaDto.Id).ToList()) - { - WorkItemVM workItemVM = new WorkItemVM() { WorkItemId = workItemDto.Id, WorkItem = workItemDto }; - - workItemVM.WorkItem.WorkArea = new WorkArea(); - - if (workItemVM.WorkItem.ActivityMaster != null) - { - workItemVM.WorkItem.ActivityMaster.Tenant = new Tenant(); - } - workItemVM.WorkItem.Tenant = new Tenant(); - - double todaysAssigned = 0; - if (vm.Tasks != null) - { - var tasks = vm.Tasks.Where(t => t.WorkItemId == workItemDto.Id).ToList(); - foreach (TaskAllocation task in tasks) - { - todaysAssigned += task.PlannedTask; - } - } - workItemVM.TodaysAssigned = todaysAssigned; - - workAreaVM.WorkItems.Add(workItemVM); - } - } - - floorVM.WorkAreas.Add(workAreaVM); - } - } - - buildVM.Floors.Add(floorVM); - } - } - projectVM.Buildings.Add(buildVM); - } - } - return Ok(ApiResponse.SuccessResponse(projectVM, "Success.", 200)); - } + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetProjectDetailsAsync(id, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } @@ -409,7 +159,7 @@ namespace MarcoBMS.Services.Controllers #region =================================================================== Project Manage APIs =================================================================== [HttpPost] - public async Task Create([FromBody] CreateProjectDto projectDto) + public async Task CreateProject([FromBody] CreateProjectDto projectDto) { // 1. Validate input first (early exit) if (!ModelState.IsValid) @@ -420,87 +170,13 @@ namespace MarcoBMS.Services.Controllers // 2. Prepare data without I/O Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var loggedInUserId = loggedInEmployee.Id; - var project = projectDto.ToProjectFromCreateProjectDto(tenantId); - - // 3. Store it to database - try + var response = await _projectServices.CreateProjectAsync(projectDto, tenantId, loggedInEmployee); + if (response.Success) { - _context.Projects.Add(project); - await _context.SaveChangesAsync(); - } - catch (Exception ex) - { - // Log the detailed exception - _logger.LogError("Failed to create project in database. Rolling back transaction. : {Error}", ex.Message); - // Return a server error as the primary operation failed - return StatusCode(500, ApiResponse.ErrorResponse("An error occurred while saving the project.", ex.Message, 500)); - } - - // 4. Perform non-critical side-effects (caching, notifications) concurrently - try - { - // These operations do not depend on each other, so they can run in parallel. - Task cacheAddDetailsTask = _cache.AddProjectDetails(project); - 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) - Task notificationTask = _signalR.Clients.Group(tenantId.ToString()).SendAsync("NotificationEventHandler", notification); - - // Await all side-effect tasks to complete in parallel - await Task.WhenAll(cacheAddDetailsTask, cacheClearListTask, notificationTask); - } - catch (Exception ex) - { - // The project was created successfully, but a side-effect failed. - // Log this as a warning, as the primary operation succeeded. Don't return an error to the user. - _logger.LogWarning("Project {ProjectId} was created, but a post-creation side-effect (caching/notification) failed. : {Error}", project.Id, ex.Message); - } - - // 5. Return a success response to the user as soon as the critical data is saved. - return Ok(ApiResponse.SuccessResponse(project.ToProjectDto(), "Project created successfully.", 200)); - } - - [HttpPut] - [Route("update1/{id}")] - public async Task Update([FromRoute] Guid id, [FromBody] UpdateProjectDto updateProjectDto) - { - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - if (!ModelState.IsValid) - { - var errors = ModelState.Values - .SelectMany(v => v.Errors) - .Select(e => e.ErrorMessage) - .ToList(); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); - - } - try - { - Project project = updateProjectDto.ToProjectFromUpdateProjectDto(tenantId, id); - _context.Projects.Update(project); - - await _context.SaveChangesAsync(); - - // Cache functions - bool isUpdated = await _cache.UpdateProjectDetailsOnly(project); - if (!isUpdated) - { - await _cache.AddProjectDetails(project); - } - - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Update_Project", Response = project.ToProjectDto() }; - + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Create_Project", Response = response.Data }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); - - return Ok(ApiResponse.SuccessResponse(project.ToProjectDto(), "Success.", 200)); - - } - catch (Exception ex) - { - return BadRequest(ApiResponse.ErrorResponse(ex.Message, ex, 400)); } + return StatusCode(response.StatusCode, response); } /// @@ -522,76 +198,15 @@ namespace MarcoBMS.Services.Controllers return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - try + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.UpdateProjectAsync(id, updateProjectDto, tenantId, loggedInEmployee); + if (response.Success) { - var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - // --- Step 2: Fetch the Existing Entity from the Database --- - // This is crucial to avoid the data loss bug. We only want to modify an existing record. - var existingProject = await _context.Projects - .Where(p => p.Id == id && p.TenantId == tenantId) - .SingleOrDefaultAsync(); - - // 2a. Existence Check - if (existingProject == null) - { - _logger.LogWarning("Attempt to update non-existent project with ID {ProjectId} by user {UserId}.", id, loggedInEmployee.Id); - return NotFound(ApiResponse.ErrorResponse("Project not found.", $"No project found with ID {id}.", 404)); - } - - // 2b. Security Check - var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, id); - if (!hasPermission) - { - _logger.LogWarning("Access DENIED for user {UserId} attempting to update project {ProjectId}.", loggedInEmployee.Id, id); - return StatusCode(403, (ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to modify this project.", 403))); - } - - // --- Step 3: Apply Changes and Save --- - // Map the changes from the DTO onto the entity we just fetched from the database. - // This only modifies the properties defined in the mapping, preventing data loss. - _mapper.Map(updateProjectDto, existingProject); - - // Mark the entity as modified (if your mapping doesn't do it automatically). - _context.Entry(existingProject).State = EntityState.Modified; - - try - { - await _context.SaveChangesAsync(); - _logger.LogInfo("Successfully updated project {ProjectId} by user {UserId}.", id, loggedInEmployee.Id); - } - catch (DbUpdateConcurrencyException ex) - { - // --- Step 4: Handle Concurrency Conflicts --- - // This happens if another user modified the project after we fetched it. - _logger.LogWarning("Concurrency conflict while updating project {ProjectId} \n {Error}", id, ex.Message); - return Conflict(ApiResponse.ErrorResponse("Conflict occurred.", "This project has been modified by someone else. Please refresh and try again.", 409)); - } - - // --- Step 5: Perform Side-Effects in the Background (Fire and Forget) --- - // The core database operation is done. Now, we perform non-blocking cache and notification updates. - _ = Task.Run(async () => - { - // Create a DTO of the updated project to pass to background tasks. - var projectDto = _mapper.Map(existingProject); - - // 5a. Update Cache - await UpdateCacheInBackground(existingProject); - - // 5b. Send Targeted SignalR Notification - var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Update_Project", Response = projectDto }; - await SendNotificationInBackground(notification, projectDto.Id); - }); - - // --- Step 6: Return Success Response Immediately --- - // The client gets a fast response without waiting for caching or SignalR. - return Ok(ApiResponse.SuccessResponse(_mapper.Map(existingProject), "Project updated successfully.", 200)); - } - catch (Exception ex) - { - // --- Step 7: Graceful Error Handling for Unexpected Errors --- - _logger.LogError("An unexpected error occurred while updating project {ProjectId} \n {Error}", id, ex.Message); - return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred.", null, 500)); + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Update_Project", Response = response.Data }; + await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); } + return StatusCode(response.StatusCode, response); } #endregion @@ -1367,241 +982,5 @@ namespace MarcoBMS.Services.Controllers #endregion - #region =================================================================== Helper Functions =================================================================== - - /// - /// 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; - } - - private async Task GetProjectViewModel(Guid? id, Project project) - { - ProjectDetailsVM vm = new ProjectDetailsVM(); - - // List buildings = _unitOfWork.Building.GetAll(c => c.ProjectId == id).ToList(); - List buildings = await _context.Buildings.Where(c => c.ProjectId == id).ToListAsync(); - List idList = buildings.Select(o => o.Id).ToList(); - // List floors = _unitOfWork.Floor.GetAll(c => idList.Contains(c.Id)).ToList(); - List floors = await _context.Floor.Where(c => idList.Contains(c.BuildingId)).ToListAsync(); - idList = floors.Select(o => o.Id).ToList(); - //List workAreas = _unitOfWork.WorkArea.GetAll(c => idList.Contains(c.Id), includeProperties: "WorkItems,WorkItems.ActivityMaster").ToList(); - - List workAreas = await _context.WorkAreas.Where(c => idList.Contains(c.FloorId)).ToListAsync(); - - idList = workAreas.Select(o => o.Id).ToList(); - List workItems = await _context.WorkItems.Include(c => c.WorkCategoryMaster).Where(c => idList.Contains(c.WorkAreaId)).Include(c => c.ActivityMaster).ToListAsync(); - // List workItems = _unitOfWork.WorkItem.GetAll(c => idList.Contains(c.WorkAreaId), includeProperties: "ActivityMaster").ToList(); - idList = workItems.Select(t => t.Id).ToList(); - List tasks = await _context.TaskAllocations.Where(t => idList.Contains(t.WorkItemId) && t.AssignmentDate.Date == DateTime.UtcNow.Date).ToListAsync(); - vm.project = project; - vm.buildings = buildings; - vm.floors = floors; - vm.workAreas = workAreas; - vm.workItems = workItems; - vm.Tasks = tasks; - return vm; - } - - /// - /// Fetches project details from the database for a given list of project IDs and assembles them into MongoDB models. - /// This method encapsulates the optimized, parallel database queries. - /// - /// The list of project IDs to fetch. - /// The current tenant ID for filtering. - /// A list of fully populated ProjectMongoDB objects. - private async Task> FetchAndBuildProjectDetails(List projectIdsToFetch, Guid tenantId) - { - // Task to get base project details for the MISSING projects - var projectsTask = Task.Run(async () => - { - using var context = _dbContextFactory.CreateDbContext(); - return await context.Projects.AsNoTracking() - .Where(p => projectIdsToFetch.Contains(p.Id) && p.TenantId == tenantId) - .ToListAsync(); - }); - - // Task to get team sizes for the MISSING projects - var teamSizesTask = Task.Run(async () => - { - using var context = _dbContextFactory.CreateDbContext(); - return await context.ProjectAllocations.AsNoTracking() - .Where(pa => pa.TenantId == tenantId && projectIdsToFetch.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); - }); - - // Task to get work summaries for the MISSING projects - var workSummariesTask = Task.Run(async () => - { - using var context = _dbContextFactory.CreateDbContext(); - return await context.WorkItems.AsNoTracking() - .Where(wi => wi.TenantId == tenantId && - wi.WorkArea != null && - wi.WorkArea.Floor != null && - wi.WorkArea.Floor.Building != null && - projectIdsToFetch.Contains(wi.WorkArea.Floor.Building.ProjectId)) - .GroupBy(wi => wi.WorkArea!.Floor!.Building!.ProjectId) - .Select(g => new { ProjectId = g.Key, PlannedWork = g.Sum(i => i.PlannedWork), CompletedWork = g.Sum(i => i.CompletedWork) }) - .ToDictionaryAsync(x => x.ProjectId); - }); - - // Await all parallel tasks to complete - await Task.WhenAll(projectsTask, teamSizesTask, workSummariesTask); - - var projects = await projectsTask; - var teamSizes = await teamSizesTask; - var workSummaries = await workSummariesTask; - - // Proactively update the cache with the items we just fetched. - _logger.LogInfo("Updating cache with {NewItemCount} newly fetched projects.", projects.Count); - await _cache.AddProjectDetailsList(projects); - - // This section would build the full ProjectMongoDB objects, similar to your AddProjectDetailsList method. - // For brevity, assuming you have a mapper or a builder for this. Here's a simplified representation: - var mongoDetailsList = new List(); - foreach (var project in projects) - { - // This is a placeholder for the full build logic from your other methods. - // In a real scenario, you would fetch all hierarchy levels (buildings, floors, etc.) - // for the `projectIdsToFetch` and build the complete MongoDB object. - var mongoDetail = _mapper.Map(project); - mongoDetail.Id = project.Id; - mongoDetail.TeamSize = teamSizes.GetValueOrDefault(project.Id, 0); - if (workSummaries.TryGetValue(project.Id, out var summary)) - { - mongoDetail.PlannedWork = summary.PlannedWork; - mongoDetail.CompletedWork = summary.CompletedWork; - } - mongoDetailsList.Add(mongoDetail); - } - - return mongoDetailsList; - } - - /// - /// Private helper to encapsulate the cache-first data retrieval logic. - /// - /// A ProjectDetailVM if found, otherwise null. - private async Task GetProjectDataAsync(Guid projectId) - { - // --- Cache First --- - _logger.LogDebug("Attempting to fetch project {ProjectId} from cache.", projectId); - var cachedProject = await _cache.GetProjectDetails(projectId); - if (cachedProject != null) - { - _logger.LogInfo("Cache HIT for project {ProjectId}.", projectId); - // Map from the cache model (e.g., ProjectMongoDB) to the response ViewModel. - return _mapper.Map(cachedProject); - } - - // --- Database Second (on Cache Miss) --- - _logger.LogInfo("Cache MISS for project {ProjectId}. Fetching from database.", projectId); - var dbProject = await _context.Projects - .AsNoTracking() // Use AsNoTracking for read-only queries. - .Where(p => p.Id == projectId && p.TenantId == tenantId) - .SingleOrDefaultAsync(); - - if (dbProject == null) - { - return null; // The project doesn't exist. - } - - // --- Proactively Update Cache --- - // The next request for this project will now be a cache hit. - try - { - // Map the DB entity to the cache model (e.g., ProjectMongoDB) before caching. - await _cache.AddProjectDetails(dbProject); - _logger.LogInfo("Updated cache with project {ProjectId}.", projectId); - } - catch (Exception ex) - { - _logger.LogWarning("Failed to update cache for project {ProjectId} : \n {Error}", projectId, ex.Message); - } - - // Map from the database entity to the response ViewModel. - return dbProject; - } - - // Helper method for background cache update - private async Task UpdateCacheInBackground(Project project) - { - try - { - // This logic can be more complex, but the idea is to update or add. - if (!await _cache.UpdateProjectDetailsOnly(project)) - { - await _cache.AddProjectDetails(project); - } - _logger.LogInfo("Background cache update succeeded for project {ProjectId}.", project.Id); - } - catch (Exception ex) - { - _logger.LogError("Background cache update failed for project {ProjectId} \n {Error}", project.Id, ex.Message); - } - } - - // Helper method for background notification - private async Task SendNotificationInBackground(object notification, Guid projectId) - { - try - { - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); - _logger.LogInfo("Background SignalR notification sent for project {ProjectId}.", projectId); - } - catch (Exception ex) - { - _logger.LogError("Background SignalR notification failed for project {ProjectId} \n {Error}", projectId, ex.Message); - } - } - - #endregion } } \ No newline at end of file diff --git a/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs b/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs index 18db7ff..b811056 100644 --- a/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs @@ -39,6 +39,7 @@ namespace Marco.Pms.Services.MappingProfiles CreateMap(); CreateMap(); + CreateMap(); } } } diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index 7fa2647..6553745 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -6,6 +6,7 @@ using Marco.Pms.Model.Utilities; using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Service; +using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Middleware; using MarcoBMS.Services.Service; @@ -154,8 +155,13 @@ builder.Services.AddTransient(); builder.Services.AddTransient(); // Scoped services (one instance per HTTP request) +#region Customs Services builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +#endregion + +#region Helpers builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -164,9 +170,13 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +#endregion + +#region Cache Services builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +#endregion // Singleton services (one instance for the app's lifetime) builder.Services.AddSingleton(); diff --git a/Marco.Pms.Services/Service/PermissionServices.cs b/Marco.Pms.Services/Service/PermissionServices.cs index f20a768..9758a5f 100644 --- a/Marco.Pms.Services/Service/PermissionServices.cs +++ b/Marco.Pms.Services/Service/PermissionServices.cs @@ -37,7 +37,7 @@ namespace Marco.Pms.Services.Service if (projectIds == null) { - var hasPermission = await HasPermission(employeeId, PermissionsMaster.ManageProject); + var hasPermission = await HasPermission(PermissionsMaster.ManageProject, employeeId); if (hasPermission) { var projects = await _context.Projects.Where(c => c.TenantId == LoggedInEmployee.TenantId).ToListAsync(); @@ -45,12 +45,12 @@ namespace Marco.Pms.Services.Service } else { - var allocation = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.IsActive == true).ToListAsync(); - if (allocation.Any()) + var allocation = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.IsActive).ToListAsync(); + if (!allocation.Any()) { - projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); + return false; } - return false; + projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); } await _cache.AddProjects(LoggedInEmployee.Id, projectIds); } diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs new file mode 100644 index 0000000..3280558 --- /dev/null +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -0,0 +1,691 @@ +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.MongoDBModels; +using Marco.Pms.Model.Projects; +using Marco.Pms.Model.Utilities; +using Marco.Pms.Model.ViewModels.Projects; +using Marco.Pms.Services.Helpers; +using Marco.Pms.Services.Service.ServiceInterfaces; +using MarcoBMS.Services.Helpers; +using MarcoBMS.Services.Service; +using Microsoft.EntityFrameworkCore; + +namespace Marco.Pms.Services.Service +{ + public class ProjectServices : IProjectServices + { + private readonly IDbContextFactory _dbContextFactory; + private readonly ApplicationDbContext _context; // Keeping this for direct scoped context use where appropriate + private readonly ILoggingService _logger; + private readonly ProjectsHelper _projectsHelper; + private readonly PermissionServices _permission; + private readonly CacheUpdateHelper _cache; + private readonly IMapper _mapper; + public ProjectServices( + IDbContextFactory dbContextFactory, + ApplicationDbContext context, + ILoggingService logger, + ProjectsHelper projectsHelper, + PermissionServices permission, + CacheUpdateHelper cache, + IMapper mapper) + { + _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); + _context = context ?? throw new ArgumentNullException(nameof(context)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _projectsHelper = projectsHelper ?? throw new ArgumentNullException(nameof(projectsHelper)); + _permission = permission ?? throw new ArgumentNullException(nameof(permission)); + _cache = cache ?? throw new ArgumentNullException(nameof(cache)); + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + } + #region =================================================================== Project Get APIs =================================================================== + + public async Task> GetAllProjectsBasicAsync(Guid tenantId, Employee loggedInEmployee) + { + try + { + // Step 1: Verify the current user + if (loggedInEmployee == null) + { + return 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 + List accessibleProjectIds = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); + + if (accessibleProjectIds == null || !accessibleProjectIds.Any()) + { + _logger.LogInfo("No accessible projects found for EmployeeId {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.SuccessResponse(new List(), "0 records of project fetchd successfully", 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 ApiResponse.SuccessResponse(projectVMs, $"{projectVMs.Count} records of project fetchd successfully", 200); + } + catch (Exception ex) + { + // --- Step 5: Graceful Error Handling --- + _logger.LogError("An unexpected error occurred in GetAllProjectsBasic for tenant {TenantId}. \n {Error}", tenantId, ex.Message); + return ApiResponse.ErrorResponse("An internal server error occurred. Please try again later.", null, 500); + } + } + + public async Task> GetAllProjectsAsync(Guid tenantId, Employee loggedInEmployee) + { + try + { + _logger.LogInfo("Starting GetAllProjects for TenantId: {TenantId}, User: {UserId}", tenantId, loggedInEmployee.Id); + + // --- Step 1: Get a list of project IDs the user can access --- + List projectIds = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); + if (!projectIds.Any()) + { + _logger.LogInfo("User has no assigned projects. Returning empty list."); + return ApiResponse.SuccessResponse(new List(), "No projects found for the current user.", 200); + } + + // --- Step 2: Efficiently handle partial cache hits --- + _logger.LogInfo("Attempting to fetch details for {ProjectCount} projects from cache.", projectIds.Count); + + // Fetch what we can from the cache. + var cachedDetails = await _cache.GetProjectDetailsList(projectIds) ?? new List(); + var cachedDictionary = cachedDetails.ToDictionary(p => Guid.Parse(p.Id)); + + // Identify which projects are missing from the cache. + var missingIds = projectIds.Where(id => !cachedDictionary.ContainsKey(id)).ToList(); + + // Start building the response with the items we found in the cache. + var responseVms = _mapper.Map>(cachedDictionary.Values); + + if (missingIds.Any()) + { + // --- Step 3: Fetch ONLY the missing items from the database --- + _logger.LogInfo("Cache partial MISS. Found {CachedCount}, fetching {MissingCount} projects from DB.", + cachedDictionary.Count, missingIds.Count); + + // Call our dedicated data-fetching method for the missing IDs. + var newMongoDetails = await FetchAndBuildProjectDetails(missingIds, tenantId); + + if (newMongoDetails.Any()) + { + // Map the newly fetched items and add them to our response list. + responseVms.AddRange(newMongoDetails); + } + } + else + { + _logger.LogInfo("Cache HIT. All {ProjectCount} projects found in cache.", projectIds.Count); + } + + // --- Step 4: Return the combined result --- + _logger.LogInfo("Successfully retrieved a total of {ProjectCount} projects.", responseVms.Count); + return ApiResponse.SuccessResponse(responseVms, "Projects retrieved successfully.", 200); + } + catch (Exception ex) + { + // --- Step 5: Graceful Error Handling --- + _logger.LogError("An unexpected error occurred in GetAllProjects for tenant {TenantId}. \n {Error}", tenantId, ex.Message); + return ApiResponse.ErrorResponse("An internal server error occurred. Please try again later.", null, 500); + } + } + + public async Task> GetProjectAsync(Guid id, Guid tenantId, Employee loggedInEmployee) + { + try + { + // --- Step 1: Run independent operations in PARALLEL --- + // We can check permissions and fetch data at the same time to reduce latency. + var permissionTask = _permission.HasProjectPermission(loggedInEmployee, id); + + // This helper method encapsulates the "cache-first, then database" logic. + var projectDataTask = GetProjectDataAsync(id, tenantId); + + // Await both tasks to complete. + await Task.WhenAll(permissionTask, projectDataTask); + + var hasPermission = await permissionTask; + var projectVm = await projectDataTask; + + // --- Step 2: Process results sequentially --- + + // 2a. Check for permission first. Forbid() is the idiomatic way to return 403. + if (!hasPermission) + { + _logger.LogWarning("Access denied for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, id); + return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to access this project.", 403); + } + + // 2b. Check if the project was found (either in cache or DB). + if (projectVm == null) + { + _logger.LogInfo("Project with ID {ProjectId} not found.", id); + return ApiResponse.ErrorResponse("Project not found.", $"No project found with ID {id}.", 404); + } + + // 2c. Success. Return the consistent ViewModel. + _logger.LogInfo("Successfully retrieved project {ProjectId}.", id); + return ApiResponse.SuccessResponse(projectVm, "Project retrieved successfully.", 200); + } + catch (Exception ex) + { + _logger.LogError("An unexpected error occurred while getting project {ProjectId} : \n {Error}", id, ex.Message); + return ApiResponse.ErrorResponse("An internal server error occurred.", null, 500); + } + } + + public async Task> GetProjectDetailsAsync(Guid id, Guid tenantId, Employee loggedInEmployee) + { + try + { + _logger.LogInfo("Details requested by EmployeeId: {EmployeeId} for ProjectId: {ProjectId}", loggedInEmployee.Id, id); + + // Step 1: Check global view project permission + var hasViewProjectPermission = await _permission.HasPermission(PermissionsMaster.ViewProject, loggedInEmployee.Id); + if (!hasViewProjectPermission) + { + _logger.LogWarning("ViewProjects permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Access denied", "You don't have permission to view projects", 403); + } + + // Step 2: Check permission for this specific project + var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, id); + if (!hasProjectPermission) + { + _logger.LogWarning("Project-specific access denied. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployee.Id, id); + return ApiResponse.ErrorResponse("Access denied", "You don't have access to this project", 403); + } + + // Step 3: Fetch project with status + var projectDetails = await _cache.GetProjectDetails(id); + ProjectVM? projectVM = null; + if (projectDetails == null) + { + var project = await _context.Projects + .Include(c => c.ProjectStatus) + .FirstOrDefaultAsync(c => c.TenantId == tenantId && c.Id == id); + + projectVM = _mapper.Map(project); + + if (project != null) + { + await _cache.AddProjectDetails(project); + } + } + else + { + projectVM = _mapper.Map(projectDetails); + if (projectVM.ProjectStatus != null) + { + projectVM.ProjectStatus.TenantId = tenantId; + } + } + + if (projectVM == null) + { + _logger.LogWarning("Project not found. ProjectId: {ProjectId}", id); + return ApiResponse.ErrorResponse("Project not found", "Project not found", 404); + } + + // Step 4: Return result + + _logger.LogInfo("Project details fetched successfully. ProjectId: {ProjectId}", id); + return ApiResponse.SuccessResponse(projectVM, "Project details fetched successfully", 200); + } + catch (Exception ex) + { + // --- Step 5: Graceful Error Handling --- + _logger.LogError("An unexpected error occurred in Get Project Details for project {ProjectId} for tenant {TenantId}. \n {Error}", id, tenantId, ex.Message); + return ApiResponse.ErrorResponse("An internal server error occurred. Please try again later.", null, 500); + } + } + + public async Task> GetProjectDetailsOldAsync(Guid id, Guid tenantId, Employee loggedInEmployee) + { + var project = await _context.Projects + .Where(c => c.TenantId == tenantId && c.Id == id) + .Include(c => c.ProjectStatus) + .SingleOrDefaultAsync(); + + if (project == null) + { + return ApiResponse.ErrorResponse("Project not found", "Project not found", 404); + + } + else + { + ProjectDetailsVM vm = await GetProjectViewModel(id, project); + + OldProjectVM projectVM = new OldProjectVM(); + if (vm.project != null) + { + projectVM.Id = vm.project.Id; + projectVM.Name = vm.project.Name; + projectVM.ShortName = vm.project.ShortName; + projectVM.ProjectAddress = vm.project.ProjectAddress; + projectVM.ContactPerson = vm.project.ContactPerson; + projectVM.StartDate = vm.project.StartDate; + projectVM.EndDate = vm.project.EndDate; + projectVM.ProjectStatusId = vm.project.ProjectStatusId; + } + projectVM.Buildings = new List(); + if (vm.buildings != null) + { + foreach (Building build in vm.buildings) + { + BuildingVM buildVM = new BuildingVM() { Id = build.Id, Description = build.Description, Name = build.Name }; + buildVM.Floors = new List(); + if (vm.floors != null) + { + foreach (Floor floorDto in vm.floors.Where(c => c.BuildingId == build.Id).ToList()) + { + FloorsVM floorVM = new FloorsVM() { FloorName = floorDto.FloorName, Id = floorDto.Id }; + floorVM.WorkAreas = new List(); + + if (vm.workAreas != null) + { + foreach (WorkArea workAreaDto in vm.workAreas.Where(c => c.FloorId == floorVM.Id).ToList()) + { + WorkAreaVM workAreaVM = new WorkAreaVM() { Id = workAreaDto.Id, AreaName = workAreaDto.AreaName, WorkItems = new List() }; + + if (vm.workItems != null) + { + foreach (WorkItem workItemDto in vm.workItems.Where(c => c.WorkAreaId == workAreaDto.Id).ToList()) + { + WorkItemVM workItemVM = new WorkItemVM() { WorkItemId = workItemDto.Id, WorkItem = workItemDto }; + + workItemVM.WorkItem.WorkArea = new WorkArea(); + + if (workItemVM.WorkItem.ActivityMaster != null) + { + workItemVM.WorkItem.ActivityMaster.Tenant = new Tenant(); + } + workItemVM.WorkItem.Tenant = new Tenant(); + + double todaysAssigned = 0; + if (vm.Tasks != null) + { + var tasks = vm.Tasks.Where(t => t.WorkItemId == workItemDto.Id).ToList(); + foreach (TaskAllocation task in tasks) + { + todaysAssigned += task.PlannedTask; + } + } + workItemVM.TodaysAssigned = todaysAssigned; + + workAreaVM.WorkItems.Add(workItemVM); + } + } + + floorVM.WorkAreas.Add(workAreaVM); + } + } + + buildVM.Floors.Add(floorVM); + } + } + projectVM.Buildings.Add(buildVM); + } + } + return ApiResponse.SuccessResponse(projectVM, "Success.", 200); + } + } + + #endregion + + #region =================================================================== Project Manage APIs =================================================================== + + public async Task> CreateProjectAsync(CreateProjectDto projectDto, Guid tenantId, Employee loggedInEmployee) + { + // 1. Prepare data without I/O + var loggedInUserId = loggedInEmployee.Id; + var project = _mapper.Map(projectDto); + project.TenantId = tenantId; + + // 2. Store it to database + try + { + _context.Projects.Add(project); + await _context.SaveChangesAsync(); + } + catch (Exception ex) + { + // Log the detailed exception + _logger.LogError("Failed to create project in database. Rolling back transaction. \n {Error}", ex.Message); + // Return a server error as the primary operation failed + return ApiResponse.ErrorResponse("An error occurred while saving the project.", ex.Message, 500); + } + + // 3. Perform non-critical side-effects (caching, notifications) concurrently + try + { + // These operations do not depend on each other, so they can run in parallel. + Task cacheAddDetailsTask = _cache.AddProjectDetails(project); + Task cacheClearListTask = _cache.ClearAllProjectIdsByPermissionId(PermissionsMaster.ManageProject); + + // Await all side-effect tasks to complete in parallel + await Task.WhenAll(cacheAddDetailsTask, cacheClearListTask); + } + catch (Exception ex) + { + // The project was created successfully, but a side-effect failed. + // Log this as a warning, as the primary operation succeeded. Don't return an error to the user. + _logger.LogWarning("Project {ProjectId} was created, but a post-creation side-effect (caching/notification) failed. \n {Error}", project.Id, ex.Message); + } + + // 4. Return a success response to the user as soon as the critical data is saved. + return ApiResponse.SuccessResponse(_mapper.Map(project), "Project created successfully.", 200); + } + + /// + /// Updates an existing project's details. + /// This endpoint is secure, handles concurrency, and performs non-essential tasks in the background. + /// + /// The ID of the project to update. + /// The data to update the project with. + /// An ApiResponse confirming the update or an appropriate error. + public async Task> UpdateProjectAsync(Guid id, UpdateProjectDto updateProjectDto, Guid tenantId, Employee loggedInEmployee) + { + try + { + // --- Step 1: Fetch the Existing Entity from the Database --- + // This is crucial to avoid the data loss bug. We only want to modify an existing record. + var existingProject = await _context.Projects + .Where(p => p.Id == id && p.TenantId == tenantId) + .SingleOrDefaultAsync(); + + // 1a. Existence Check + if (existingProject == null) + { + _logger.LogWarning("Attempt to update non-existent project with ID {ProjectId} by user {UserId}.", id, loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Project not found.", $"No project found with ID {id}.", 404); + } + + // 1b. Security Check + var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, id); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} attempting to update project {ProjectId}.", loggedInEmployee.Id, id); + return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to modify this project.", 403); + } + + // --- Step 2: Apply Changes and Save --- + // Map the changes from the DTO onto the entity we just fetched from the database. + // This only modifies the properties defined in the mapping, preventing data loss. + _mapper.Map(updateProjectDto, existingProject); + + // Mark the entity as modified (if your mapping doesn't do it automatically). + _context.Entry(existingProject).State = EntityState.Modified; + + try + { + await _context.SaveChangesAsync(); + _logger.LogInfo("Successfully updated project {ProjectId} by user {UserId}.", id, loggedInEmployee.Id); + } + catch (DbUpdateConcurrencyException ex) + { + // --- Step 3: Handle Concurrency Conflicts --- + // This happens if another user modified the project after we fetched it. + _logger.LogWarning("Concurrency conflict while updating project {ProjectId} \n {Error}", id, ex.Message); + return ApiResponse.ErrorResponse("Conflict occurred.", "This project has been modified by someone else. Please refresh and try again.", 409); + } + + // --- Step 4: Perform Side-Effects in the Background (Fire and Forget) --- + // The core database operation is done. Now, we perform non-blocking cache and notification updates. + _ = Task.Run(async () => + { + // Create a DTO of the updated project to pass to background tasks. + var projectDto = _mapper.Map(existingProject); + + // 4a. Update Cache + await UpdateCacheInBackground(existingProject); + + }); + + // --- Step 5: Return Success Response Immediately --- + // The client gets a fast response without waiting for caching or SignalR. + return ApiResponse.SuccessResponse(_mapper.Map(existingProject), "Project updated successfully.", 200); + } + catch (Exception ex) + { + // --- Step 6: Graceful Error Handling for Unexpected Errors --- + _logger.LogError("An unexpected error occurred while updating project {ProjectId} \n {Error}", id, ex.Message); + return ApiResponse.ErrorResponse("An internal server error occurred.", null, 500); + } + } + + #endregion + + #region =================================================================== Helper Functions =================================================================== + + /// + /// 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; + } + + private async Task GetProjectViewModel(Guid? id, Project project) + { + ProjectDetailsVM vm = new ProjectDetailsVM(); + + // List buildings = _unitOfWork.Building.GetAll(c => c.ProjectId == id).ToList(); + List buildings = await _context.Buildings.Where(c => c.ProjectId == id).ToListAsync(); + List idList = buildings.Select(o => o.Id).ToList(); + // List floors = _unitOfWork.Floor.GetAll(c => idList.Contains(c.Id)).ToList(); + List floors = await _context.Floor.Where(c => idList.Contains(c.BuildingId)).ToListAsync(); + idList = floors.Select(o => o.Id).ToList(); + //List workAreas = _unitOfWork.WorkArea.GetAll(c => idList.Contains(c.Id), includeProperties: "WorkItems,WorkItems.ActivityMaster").ToList(); + + List workAreas = await _context.WorkAreas.Where(c => idList.Contains(c.FloorId)).ToListAsync(); + + idList = workAreas.Select(o => o.Id).ToList(); + List workItems = await _context.WorkItems.Include(c => c.WorkCategoryMaster).Where(c => idList.Contains(c.WorkAreaId)).Include(c => c.ActivityMaster).ToListAsync(); + // List workItems = _unitOfWork.WorkItem.GetAll(c => idList.Contains(c.WorkAreaId), includeProperties: "ActivityMaster").ToList(); + idList = workItems.Select(t => t.Id).ToList(); + List tasks = await _context.TaskAllocations.Where(t => idList.Contains(t.WorkItemId) && t.AssignmentDate.Date == DateTime.UtcNow.Date).ToListAsync(); + vm.project = project; + vm.buildings = buildings; + vm.floors = floors; + vm.workAreas = workAreas; + vm.workItems = workItems; + vm.Tasks = tasks; + return vm; + } + + /// + /// Fetches project details from the database for a given list of project IDs and assembles them into MongoDB models. + /// This method encapsulates the optimized, parallel database queries. + /// + /// The list of project IDs to fetch. + /// The current tenant ID for filtering. + /// A list of fully populated ProjectMongoDB objects. + private async Task> FetchAndBuildProjectDetails(List projectIdsToFetch, Guid tenantId) + { + // Task to get base project details for the MISSING projects + var projectsTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + return await context.Projects.AsNoTracking() + .Where(p => projectIdsToFetch.Contains(p.Id) && p.TenantId == tenantId) + .ToListAsync(); + }); + + // Task to get team sizes for the MISSING projects + var teamSizesTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + return await context.ProjectAllocations.AsNoTracking() + .Where(pa => pa.TenantId == tenantId && projectIdsToFetch.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); + }); + + // Task to get work summaries for the MISSING projects + var workSummariesTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + return await context.WorkItems.AsNoTracking() + .Where(wi => wi.TenantId == tenantId && + wi.WorkArea != null && + wi.WorkArea.Floor != null && + wi.WorkArea.Floor.Building != null && + projectIdsToFetch.Contains(wi.WorkArea.Floor.Building.ProjectId)) + .GroupBy(wi => wi.WorkArea!.Floor!.Building!.ProjectId) + .Select(g => new { ProjectId = g.Key, PlannedWork = g.Sum(i => i.PlannedWork), CompletedWork = g.Sum(i => i.CompletedWork) }) + .ToDictionaryAsync(x => x.ProjectId); + }); + + // Await all parallel tasks to complete + await Task.WhenAll(projectsTask, teamSizesTask, workSummariesTask); + + var projects = await projectsTask; + var teamSizes = await teamSizesTask; + var workSummaries = await workSummariesTask; + + // Proactively update the cache with the items we just fetched. + _logger.LogInfo("Updating cache with {NewItemCount} newly fetched projects.", projects.Count); + await _cache.AddProjectDetailsList(projects); + + // This section would build the full ProjectMongoDB objects, similar to your AddProjectDetailsList method. + // For brevity, assuming you have a mapper or a builder for this. Here's a simplified representation: + var mongoDetailsList = new List(); + foreach (var project in projects) + { + // This is a placeholder for the full build logic from your other methods. + // In a real scenario, you would fetch all hierarchy levels (buildings, floors, etc.) + // for the `projectIdsToFetch` and build the complete MongoDB object. + var mongoDetail = _mapper.Map(project); + mongoDetail.Id = project.Id; + mongoDetail.TeamSize = teamSizes.GetValueOrDefault(project.Id, 0); + if (workSummaries.TryGetValue(project.Id, out var summary)) + { + mongoDetail.PlannedWork = summary.PlannedWork; + mongoDetail.CompletedWork = summary.CompletedWork; + } + mongoDetailsList.Add(mongoDetail); + } + + return mongoDetailsList; + } + + /// + /// Private helper to encapsulate the cache-first data retrieval logic. + /// + /// A ProjectDetailVM if found, otherwise null. + private async Task GetProjectDataAsync(Guid projectId, Guid tenantId) + { + // --- Cache First --- + _logger.LogDebug("Attempting to fetch project {ProjectId} from cache.", projectId); + var cachedProject = await _cache.GetProjectDetails(projectId); + if (cachedProject != null) + { + _logger.LogInfo("Cache HIT for project {ProjectId}.", projectId); + // Map from the cache model (e.g., ProjectMongoDB) to the response ViewModel. + return _mapper.Map(cachedProject); + } + + // --- Database Second (on Cache Miss) --- + _logger.LogInfo("Cache MISS for project {ProjectId}. Fetching from database.", projectId); + var dbProject = await _context.Projects + .AsNoTracking() // Use AsNoTracking for read-only queries. + .Where(p => p.Id == projectId && p.TenantId == tenantId) + .SingleOrDefaultAsync(); + + if (dbProject == null) + { + return null; // The project doesn't exist. + } + + // --- Proactively Update Cache --- + // The next request for this project will now be a cache hit. + try + { + // Map the DB entity to the cache model (e.g., ProjectMongoDB) before caching. + await _cache.AddProjectDetails(dbProject); + _logger.LogInfo("Updated cache with project {ProjectId}.", projectId); + } + catch (Exception ex) + { + _logger.LogWarning("Failed to update cache for project {ProjectId} : \n {Error}", projectId, ex.Message); + } + + // Map from the database entity to the response ViewModel. + return dbProject; + } + + // Helper method for background cache update + private async Task UpdateCacheInBackground(Project project) + { + try + { + // This logic can be more complex, but the idea is to update or add. + if (!await _cache.UpdateProjectDetailsOnly(project)) + { + await _cache.AddProjectDetails(project); + } + _logger.LogInfo("Background cache update succeeded for project {ProjectId}.", project.Id); + } + catch (Exception ex) + { + _logger.LogError("Background cache update failed for project {ProjectId} \n {Error}", project.Id, ex.Message); + } + } + + #endregion + } +} diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs new file mode 100644 index 0000000..a23eba0 --- /dev/null +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs @@ -0,0 +1,17 @@ +using Marco.Pms.Model.Dtos.Project; +using Marco.Pms.Model.Employees; +using Marco.Pms.Model.Utilities; + +namespace Marco.Pms.Services.Service.ServiceInterfaces +{ + public interface IProjectServices + { + Task> GetAllProjectsBasicAsync(Guid tenantId, Employee loggedInEmployee); + Task> GetAllProjectsAsync(Guid tenantId, Employee loggedInEmployee); + Task> GetProjectAsync(Guid id, Guid tenantId, Employee loggedInEmployee); + Task> GetProjectDetailsAsync(Guid id, Guid tenantId, Employee loggedInEmployee); + Task> GetProjectDetailsOldAsync(Guid id, Guid tenantId, Employee loggedInEmployee); + Task> CreateProjectAsync(CreateProjectDto projectDto, Guid tenantId, Employee loggedInEmployee); + Task> UpdateProjectAsync(Guid id, UpdateProjectDto updateProjectDto, Guid tenantId, Employee loggedInEmployee); + } +} From 73aa1d618150e1a48512cd975205e6b5abfddc6b Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 15 Jul 2025 12:44:38 +0530 Subject: [PATCH 070/124] adde functionality to delete workItems from cache --- .../Controllers/AttendanceController.cs | 30 +-- .../Controllers/AuthController.cs | 34 +-- .../Controllers/DashboardController.cs | 10 +- .../Controllers/DirectoryController.cs | 4 +- .../Controllers/EmployeeController.cs | 4 +- .../Controllers/ForumController.cs | 30 +-- .../Controllers/MasterController.cs | 48 ++-- .../Controllers/ProjectController.cs | 129 ++++------ .../Controllers/ReportController.cs | 16 +- .../Helpers/CacheUpdateHelper.cs | 8 +- Marco.Pms.Services/Helpers/DirectoryHelper.cs | 18 +- Marco.Pms.Services/Helpers/EmployeeHelper.cs | 6 +- Marco.Pms.Services/Helpers/MasterHelper.cs | 10 +- Marco.Pms.Services/Helpers/ReportHelper.cs | 10 +- Marco.Pms.Services/Helpers/RolesHelper.cs | 4 +- ...ectMappingProfile.cs => MappingProfile.cs} | 12 +- Marco.Pms.Services/Program.cs | 1 + Marco.Pms.Services/Service/ILoggingService.cs | 2 +- Marco.Pms.Services/Service/LoggingServices.cs | 6 +- Marco.Pms.Services/Service/ProjectServices.cs | 227 +++++++++++++++++- .../Service/RefreshTokenService.cs | 14 +- Marco.Pms.Services/Service/S3UploadService.cs | 14 +- .../ServiceInterfaces/IProjectServices.cs | 2 + .../ServiceInterfaces/ISignalRService.cs | 7 + Marco.Pms.Services/Service/SignalRService.cs | 29 +++ 25 files changed, 444 insertions(+), 231 deletions(-) rename Marco.Pms.Services/MappingProfiles/{ProjectMappingProfile.cs => MappingProfile.cs} (75%) create mode 100644 Marco.Pms.Services/Service/ServiceInterfaces/ISignalRService.cs create mode 100644 Marco.Pms.Services/Service/SignalRService.cs diff --git a/Marco.Pms.Services/Controllers/AttendanceController.cs b/Marco.Pms.Services/Controllers/AttendanceController.cs index 4c2f2c1..1a5e4e7 100644 --- a/Marco.Pms.Services/Controllers/AttendanceController.cs +++ b/Marco.Pms.Services/Controllers/AttendanceController.cs @@ -90,18 +90,18 @@ namespace MarcoBMS.Services.Controllers if (dateFrom != null && DateTime.TryParse(dateFrom, out fromDate) == false) { - _logger.LogError("User sent Invalid from Date while featching attendance logs"); + _logger.LogWarning("User sent Invalid from Date while featching attendance logs"); return BadRequest(ApiResponse.ErrorResponse("Invalid Date", "Invalid Date", 400)); } if (dateTo != null && DateTime.TryParse(dateTo, out toDate) == false) { - _logger.LogError("User sent Invalid to Date while featching attendance logs"); + _logger.LogWarning("User sent Invalid to Date while featching attendance logs"); return BadRequest(ApiResponse.ErrorResponse("Invalid Date", "Invalid Date", 400)); } if (employeeId == Guid.Empty) { - _logger.LogError("The employee Id sent by user is empty"); + _logger.LogWarning("The employee Id sent by user is empty"); return BadRequest(ApiResponse.ErrorResponse("Employee ID is required and must not be Empty.", "Employee ID is required and must not be empty.", 400)); } List attendances = await _context.Attendes.Where(c => c.EmployeeID == employeeId && c.TenantId == TenantId && c.AttendanceDate.Date >= fromDate && c.AttendanceDate.Date <= toDate).ToListAsync(); @@ -161,18 +161,18 @@ namespace MarcoBMS.Services.Controllers if (dateFrom != null && DateTime.TryParse(dateFrom, out fromDate) == false) { - _logger.LogError("User sent Invalid fromDate while featching attendance logs"); + _logger.LogWarning("User sent Invalid fromDate while featching attendance logs"); return BadRequest(ApiResponse.ErrorResponse("Invalid Date", "Invalid Date", 400)); } if (dateTo != null && DateTime.TryParse(dateTo, out toDate) == false) { - _logger.LogError("User sent Invalid toDate while featching attendance logs"); + _logger.LogWarning("User sent Invalid toDate while featching attendance logs"); return BadRequest(ApiResponse.ErrorResponse("Invalid Date", "Invalid Date", 400)); } if (projectId == Guid.Empty) { - _logger.LogError("The project Id sent by user is less than or equal to zero"); + _logger.LogWarning("The project Id sent by user is less than or equal to zero"); return BadRequest(ApiResponse.ErrorResponse("Project ID is required and must be greater than zero.", "Project ID is required and must be greater than zero.", 400)); } @@ -276,13 +276,13 @@ namespace MarcoBMS.Services.Controllers if (date != null && DateTime.TryParse(date, out forDate) == false) { - _logger.LogError("User sent Invalid Date while featching attendance logs"); + _logger.LogWarning("User sent Invalid Date while featching attendance logs"); return BadRequest(ApiResponse.ErrorResponse("Invalid Date", "Invalid Date", 400)); } if (projectId == Guid.Empty) { - _logger.LogError("The project Id sent by user is less than or equal to zero"); + _logger.LogWarning("The project Id sent by user is less than or equal to zero"); return BadRequest(ApiResponse.ErrorResponse("Project ID is required and must be greater than zero.", "Project ID is required and must be greater than zero.", 400)); } @@ -425,7 +425,7 @@ namespace MarcoBMS.Services.Controllers .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - _logger.LogError("User sent Invalid Date while marking attendance"); + _logger.LogWarning("User sent Invalid Date while marking attendance \n {Error}", string.Join(",", errors)); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } @@ -439,14 +439,14 @@ namespace MarcoBMS.Services.Controllers if (recordAttendanceDot.MarkTime == null) { - _logger.LogError("User sent Invalid Mark Time while marking attendance"); + _logger.LogWarning("User sent Invalid Mark Time while marking attendance"); return BadRequest(ApiResponse.ErrorResponse("Invalid Mark Time", "Invalid Mark Time", 400)); } DateTime finalDateTime = GetDateFromTimeStamp(recordAttendanceDot.Date, recordAttendanceDot.MarkTime); if (recordAttendanceDot.Comment == null) { - _logger.LogError("User sent Invalid comment while marking attendance"); + _logger.LogWarning("User sent Invalid comment while marking attendance"); return BadRequest(ApiResponse.ErrorResponse("Invalid Comment", "Invalid Comment", 400)); } @@ -480,7 +480,7 @@ namespace MarcoBMS.Services.Controllers } else { - _logger.LogError("Employee {EmployeeId} sent regularization request but it check-out time is earlier than check-out"); + _logger.LogWarning("Employee {EmployeeId} sent regularization request but it check-out time is earlier than check-out"); return BadRequest(ApiResponse.ErrorResponse("Check-out time must be later than check-in time", "Check-out time must be later than check-in time", 400)); } // do nothing @@ -585,7 +585,7 @@ namespace MarcoBMS.Services.Controllers catch (Exception ex) { await transaction.RollbackAsync(); // Rollback on failure - _logger.LogError("{Error} while marking attendance", ex.Message); + _logger.LogError(ex, "An Error occured while marking attendance"); var response = new { message = ex.Message, @@ -604,7 +604,7 @@ namespace MarcoBMS.Services.Controllers if (!ModelState.IsValid) { var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); - _logger.LogError("Invalid attendance model received."); + _logger.LogWarning("Invalid attendance model received. \n {Error}", string.Join(",", errors)); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } @@ -780,7 +780,7 @@ namespace MarcoBMS.Services.Controllers catch (Exception ex) { await transaction.RollbackAsync(); - _logger.LogError("Error while recording attendance : {Error}", ex.Message); + _logger.LogError(ex, "Error while recording attendance"); return BadRequest(ApiResponse.ErrorResponse("Something went wrong", ex.Message, 500)); } } diff --git a/Marco.Pms.Services/Controllers/AuthController.cs b/Marco.Pms.Services/Controllers/AuthController.cs index 1b45eb7..429a38b 100644 --- a/Marco.Pms.Services/Controllers/AuthController.cs +++ b/Marco.Pms.Services/Controllers/AuthController.cs @@ -1,8 +1,4 @@ -using System.Net; -using System.Security.Claims; -using System.Security.Cryptography; -using System.Text; -using Marco.Pms.DataAccess.Data; +using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Authentication; using Marco.Pms.Model.Dtos.Authentication; using Marco.Pms.Model.Dtos.Util; @@ -15,6 +11,10 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using System.Net; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; namespace MarcoBMS.Services.Controllers { @@ -110,7 +110,7 @@ namespace MarcoBMS.Services.Controllers } catch (Exception ex) { - _logger.LogError("Unexpected error during login : {Error}", ex.Message); + _logger.LogError(ex, "Unexpected error during login"); return StatusCode(500, ApiResponse.ErrorResponse("Unexpected error", ex.Message, 500)); } } @@ -270,7 +270,7 @@ namespace MarcoBMS.Services.Controllers } catch (Exception ex) { - _logger.LogError("Unexpected error occurred while verifying MPIN : {Error}", ex.Message); + _logger.LogError(ex, "Unexpected error occurred while verifying MPIN"); return StatusCode(500, ApiResponse.ErrorResponse("Unexpected error", ex.Message, 500)); } } @@ -307,7 +307,7 @@ namespace MarcoBMS.Services.Controllers } catch (Exception ex) { - _logger.LogError("Unexpected error during logout : {Error}", ex.Message); + _logger.LogError(ex, "Unexpected error during logout"); return StatusCode(500, ApiResponse.ErrorResponse("Unexpected error occurred", ex.Message, 500)); } } @@ -351,7 +351,7 @@ namespace MarcoBMS.Services.Controllers if (string.IsNullOrWhiteSpace(user.UserName)) { - _logger.LogError("Username missing for user ID: {UserId}", user.Id); + _logger.LogWarning("Username missing for user ID: {UserId}", user.Id); return NotFound(ApiResponse.ErrorResponse("Username not found.", "Username not found.", 404)); } @@ -370,7 +370,7 @@ namespace MarcoBMS.Services.Controllers } catch (Exception ex) { - _logger.LogError("An unexpected error occurred during token refresh. : {Error}", ex.Message); + _logger.LogError(ex, "An unexpected error occurred during token refresh."); return StatusCode(500, ApiResponse.ErrorResponse("Unexpected error occurred.", ex.Message, 500)); } } @@ -406,7 +406,7 @@ namespace MarcoBMS.Services.Controllers } catch (Exception ex) { - _logger.LogError("Error while sending password reset email to: {Error}", ex.Message); + _logger.LogError(ex, "Error while sending password reset email to"); return StatusCode(500, ApiResponse.ErrorResponse("Error sending password reset email.", ex.Message, 500)); } } @@ -480,7 +480,7 @@ namespace MarcoBMS.Services.Controllers } catch (Exception ex) { - _logger.LogError("Error while sending reset password success email to user: {Error}", ex.Message); + _logger.LogError(ex, "Error while sending reset password success email to user"); // Continue, do not fail because of email issue } @@ -547,7 +547,7 @@ namespace MarcoBMS.Services.Controllers } catch (Exception ex) { - _logger.LogError("An unexpected error occurred while sending OTP to {Email} : {Error}", generateOTP.Email ?? "", ex.Message); + _logger.LogError(ex, "An unexpected error occurred while sending OTP to {Email}", generateOTP.Email ?? ""); return StatusCode(500, ApiResponse.ErrorResponse("An unexpected error occurred.", ex.Message, 500)); } } @@ -638,7 +638,7 @@ namespace MarcoBMS.Services.Controllers } catch (Exception ex) { - _logger.LogError("An unexpected error occurred during OTP login for email {Email} : {Error}", verifyOTP.Email ?? string.Empty, ex.Message); + _logger.LogError(ex, "An unexpected error occurred during OTP login for email {Email}", verifyOTP.Email ?? string.Empty); return StatusCode(500, ApiResponse.ErrorResponse("Unexpected error", ex.Message, 500)); } } @@ -719,7 +719,7 @@ namespace MarcoBMS.Services.Controllers if (!result.Succeeded) { var errors = result.Errors.Select(e => e.Description).ToList(); - _logger.LogError("Password reset failed for user {Email}. Errors: {Errors}", changePassword.Email, string.Join("; ", errors)); + _logger.LogWarning("Password reset failed for user {Email}. Errors: {Errors}", changePassword.Email, string.Join("; ", errors)); return BadRequest(ApiResponse.ErrorResponse("Failed to change password", errors, 400)); } @@ -732,7 +732,7 @@ namespace MarcoBMS.Services.Controllers } catch (Exception exp) { - _logger.LogError("An unexpected error occurred while changing password : {Error}", exp.Message); + _logger.LogError(exp, "An unexpected error occurred while changing password"); return StatusCode(500, ApiResponse.ErrorResponse("An unexpected error occurred.", exp.Message, 500)); } } @@ -752,7 +752,7 @@ namespace MarcoBMS.Services.Controllers // Validate employee and MPIN input if (requestEmployee == null || string.IsNullOrWhiteSpace(generateMPINDto.MPIN) || generateMPINDto.MPIN.Length != 6 || !generateMPINDto.MPIN.All(char.IsDigit)) { - _logger.LogError("Employee {EmployeeId} provided invalid information to generate MPIN", loggedInEmployee.Id); + _logger.LogWarning("Employee {EmployeeId} provided invalid information to generate MPIN", loggedInEmployee.Id); return BadRequest(ApiResponse.ErrorResponse("Provided invalid information", "Provided invalid information", 400)); } diff --git a/Marco.Pms.Services/Controllers/DashboardController.cs b/Marco.Pms.Services/Controllers/DashboardController.cs index bdb965c..934725a 100644 --- a/Marco.Pms.Services/Controllers/DashboardController.cs +++ b/Marco.Pms.Services/Controllers/DashboardController.cs @@ -221,7 +221,7 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Number of pending regularization and pending check-out are fetched successfully for employee {EmployeeId}", LoggedInEmployee.Id); return Ok(ApiResponse.SuccessResponse(response, "Pending regularization and pending check-out are fetched successfully", 200)); } - _logger.LogError("No attendance entry was found for employee {EmployeeId}", LoggedInEmployee.Id); + _logger.LogWarning("No attendance entry was found for employee {EmployeeId}", LoggedInEmployee.Id); return NotFound(ApiResponse.ErrorResponse("No attendance entry was found for this employee", "No attendance entry was found for this employee", 404)); } @@ -235,14 +235,14 @@ namespace Marco.Pms.Services.Controllers List? projectProgressionVMs = new List(); if (date != null && DateTime.TryParse(date, out currentDate) == false) { - _logger.LogError($"user send invalid date"); + _logger.LogWarning($"user send invalid date"); return BadRequest(ApiResponse.ErrorResponse("Invalid date.", "Invalid date.", 400)); } Project? project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == projectId); if (project == null) { - _logger.LogError("Employee {EmployeeId} was attempted to get project attendance for date {Date}, but project not found in database", LoggedInEmployee.Id, currentDate); + _logger.LogWarning("Employee {EmployeeId} was attempted to get project attendance for date {Date}, but project not found in database", LoggedInEmployee.Id, currentDate); return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); } List? projectAllocation = await _context.ProjectAllocations.Where(p => p.ProjectId == projectId && p.IsActive && p.TenantId == tenantId).ToListAsync(); @@ -288,14 +288,14 @@ namespace Marco.Pms.Services.Controllers DateTime currentDate = DateTime.UtcNow; if (date != null && DateTime.TryParse(date, out currentDate) == false) { - _logger.LogError($"user send invalid date"); + _logger.LogWarning($"user send invalid date"); return BadRequest(ApiResponse.ErrorResponse("Invalid date.", "Invalid date.", 400)); } Project? project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == projectId); if (project == null) { - _logger.LogError("Employee {EmployeeId} was attempted to get activities performed for date {Date}, but project not found in database", LoggedInEmployee.Id, currentDate); + _logger.LogWarning("Employee {EmployeeId} was attempted to get activities performed for date {Date}, but project not found in database", LoggedInEmployee.Id, currentDate); return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); } diff --git a/Marco.Pms.Services/Controllers/DirectoryController.cs b/Marco.Pms.Services/Controllers/DirectoryController.cs index 4a0e41e..9eb06e0 100644 --- a/Marco.Pms.Services/Controllers/DirectoryController.cs +++ b/Marco.Pms.Services/Controllers/DirectoryController.cs @@ -77,7 +77,7 @@ namespace Marco.Pms.Services.Controllers .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - _logger.LogError("User sent Invalid Date while marking attendance"); + _logger.LogWarning("User sent Invalid Date while marking attendance"); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } var response = await _directoryHelper.CreateContact(createContact); @@ -256,7 +256,7 @@ namespace Marco.Pms.Services.Controllers .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - _logger.LogError("User sent Invalid Date while marking attendance"); + _logger.LogWarning("User sent Invalid Date while marking attendance"); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } var response = await _directoryHelper.CreateBucket(bucketDto); diff --git a/Marco.Pms.Services/Controllers/EmployeeController.cs b/Marco.Pms.Services/Controllers/EmployeeController.cs index 2f0ca5e..c9e19fa 100644 --- a/Marco.Pms.Services/Controllers/EmployeeController.cs +++ b/Marco.Pms.Services/Controllers/EmployeeController.cs @@ -382,7 +382,7 @@ namespace MarcoBMS.Services.Controllers Employee? existingEmployee = await _context.Employees.FirstOrDefaultAsync(e => e.Id == model.Id.Value); if (existingEmployee == null) { - _logger.LogError("User tries to update employee {EmployeeId} but not found in database", model.Id); + _logger.LogWarning("User tries to update employee {EmployeeId} but not found in database", model.Id); return NotFound(ApiResponse.ErrorResponse("Employee not found", "Employee not found", 404)); } byte[]? imageBytes = null; @@ -495,7 +495,7 @@ namespace MarcoBMS.Services.Controllers } else { - _logger.LogError("Employee with ID {EmploueeId} not found in database", id); + _logger.LogWarning("Employee with ID {EmploueeId} not found in database", id); } return Ok(ApiResponse.SuccessResponse(new { }, "Employee Suspended successfully", 200)); } diff --git a/Marco.Pms.Services/Controllers/ForumController.cs b/Marco.Pms.Services/Controllers/ForumController.cs index 769c08a..fb6d0e7 100644 --- a/Marco.Pms.Services/Controllers/ForumController.cs +++ b/Marco.Pms.Services/Controllers/ForumController.cs @@ -44,7 +44,7 @@ namespace Marco.Pms.Services.Controllers .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - _logger.LogError("{error}", errors); + _logger.LogWarning("{error}", errors); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } Guid tenantId = _userHelper.GetTenantId(); @@ -66,7 +66,7 @@ namespace Marco.Pms.Services.Controllers var Image = attachmentDto; if (string.IsNullOrEmpty(Image.Base64Data)) { - _logger.LogError("Base64 data is missing"); + _logger.LogWarning("Base64 data is missing"); return BadRequest(ApiResponse.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); } @@ -160,7 +160,7 @@ namespace Marco.Pms.Services.Controllers .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - _logger.LogError("{error}", errors); + _logger.LogWarning("{error}", errors); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } Guid tenantId = _userHelper.GetTenantId(); @@ -197,7 +197,7 @@ namespace Marco.Pms.Services.Controllers var Image = attachmentDto; if (string.IsNullOrEmpty(Image.Base64Data)) { - _logger.LogError("Base64 data is missing"); + _logger.LogWarning("Base64 data is missing"); return BadRequest(ApiResponse.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); } @@ -336,7 +336,7 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Ticket {TicketId} updated", updateTicketDto.Id); return Ok(ApiResponse.SuccessResponse(ticketVM, "Ticket Updated Successfully", 200)); } - _logger.LogError("Ticket {TicketId} not Found in database", updateTicketDto.Id); + _logger.LogWarning("Ticket {TicketId} not Found in database", updateTicketDto.Id); return NotFound(ApiResponse.ErrorResponse("Ticket not Found", "Ticket not Found", 404)); } @@ -349,7 +349,7 @@ namespace Marco.Pms.Services.Controllers .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - _logger.LogError("{error}", errors); + _logger.LogWarning("{error}", errors); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } @@ -364,7 +364,7 @@ namespace Marco.Pms.Services.Controllers if (ticket == null) { - _logger.LogError("Ticket {TicketId} not Found in database", addCommentDto.TicketId); + _logger.LogWarning("Ticket {TicketId} not Found in database", addCommentDto.TicketId); return NotFound(ApiResponse.ErrorResponse("Ticket not Found", "Ticket not Found", 404)); } @@ -379,7 +379,7 @@ namespace Marco.Pms.Services.Controllers var Image = attachmentDto; if (string.IsNullOrEmpty(Image.Base64Data)) { - _logger.LogError("Base64 data is missing"); + _logger.LogWarning("Base64 data is missing"); return BadRequest(ApiResponse.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); } @@ -437,7 +437,7 @@ namespace Marco.Pms.Services.Controllers .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - _logger.LogError("{error}", errors); + _logger.LogWarning("{error}", errors); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } @@ -451,7 +451,7 @@ namespace Marco.Pms.Services.Controllers if (ticket == null) { - _logger.LogError("Ticket {TicketId} not Found in database", updateCommentDto.TicketId); + _logger.LogWarning("Ticket {TicketId} not Found in database", updateCommentDto.TicketId); return NotFound(ApiResponse.ErrorResponse("Ticket not Found", "Ticket not Found", 404)); } @@ -474,7 +474,7 @@ namespace Marco.Pms.Services.Controllers var Image = attachmentDto; if (string.IsNullOrEmpty(Image.Base64Data)) { - _logger.LogError("Base64 data is missing"); + _logger.LogWarning("Base64 data is missing"); return BadRequest(ApiResponse.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); } @@ -552,7 +552,7 @@ namespace Marco.Pms.Services.Controllers .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - _logger.LogError("{error}", errors); + _logger.LogWarning("{error}", errors); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } @@ -568,7 +568,7 @@ namespace Marco.Pms.Services.Controllers if (tickets == null || tickets.Count > 0) { - _logger.LogError("Tickets not Found in database"); + _logger.LogWarning("Tickets not Found in database"); return NotFound(ApiResponse.ErrorResponse("Ticket not Found", "Ticket not Found", 404)); } @@ -578,12 +578,12 @@ namespace Marco.Pms.Services.Controllers { if (string.IsNullOrEmpty(forumAttachmentDto.Base64Data)) { - _logger.LogError("Base64 data is missing"); + _logger.LogWarning("Base64 data is missing"); return BadRequest(ApiResponse.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); } if (forumAttachmentDto.TicketId == null) { - _logger.LogError("ticket ID is missing"); + _logger.LogWarning("ticket ID is missing"); return BadRequest(ApiResponse.ErrorResponse("ticket ID is missing", "ticket ID is missing", 400)); } var ticket = tickets.FirstOrDefault(t => t.Id == forumAttachmentDto.TicketId); diff --git a/Marco.Pms.Services/Controllers/MasterController.cs b/Marco.Pms.Services/Controllers/MasterController.cs index ebd8998..9000cdf 100644 --- a/Marco.Pms.Services/Controllers/MasterController.cs +++ b/Marco.Pms.Services/Controllers/MasterController.cs @@ -168,7 +168,7 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("activity updated successfully from tenant {tenantId}", tenantId); return Ok(ApiResponse.SuccessResponse(activityVM, "activity updated successfully", 200)); } - _logger.LogError("Activity {ActivityId} not found", id); + _logger.LogWarning("Activity {ActivityId} not found", id); return NotFound(ApiResponse.ErrorResponse("Activity not found", "Activity not found", 404)); } @@ -230,7 +230,7 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Ticket Status master {TicketStatusId} added successfully from tenant {tenantId}", statusMaster.Id, tenantId); return Ok(ApiResponse.SuccessResponse(statusVM, "Ticket Status master added successfully", 200)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("Sent Empty payload", "Sent Empty payload", 400)); } @@ -251,10 +251,10 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Ticket Status master {TicketStatusId} updated successfully from tenant {tenantId}", statusMaster.Id, tenantId); return Ok(ApiResponse.SuccessResponse(statusVM, "Ticket Status master updated successfully", 200)); } - _logger.LogError("Ticket Status master {TicketStatusId} not found in database", statusMasterDto.Id != null ? statusMasterDto.Id.Value : Guid.Empty); + _logger.LogWarning("Ticket Status master {TicketStatusId} not found in database", statusMasterDto.Id != null ? statusMasterDto.Id.Value : Guid.Empty); return NotFound(ApiResponse.ErrorResponse("Ticket Status master not found", "Ticket Status master not found", 404)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("Sent Empty payload", "Sent Empty payload", 400)); } @@ -281,7 +281,7 @@ namespace Marco.Pms.Services.Controllers } else { - _logger.LogError("Ticket Status {TickeStatusId} not found in database", id); + _logger.LogWarning("Ticket Status {TickeStatusId} not found in database", id); return NotFound(ApiResponse.ErrorResponse("Ticket Status not found", "Ticket Status not found", 404)); } } @@ -318,7 +318,7 @@ namespace Marco.Pms.Services.Controllers return Ok(ApiResponse.SuccessResponse(typeVM, "Ticket type master added successfully", 200)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); } @@ -339,10 +339,10 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Ticket Type master {TicketTypeId} updated successfully from tenant {tenantId}", typeMaster.Id, tenantId); return Ok(ApiResponse.SuccessResponse(typeVM, "Ticket type master updated successfully", 200)); } - _logger.LogError("Ticket type master {TicketTypeId} not found in database", typeMasterDto.Id != null ? typeMasterDto.Id.Value : Guid.Empty); + _logger.LogWarning("Ticket type master {TicketTypeId} not found in database", typeMasterDto.Id != null ? typeMasterDto.Id.Value : Guid.Empty); return NotFound(ApiResponse.ErrorResponse("Ticket type master not found", "Ticket type master not found", 404)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); } @@ -369,7 +369,7 @@ namespace Marco.Pms.Services.Controllers } else { - _logger.LogError("Ticket Type {TickeTypeId} not found in database", id); + _logger.LogWarning("Ticket Type {TickeTypeId} not found in database", id); return NotFound(ApiResponse.ErrorResponse("Ticket Type not found", "Ticket Type not found", 404)); } } @@ -407,7 +407,7 @@ namespace Marco.Pms.Services.Controllers return Ok(ApiResponse.SuccessResponse(typeVM, "Ticket Priority master added successfully", 200)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); } [HttpPost("ticket-priorities/edit/{id}")] @@ -427,10 +427,10 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Ticket Priority master {TicketPriorityId} updated successfully from tenant {tenantId}", typeMaster.Id, tenantId); return Ok(ApiResponse.SuccessResponse(typeVM, "Ticket Priority master updated successfully", 200)); } - _logger.LogError("Ticket Priority master {TicketPriorityId} not found in database", priorityMasterDto.Id != null ? priorityMasterDto.Id.Value : Guid.Empty); + _logger.LogWarning("Ticket Priority master {TicketPriorityId} not found in database", priorityMasterDto.Id != null ? priorityMasterDto.Id.Value : Guid.Empty); return NotFound(ApiResponse.ErrorResponse("Ticket Priority master not found", "Ticket Priority master not found", 404)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); } @@ -457,7 +457,7 @@ namespace Marco.Pms.Services.Controllers } else { - _logger.LogError("Ticket Priority {TickePriorityId} not found in database", id); + _logger.LogWarning("Ticket Priority {TickePriorityId} not found in database", id); return NotFound(ApiResponse.ErrorResponse("Ticket Priority not found", "Ticket Priority not found", 404)); } } @@ -494,7 +494,7 @@ namespace Marco.Pms.Services.Controllers return Ok(ApiResponse.SuccessResponse(typeVM, "Ticket tag master added successfully", 200)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); } @@ -515,10 +515,10 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Ticket Tag master {TicketTypeId} updated successfully from tenant {tenantId}", tagMaster.Id, tenantId); return Ok(ApiResponse.SuccessResponse(typeVM, "Ticket tag master updated successfully", 200)); } - _logger.LogError("Ticket tag master {TicketTypeId} not found in database", tagMasterDto.Id != null ? tagMasterDto.Id.Value : Guid.Empty); + _logger.LogWarning("Ticket tag master {TicketTypeId} not found in database", tagMasterDto.Id != null ? tagMasterDto.Id.Value : Guid.Empty); return NotFound(ApiResponse.ErrorResponse("Ticket tag master not found", "Ticket tag master not found", 404)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); } @@ -545,7 +545,7 @@ namespace Marco.Pms.Services.Controllers } else { - _logger.LogError("Ticket Tag {TickeTagId} not found in database", id); + _logger.LogWarning("Ticket Tag {TickeTagId} not found in database", id); return NotFound(ApiResponse.ErrorResponse("Ticket tag not found", "Ticket tag not found", 404)); } } @@ -609,7 +609,7 @@ namespace Marco.Pms.Services.Controllers return Ok(ApiResponse.SuccessResponse(workCategoryMasterVM, "Work category master added successfully", 200)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); } @@ -624,7 +624,7 @@ namespace Marco.Pms.Services.Controllers { if (workCategory.IsSystem) { - _logger.LogError("User tries to update system-defined work category"); + _logger.LogWarning("User tries to update system-defined work category"); return BadRequest(ApiResponse.ErrorResponse("Cannot update system-defined work", "Cannot update system-defined work", 400)); } workCategory = workCategoryMasterDto.ToWorkCategoryMasterFromWorkCategoryMasterDto(tenantId); @@ -635,10 +635,10 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Work category master {WorkCategoryId} updated successfully from tenant {tenantId}", workCategory.Id, tenantId); return Ok(ApiResponse.SuccessResponse(workCategoryMasterVM, "Work category master updated successfully", 200)); } - _logger.LogError("Work category master {WorkCategoryId} not found in database", workCategoryMasterDto.Id ?? Guid.Empty); + _logger.LogWarning("Work category master {WorkCategoryId} not found in database", workCategoryMasterDto.Id ?? Guid.Empty); return NotFound(ApiResponse.ErrorResponse("Work category master not found", "Work category master not found", 404)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); } @@ -666,7 +666,7 @@ namespace Marco.Pms.Services.Controllers } else { - _logger.LogError("Work category {WorkCategoryId} not found in database", id); + _logger.LogWarning("Work category {WorkCategoryId} not found in database", id); return NotFound(ApiResponse.ErrorResponse("Work category not found", "Work category not found", 404)); } } @@ -689,7 +689,7 @@ namespace Marco.Pms.Services.Controllers .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - _logger.LogError("User sent Invalid Date while marking attendance"); + _logger.LogWarning("User sent Invalid Date while marking attendance"); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } var response = await _masterHelper.CreateWorkStatus(createWorkStatusDto); @@ -803,7 +803,7 @@ namespace Marco.Pms.Services.Controllers .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - _logger.LogError("User sent Invalid Date while marking attendance"); + _logger.LogWarning("User sent Invalid Date while marking attendance"); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } var response = await _masterHelper.CreateContactTag(contactTagDto); diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index e7d257f..236e0cb 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -6,19 +6,18 @@ using Marco.Pms.Model.Mapper; using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; -using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Model.ViewModels.Projects; using Marco.Pms.Services.Helpers; -using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Service; using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.SignalR; +using Microsoft.CodeAnalysis; using Microsoft.EntityFrameworkCore; using MongoDB.Driver; +using Project = Marco.Pms.Model.Projects.Project; namespace MarcoBMS.Services.Controllers { @@ -31,14 +30,20 @@ namespace MarcoBMS.Services.Controllers private readonly ApplicationDbContext _context; private readonly UserHelper _userHelper; private readonly ILoggingService _logger; - private readonly IHubContext _signalR; + private readonly ISignalRService _signalR; private readonly PermissionServices _permission; private readonly CacheUpdateHelper _cache; private readonly Guid tenantId; - public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, - IHubContext signalR, CacheUpdateHelper cache, PermissionServices permission, IProjectServices projectServices) + public ProjectController( + ApplicationDbContext context, + UserHelper userHelper, + ILoggingService logger, + ISignalRService signalR, + CacheUpdateHelper cache, + PermissionServices permission, + IProjectServices projectServices) { _context = context; _userHelper = userHelper; @@ -174,7 +179,7 @@ namespace MarcoBMS.Services.Controllers if (response.Success) { var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Create_Project", Response = response.Data }; - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + await _signalR.SendNotificationAsync(notification); } return StatusCode(response.StatusCode, response); } @@ -204,7 +209,7 @@ namespace MarcoBMS.Services.Controllers if (response.Success) { var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Update_Project", Response = response.Data }; - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + await _signalR.SendNotificationAsync(notification); } return StatusCode(response.StatusCode, response); } @@ -213,90 +218,38 @@ namespace MarcoBMS.Services.Controllers #region =================================================================== Project Allocation APIs =================================================================== - [HttpGet] - [Route("employees/get/{projectid?}/{includeInactive?}")] - public async Task GetEmployeeByProjectID(Guid? projectid, bool includeInactive = false) + [HttpGet("employees/get/{projectid?}/{includeInactive?}")] + public async Task GetEmployeeByProjectId(Guid? projectId, bool includeInactive = false) { + // --- Step 1: Input Validation --- if (!ModelState.IsValid) { - var errors = ModelState.Values - .SelectMany(v => v.Errors) - .Select(e => e.ErrorMessage) - .ToList(); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); - + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("Get employee list by ProjectId called with invalid model state \n Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - if (projectid != null) - { - // Fetch assigned project - List result = new List(); - - if ((bool)includeInactive) - { - - result = await (from rpm in _context.Employees.Include(c => c.JobRole) - join fp in _context.ProjectAllocations.Where(c => c.TenantId == tenantId && c.ProjectId == projectid) - on rpm.Id equals fp.EmployeeId - select rpm).ToListAsync(); - } - else - { - result = await (from rpm in _context.Employees.Include(c => c.JobRole) - join fp in _context.ProjectAllocations.Where(c => c.TenantId == tenantId && c.ProjectId == projectid && c.IsActive) - on rpm.Id equals fp.EmployeeId - select rpm).ToListAsync(); - } - - List resultVM = new List(); - foreach (Employee employee in result) - { - EmployeeVM vm = employee.ToEmployeeVMFromEmployee(); - resultVM.Add(vm); - } - - return Ok(ApiResponse.SuccessResponse(resultVM, "Success.", 200)); - } - else - { - return NotFound(ApiResponse.ErrorResponse("Invalid Input Parameter", 404)); - } - - + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetEmployeeByProjectIdAsync(projectId, includeInactive, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } - [HttpGet] - [Route("allocation/{projectId}")] + [HttpGet("allocation/{projectId}")] public async Task GetProjectAllocation(Guid? projectId) { + // --- Step 1: Input Validation --- if (!ModelState.IsValid) { - var errors = ModelState.Values - .SelectMany(v => v.Errors) - .Select(e => e.ErrorMessage) - .ToList(); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); - + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("Get employee list by ProjectId called with invalid model state \n Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - var employees = await _context.ProjectAllocations - .Where(c => c.TenantId == tenantId && c.ProjectId == projectId && c.Employee != null) - .Include(e => e.Employee) - .Select(e => new - { - ID = e.Id, - EmployeeId = e.EmployeeId, - ProjectId = e.ProjectId, - AllocationDate = e.AllocationDate, - ReAllocationDate = e.ReAllocationDate, - FirstName = e.Employee != null ? e.Employee.FirstName : string.Empty, - LastName = e.Employee != null ? e.Employee.LastName : string.Empty, - MiddleName = e.Employee != null ? e.Employee.MiddleName : string.Empty, - IsActive = e.IsActive, - JobRoleId = (e.JobRoleId != null ? e.JobRoleId : e.Employee != null ? e.Employee.JobRoleId : null) - }).ToListAsync(); - - return Ok(ApiResponse.SuccessResponse(employees, "Success.", 200)); + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetProjectAllocationAsync(projectId, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } [HttpPost("allocation")] @@ -375,7 +328,7 @@ namespace MarcoBMS.Services.Controllers } var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeList = employeeIds }; - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + await _signalR.SendNotificationAsync(notification); return Ok(ApiResponse.SuccessResponse(result, "Data saved successfully", 200)); } @@ -494,7 +447,7 @@ namespace MarcoBMS.Services.Controllers await _cache.ClearAllProjectIds(employeeId); var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeId = employeeId }; - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + await _signalR.SendNotificationAsync(notification); return Ok(ApiResponse.SuccessResponse(result, "Data saved successfully", 200)); } @@ -799,7 +752,7 @@ namespace MarcoBMS.Services.Controllers var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = message }; - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + await _signalR.SendNotificationAsync(notification); return Ok(ApiResponse.SuccessResponse(responseList, responseMessage, 200)); } @@ -826,9 +779,15 @@ namespace MarcoBMS.Services.Controllers workAreaIds.Add(task.WorkAreaId); + var projectId = floor?.Building?.ProjectId; var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = $"Task Deleted in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}, in Area: {task.WorkArea?.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}" }; - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + await _signalR.SendNotificationAsync(notification); + await _cache.DeleteWorkItemByIdAsync(task.Id); + if (projectId != null) + { + await _cache.DeleteProjectByIdAsync(projectId.Value); + } } else { @@ -847,7 +806,7 @@ namespace MarcoBMS.Services.Controllers } else { - _logger.LogError("Task with ID {WorkItemId} not found ID database", id); + _logger.LogWarning("Task with ID {WorkItemId} not found ID database", id); } return Ok(ApiResponse.SuccessResponse(new { }, "Task deleted successfully", 200)); } @@ -973,7 +932,7 @@ namespace MarcoBMS.Services.Controllers message = $"{message} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}"; var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = message }; - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + await _signalR.SendNotificationAsync(notification); return Ok(ApiResponse.SuccessResponse(responseData, responseMessage, 200)); } return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Infra Details are not valid.", 400)); diff --git a/Marco.Pms.Services/Controllers/ReportController.cs b/Marco.Pms.Services/Controllers/ReportController.cs index 717a273..87382d7 100644 --- a/Marco.Pms.Services/Controllers/ReportController.cs +++ b/Marco.Pms.Services/Controllers/ReportController.cs @@ -106,7 +106,7 @@ namespace Marco.Pms.Services.Controllers } catch (Exception ex) { - _logger.LogError("Database Error: Failed to check existence of MailListId '{MailListId}' for TenantId: {TenantId}. : {Error}", mailDetailsDto.MailListId, tenantId, ex.Message); + _logger.LogError(ex, "Database Error: Failed to check existence of MailListId '{MailListId}' for TenantId: {TenantId}.", mailDetailsDto.MailListId, tenantId); return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An error occurred while validating mail template.", 500)); } @@ -143,13 +143,13 @@ namespace Marco.Pms.Services.Controllers } catch (DbUpdateException dbEx) { - _logger.LogError("Database Error: Failed to save new mail details for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}. : {Error}", newMailDetails.ProjectId, newMailDetails.Recipient, tenantId, dbEx.Message); + _logger.LogError(dbEx, "Database Error: Failed to save new mail details for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}.", newMailDetails.ProjectId, newMailDetails.Recipient, tenantId); // Check for specific constraint violations if applicable (e.g., duplicate recipient for a project) return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An error occurred while saving the mail details.", 500)); } catch (Exception ex) { - _logger.LogError("Unexpected Error: An unhandled exception occurred while adding mail details for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}. : {Error}", newMailDetails.ProjectId, newMailDetails.Recipient, tenantId, ex.Message); + _logger.LogError(ex, "Unexpected Error: An unhandled exception occurred while adding mail details for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}.", newMailDetails.ProjectId, newMailDetails.Recipient, tenantId); return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500)); } } @@ -234,7 +234,7 @@ namespace Marco.Pms.Services.Controllers } catch (Exception ex) { - _logger.LogError("Database Error: Failed to check for existing mail template with title '{Title}' for TenantId: {TenantId}.: {Error}", mailTemplateDto.Title, tenantId, ex.Message); + _logger.LogError(ex, "Database Error: Failed to check for existing mail template with title '{Title}' for TenantId: {TenantId}.", mailTemplateDto.Title, tenantId); return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An error occurred while checking for existing templates.", 500)); } @@ -270,12 +270,12 @@ namespace Marco.Pms.Services.Controllers } catch (DbUpdateException dbEx) { - _logger.LogError("Database Error: Failed to save new mail template '{Title}' for TenantId: {TenantId}. : {Error}", mailTemplateDto.Title, tenantId, dbEx.Message); + _logger.LogError(dbEx, "Database Error: Failed to save new mail template '{Title}' for TenantId: {TenantId}. : {Error}", mailTemplateDto.Title, tenantId); return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An error occurred while saving the mail template.", 500)); } catch (Exception ex) { - _logger.LogError("Unexpected Error: An unhandled exception occurred while adding mail template '{Title}' for TenantId: {TenantId}. : {Error}", mailTemplateDto.Title, tenantId, ex.Message); + _logger.LogError(ex, "Unexpected Error: An unhandled exception occurred while adding mail template '{Title}' for TenantId: {TenantId}.", mailTemplateDto.Title, tenantId); return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500)); } } @@ -350,7 +350,7 @@ namespace Marco.Pms.Services.Controllers { // 3. OPTIMIZATION: Make the process resilient. // If one task fails unexpectedly, log it and continue with others. - _logger.LogError("Failed to send report for project {ProjectId} : {Error}", mailGroup.ProjectId, ex.Message); + _logger.LogError(ex, "Failed to send report for project {ProjectId}", mailGroup.ProjectId); Interlocked.Increment(ref failureCount); } } @@ -527,7 +527,7 @@ namespace Marco.Pms.Services.Controllers catch (Exception ex) { // It's good practice to log any unexpected errors within a concurrent task. - _logger.LogError("Failed to process project report for ProjectId {ProjectId} : {Error}", group.ProjectId, ex.Message); + _logger.LogError(ex, "Failed to process project report for ProjectId {ProjectId}", group.ProjectId); } } }); diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 5bae90f..aca439b 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -118,8 +118,8 @@ namespace Marco.Pms.Services.Helpers projectDetails.ProjectStatus = new StatusMasterMongoDB { - Id = status?.Id.ToString(), - Status = status?.Status + Id = status!.Id.ToString(), + Status = status.Status }; // Use fast in-memory lookups instead of .Where() in loops. @@ -797,7 +797,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogError("Error occured while fetching project report mail bodys: {Error}", ex.Message); + _logger.LogError(ex, "Error occured while fetching project report mail bodys"); return null; } } @@ -809,7 +809,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogError("Error occured while adding project report mail bodys: {Error}", ex.Message); + _logger.LogError(ex, "Error occured while adding project report mail bodys"); } } } diff --git a/Marco.Pms.Services/Helpers/DirectoryHelper.cs b/Marco.Pms.Services/Helpers/DirectoryHelper.cs index 37f58cf..f8e1b07 100644 --- a/Marco.Pms.Services/Helpers/DirectoryHelper.cs +++ b/Marco.Pms.Services/Helpers/DirectoryHelper.cs @@ -52,7 +52,7 @@ namespace Marco.Pms.Services.Helpers } else { - _logger.LogError("Employee {EmployeeId} attemped to access a contacts, but do not have permission", LoggedInEmployee.Id); + _logger.LogWarning("Employee {EmployeeId} attemped to access a contacts, but do not have permission", LoggedInEmployee.Id); return ApiResponse.ErrorResponse("You don't have permission", "You don't have permission", 401); } @@ -202,7 +202,7 @@ namespace Marco.Pms.Services.Helpers } else { - _logger.LogError("Employee {EmployeeId} attemped to access a contacts with in bucket {BucketId}, but do not have permission", LoggedInEmployee.Id, id); + _logger.LogWarning("Employee {EmployeeId} attemped to access a contacts with in bucket {BucketId}, but do not have permission", LoggedInEmployee.Id, id); return ApiResponse.ErrorResponse("You don't have permission", "You don't have permission", 401); } @@ -490,7 +490,7 @@ namespace Marco.Pms.Services.Helpers } else { - _logger.LogError("Employee {EmployeeId} attemped to update a contact, but do not have permission", LoggedInEmployee.Id); + _logger.LogWarning("Employee {EmployeeId} attemped to update a contact, but do not have permission", LoggedInEmployee.Id); return ApiResponse.ErrorResponse("You don't have permission", "You don't have permission", 401); } @@ -1169,7 +1169,7 @@ namespace Marco.Pms.Services.Helpers } else { - _logger.LogError("Employee {EmployeeId} attemped to access a buckets list, but do not have permission", LoggedInEmployee.Id); + _logger.LogWarning("Employee {EmployeeId} attemped to access a buckets list, but do not have permission", LoggedInEmployee.Id); return ApiResponse.ErrorResponse("You don't have permission", "You don't have permission", 401); } @@ -1204,7 +1204,7 @@ namespace Marco.Pms.Services.Helpers var demo = !permissionIds.Contains(PermissionsMaster.DirectoryUser); if (!permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && !permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && !permissionIds.Contains(PermissionsMaster.DirectoryUser)) { - _logger.LogError("Employee {EmployeeId} attemped to create a bucket, but do not have permission", LoggedInEmployee.Id); + _logger.LogWarning("Employee {EmployeeId} attemped to create a bucket, but do not have permission", LoggedInEmployee.Id); return ApiResponse.ErrorResponse("You don't have permission", "You don't have permission", 401); } @@ -1276,7 +1276,7 @@ namespace Marco.Pms.Services.Helpers } if (accessableBucket == null) { - _logger.LogError("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id); + _logger.LogWarning("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id); return ApiResponse.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 401); } @@ -1342,7 +1342,7 @@ namespace Marco.Pms.Services.Helpers } if (accessableBucket == null) { - _logger.LogError("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id); + _logger.LogWarning("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id); return ApiResponse.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 401); } var employeeIds = await _context.Employees.Where(e => e.TenantId == tenantId && e.IsActive).Select(e => e.Id).ToListAsync(); @@ -1396,7 +1396,7 @@ namespace Marco.Pms.Services.Helpers } if (removededEmployee > 0) { - _logger.LogError("Employee {EmployeeId} removed {conut} number of employees from bucket {BucketId}", LoggedInEmployee.Id, removededEmployee, bucketId); + _logger.LogWarning("Employee {EmployeeId} removed {conut} number of employees from bucket {BucketId}", LoggedInEmployee.Id, removededEmployee, bucketId); } return ApiResponse.SuccessResponse(bucketVM, "Details updated successfully", 200); } @@ -1443,7 +1443,7 @@ namespace Marco.Pms.Services.Helpers } if (accessableBucket == null) { - _logger.LogError("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id); + _logger.LogWarning("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id); return ApiResponse.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 401); } diff --git a/Marco.Pms.Services/Helpers/EmployeeHelper.cs b/Marco.Pms.Services/Helpers/EmployeeHelper.cs index 03184e5..17e5746 100644 --- a/Marco.Pms.Services/Helpers/EmployeeHelper.cs +++ b/Marco.Pms.Services/Helpers/EmployeeHelper.cs @@ -33,7 +33,7 @@ namespace MarcoBMS.Services.Helpers } catch (Exception ex) { - _logger.LogError("{Error}", ex.Message); + _logger.LogError(ex, "Error occured while fetching employee by application user ID {ApplicationUserId}", ApplicationUserID); return new Employee(); } } @@ -66,7 +66,7 @@ namespace MarcoBMS.Services.Helpers } catch (Exception ex) { - _logger.LogError("{Error}", ex.Message); + _logger.LogError(ex, "Error occoured while filtering employees by string {SearchString} or project {ProjectId}", searchString, ProjectId ?? Guid.Empty); return new List(); } } @@ -102,7 +102,7 @@ namespace MarcoBMS.Services.Helpers } catch (Exception ex) { - _logger.LogError("{Error}", ex.Message); + _logger.LogError(ex, "Error occured while featching list of employee by project ID {ProjectId}", ProjectId ?? Guid.Empty); return new List(); } } diff --git a/Marco.Pms.Services/Helpers/MasterHelper.cs b/Marco.Pms.Services/Helpers/MasterHelper.cs index f994639..83bc007 100644 --- a/Marco.Pms.Services/Helpers/MasterHelper.cs +++ b/Marco.Pms.Services/Helpers/MasterHelper.cs @@ -218,7 +218,7 @@ namespace Marco.Pms.Services.Helpers _logger.LogInfo("Contact tag master {ConatctTagId} updated successfully by employee {EmployeeId}", contactTagVm.Id, LoggedInEmployee.Id); return ApiResponse.SuccessResponse(contactTagVm, "Contact Tag master updated successfully", 200); } - _logger.LogError("Contact Tag master {ContactTagId} not found in database", id); + _logger.LogWarning("Contact Tag master {ContactTagId} not found in database", id); return ApiResponse.ErrorResponse("Contact Tag master not found", "Contact tag master not found", 404); } _logger.LogWarning("Employee with ID {LoggedInEmployeeId} sended empty payload", LoggedInEmployee.Id); @@ -294,7 +294,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogError("Error occurred while fetching work status list : {Error}", ex.Message); + _logger.LogWarning("Error occurred while fetching work status list : {Error}", ex.Message); return ApiResponse.ErrorResponse("An error occurred", "Unable to fetch work status list", 500); } } @@ -343,7 +343,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogError("Error occurred while creating work status : {Error}", ex.Message); + _logger.LogWarning("Error occurred while creating work status : {Error}", ex.Message); return ApiResponse.ErrorResponse("An error occurred", "Unable to create work status", 500); } } @@ -403,7 +403,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogError("Error occurred while updating work status ID: {Id} : {Error}", id, ex.Message); + _logger.LogError(ex, "Error occurred while updating work status ID: {Id}", id); return ApiResponse.ErrorResponse("An error occurred", "Unable to update the work status at this time", 500); } } @@ -458,7 +458,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogError("Error occurred while deleting WorkStatus Id: {Id} : {Error}", id, ex.Message); + _logger.LogError(ex, "Error occurred while deleting WorkStatus Id: {Id}", id); return ApiResponse.ErrorResponse("An error occurred", "Unable to delete work status", 500); } } diff --git a/Marco.Pms.Services/Helpers/ReportHelper.cs b/Marco.Pms.Services/Helpers/ReportHelper.cs index 4ec0978..4ec9453 100644 --- a/Marco.Pms.Services/Helpers/ReportHelper.cs +++ b/Marco.Pms.Services/Helpers/ReportHelper.cs @@ -289,13 +289,13 @@ namespace Marco.Pms.Services.Helpers // --- Input Validation --- if (projectId == Guid.Empty) { - _logger.LogError("Validation Error: Provided empty project ID while fetching project report."); + _logger.LogWarning("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); + _logger.LogWarning("Validation Error: No recipient emails provided for project ID {ProjectId}.", projectId); return ApiResponse.ErrorResponse("No recipient emails provided.", "No recipient emails provided.", 400); } @@ -316,7 +316,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogError("Email Sending Error: Failed to send project statistics email for project ID {ProjectId}. : {Error}", projectId, ex.Message); + _logger.LogError(ex, "Email Sending Error: Failed to send project statistics email for project ID {ProjectId}.", projectId); return ApiResponse.ErrorResponse("Failed to send email.", "An error occurred while sending the email.", 500); } @@ -350,14 +350,14 @@ namespace Marco.Pms.Services.Helpers } catch (DbUpdateException dbEx) { - _logger.LogError("Database Error: Failed to save mail logs for project ID {ProjectId}. : {Error}", projectId, dbEx.Message); + _logger.LogError(dbEx, "Database Error: Failed to save mail logs for project ID {ProjectId}.", projectId); // 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); + _logger.LogError(ex, "Unexpected Error: An unhandled exception occurred while processing project statistics for project ID {ProjectId}.", projectId); return ApiResponse.ErrorResponse("An unexpected error occurred.", "An unexpected error occurred.", 500); } } diff --git a/Marco.Pms.Services/Helpers/RolesHelper.cs b/Marco.Pms.Services/Helpers/RolesHelper.cs index cd73c0f..ef9f824 100644 --- a/Marco.Pms.Services/Helpers/RolesHelper.cs +++ b/Marco.Pms.Services/Helpers/RolesHelper.cs @@ -84,7 +84,7 @@ namespace MarcoBMS.Services.Helpers } catch (Exception ex) { - _logger.LogError("An error occurred while fetching permissions for EmployeeId {EmployeeId} : {Error}", EmployeeId, ex.Message); + _logger.LogError(ex, "An error occurred while fetching permissions for EmployeeId {EmployeeId}", EmployeeId); return new List(); } } @@ -144,7 +144,7 @@ namespace MarcoBMS.Services.Helpers } catch (Exception ex) { - _logger.LogError("An error occurred while fetching permissions for RoleId {RoleId}: {Error}", roleId, ex.Message); + _logger.LogError(ex, "An error occurred while fetching permissions for RoleId {RoleId}", roleId); // Return an empty list as a safe default to prevent downstream failures. return new List(); } diff --git a/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs similarity index 75% rename from Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs rename to Marco.Pms.Services/MappingProfiles/MappingProfile.cs index b811056..7d627bc 100644 --- a/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -1,16 +1,19 @@ using AutoMapper; using Marco.Pms.Model.Dtos.Project; +using Marco.Pms.Model.Employees; using Marco.Pms.Model.Master; using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.Projects; +using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Model.ViewModels.Projects; namespace Marco.Pms.Services.MappingProfiles { - public class ProjectMappingProfile : Profile + public class MappingProfile : Profile { - public ProjectMappingProfile() + public MappingProfile() { + #region ======================================================= Projects ======================================================= // Your mappings CreateMap(); CreateMap(); @@ -40,6 +43,11 @@ namespace Marco.Pms.Services.MappingProfiles CreateMap(); CreateMap(); CreateMap(); + #endregion + + #region ======================================================= Projects ======================================================= + CreateMap(); + #endregion } } } diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index 6553745..26d8eba 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -158,6 +158,7 @@ builder.Services.AddTransient(); #region Customs Services builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); #endregion diff --git a/Marco.Pms.Services/Service/ILoggingService.cs b/Marco.Pms.Services/Service/ILoggingService.cs index b835d0c..6d795cd 100644 --- a/Marco.Pms.Services/Service/ILoggingService.cs +++ b/Marco.Pms.Services/Service/ILoggingService.cs @@ -5,7 +5,7 @@ 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); + void LogError(Exception? ex, string? message, params object[]? args); } } diff --git a/Marco.Pms.Services/Service/LoggingServices.cs b/Marco.Pms.Services/Service/LoggingServices.cs index 5a016de..751f22c 100644 --- a/Marco.Pms.Services/Service/LoggingServices.cs +++ b/Marco.Pms.Services/Service/LoggingServices.cs @@ -11,16 +11,16 @@ namespace MarcoBMS.Services.Service _logger = logger; } - public void LogError(string? message, params object[]? args) + public void LogError(Exception? ex, string? message, params object[]? args) { using (LogContext.PushProperty("LogLevel", "Error")) if (args != null) { - _logger.LogError(message, args); + _logger.LogError(ex, message, args); } else { - _logger.LogError(message); + _logger.LogError(ex, message); } } diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs index 3280558..dcaf20e 100644 --- a/Marco.Pms.Services/Service/ProjectServices.cs +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -1,4 +1,5 @@ using AutoMapper; +using AutoMapper.QueryableExtensions; using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Activities; using Marco.Pms.Model.Dtos.Project; @@ -7,12 +8,15 @@ using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; +using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Model.ViewModels.Projects; using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; +using Microsoft.CodeAnalysis; using Microsoft.EntityFrameworkCore; +using Project = Marco.Pms.Model.Projects.Project; namespace Marco.Pms.Services.Service { @@ -75,7 +79,7 @@ namespace Marco.Pms.Services.Service catch (Exception ex) { // --- Step 5: Graceful Error Handling --- - _logger.LogError("An unexpected error occurred in GetAllProjectsBasic for tenant {TenantId}. \n {Error}", tenantId, ex.Message); + _logger.LogError(ex, "An unexpected error occurred in GetAllProjectsBasic for tenant {TenantId}.", tenantId); return ApiResponse.ErrorResponse("An internal server error occurred. Please try again later.", null, 500); } } @@ -134,7 +138,7 @@ namespace Marco.Pms.Services.Service catch (Exception ex) { // --- Step 5: Graceful Error Handling --- - _logger.LogError("An unexpected error occurred in GetAllProjects for tenant {TenantId}. \n {Error}", tenantId, ex.Message); + _logger.LogError(ex, "An unexpected error occurred in GetAllProjects for tenant {TenantId}.", tenantId); return ApiResponse.ErrorResponse("An internal server error occurred. Please try again later.", null, 500); } } @@ -178,7 +182,7 @@ namespace Marco.Pms.Services.Service } catch (Exception ex) { - _logger.LogError("An unexpected error occurred while getting project {ProjectId} : \n {Error}", id, ex.Message); + _logger.LogError(ex, "An unexpected error occurred while getting project {ProjectId}", id); return ApiResponse.ErrorResponse("An internal server error occurred.", null, 500); } } @@ -244,7 +248,7 @@ namespace Marco.Pms.Services.Service catch (Exception ex) { // --- Step 5: Graceful Error Handling --- - _logger.LogError("An unexpected error occurred in Get Project Details for project {ProjectId} for tenant {TenantId}. \n {Error}", id, tenantId, ex.Message); + _logger.LogError(ex, "An unexpected error occurred in Get Project Details for project {ProjectId} for tenant {TenantId}. ", id, tenantId); return ApiResponse.ErrorResponse("An internal server error occurred. Please try again later.", null, 500); } } @@ -360,7 +364,7 @@ namespace Marco.Pms.Services.Service catch (Exception ex) { // Log the detailed exception - _logger.LogError("Failed to create project in database. Rolling back transaction. \n {Error}", ex.Message); + _logger.LogError(ex, "Failed to create project in database. Rolling back transaction."); // Return a server error as the primary operation failed return ApiResponse.ErrorResponse("An error occurred while saving the project.", ex.Message, 500); } @@ -379,7 +383,7 @@ namespace Marco.Pms.Services.Service { // The project was created successfully, but a side-effect failed. // Log this as a warning, as the primary operation succeeded. Don't return an error to the user. - _logger.LogWarning("Project {ProjectId} was created, but a post-creation side-effect (caching/notification) failed. \n {Error}", project.Id, ex.Message); + _logger.LogError(ex, "Project {ProjectId} was created, but a post-creation side-effect (caching/notification) failed. ", project.Id); } // 4. Return a success response to the user as soon as the critical data is saved. @@ -435,7 +439,7 @@ namespace Marco.Pms.Services.Service { // --- Step 3: Handle Concurrency Conflicts --- // This happens if another user modified the project after we fetched it. - _logger.LogWarning("Concurrency conflict while updating project {ProjectId} \n {Error}", id, ex.Message); + _logger.LogError(ex, "Concurrency conflict while updating project {ProjectId} ", id); return ApiResponse.ErrorResponse("Conflict occurred.", "This project has been modified by someone else. Please refresh and try again.", 409); } @@ -458,13 +462,216 @@ namespace Marco.Pms.Services.Service catch (Exception ex) { // --- Step 6: Graceful Error Handling for Unexpected Errors --- - _logger.LogError("An unexpected error occurred while updating project {ProjectId} \n {Error}", id, ex.Message); + _logger.LogError(ex, "An unexpected error occurred while updating project {ProjectId} ", id); return ApiResponse.ErrorResponse("An internal server error occurred.", null, 500); } } #endregion + #region =================================================================== Project Allocation APIs =================================================================== + + public async Task> GetEmployeeByProjectID(Guid? projectid, bool includeInactive, Guid tenantId, Employee loggedInEmployee) + { + if (projectid == null) + { + return ApiResponse.ErrorResponse("Invalid Input Parameter", 404); + } + // Fetch assigned project + List result = new List(); + + var employeeQuery = _context.ProjectAllocations + .Include(pa => pa.Employee) + .Where(pa => pa.ProjectId == projectid && pa.TenantId == tenantId && pa.Employee != null); + + if (includeInactive) + { + + result = await employeeQuery.Select(pa => pa.Employee ?? new Employee()).ToListAsync(); + } + else + { + result = await employeeQuery + .Where(pa => pa.IsActive) + .Select(pa => pa.Employee ?? new Employee()).ToListAsync(); + } + + List resultVM = new List(); + foreach (Employee employee in result) + { + EmployeeVM vm = _mapper.Map(employee); + resultVM.Add(vm); + } + + return ApiResponse.SuccessResponse(resultVM, "Successfully fetched the list of employees for seleted project", 200); + } + + /// + /// Retrieves a list of employees for a specific project. + /// This method is optimized to perform all filtering and mapping on the database server. + /// + /// The ID of the project. + /// Whether to include employees from inactive allocations. + /// The ID of the current tenant. + /// The current authenticated employee (used for permission checks). + /// An ApiResponse containing a list of employees or an error. + public async Task> GetEmployeeByProjectIdAsync(Guid? projectId, bool includeInactive, Guid tenantId, Employee loggedInEmployee) + { + // --- Step 1: Input Validation --- + if (projectId == null) + { + _logger.LogWarning("GetEmployeeByProjectID called with a null projectId."); + // 400 Bad Request is more appropriate for invalid input than 404 Not Found. + return ApiResponse.ErrorResponse("Project ID is required.", "Invalid Input Parameter", 400); + } + + _logger.LogInfo("Fetching employees for ProjectID: {ProjectId}, IncludeInactive: {IncludeInactive}", projectId, includeInactive); + + try + { + // --- CRITICAL: Security Check --- + // Before fetching data, you MUST verify the user has permission to see it. + // This is a placeholder for your actual permission logic. + var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.Value); + var hasAllEmployeePermission = await _permission.HasPermission(PermissionsMaster.ViewAllEmployees, loggedInEmployee.Id); + var hasviewTeamPermission = await _permission.HasPermission(PermissionsMaster.ViewTeamMembers, loggedInEmployee.Id); + + if (!(hasProjectPermission && (hasAllEmployeePermission || hasviewTeamPermission))) + { + _logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, projectId); + return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to view this project's team.", 403); + } + + // --- Step 2: Build a Single, Efficient IQueryable --- + // We start with the base query and conditionally add filters before executing it. + // This avoids code duplication and is highly performant. + var employeeQuery = _context.ProjectAllocations + .Where(pa => pa.ProjectId == projectId && pa.TenantId == tenantId); + + // Conditionally apply the filter for active allocations. + if (!includeInactive) + { + employeeQuery = employeeQuery.Where(pa => pa.IsActive); + } + + // --- Step 3: Project Directly to the ViewModel on the Database Server --- + // This is the most significant performance optimization. + // Instead of fetching full Employee entities, we select only the data needed for the EmployeeVM. + // AutoMapper's ProjectTo is perfect for this, as it translates the mapping configuration into an efficient SQL SELECT statement. + var resultVM = await employeeQuery + .Where(pa => pa.Employee != null) // Safety check for data integrity + .Select(pa => pa.Employee) // Navigate to the Employee entity + .ProjectTo(_mapper.ConfigurationProvider) // Let AutoMapper generate the SELECT + .ToListAsync(); + + _logger.LogInfo("Successfully fetched {EmployeeCount} employees for project {ProjectId}.", resultVM.Count, projectId); + + // Note: The original mapping loop is now completely gone, replaced by the single efficient query above. + + return ApiResponse.SuccessResponse(resultVM, "Successfully fetched the list of employees for the selected project.", 200); + } + catch (Exception ex) + { + // --- Step 4: Graceful Error Handling --- + _logger.LogError(ex, "An error occurred while fetching employees for project {ProjectId}. ", projectId); + return ApiResponse.ErrorResponse("An internal server error occurred.", "Database Query Failed", 500); + } + } + + public async Task> GetProjectAllocation(Guid? projectId, Guid tenantId, Employee loggedInEmployee) + { + var employees = await _context.ProjectAllocations + .Where(c => c.TenantId == tenantId && c.ProjectId == projectId && c.Employee != null) + .Include(e => e.Employee) + .Select(e => new + { + ID = e.Id, + EmployeeId = e.EmployeeId, + ProjectId = e.ProjectId, + AllocationDate = e.AllocationDate, + ReAllocationDate = e.ReAllocationDate, + FirstName = e.Employee != null ? e.Employee.FirstName : string.Empty, + LastName = e.Employee != null ? e.Employee.LastName : string.Empty, + MiddleName = e.Employee != null ? e.Employee.MiddleName : string.Empty, + IsActive = e.IsActive, + JobRoleId = (e.JobRoleId != null ? e.JobRoleId : e.Employee != null ? e.Employee.JobRoleId : null) + }).ToListAsync(); + + return ApiResponse.SuccessResponse(employees, "Success.", 200); + } + + /// + /// Retrieves project allocation details for a specific project. + /// This method is optimized for performance and includes security checks. + /// + /// The ID of the project. + /// The ID of the current tenant. + /// The current authenticated employee for permission checks. + /// An ApiResponse containing allocation details or an appropriate error. + public async Task> GetProjectAllocationAsync(Guid? projectId, Guid tenantId, Employee loggedInEmployee) + { + // --- Step 1: Input Validation --- + if (projectId == null) + { + _logger.LogWarning("GetProjectAllocation called with a null projectId."); + return ApiResponse.ErrorResponse("Project ID is required.", "Invalid Input Parameter", 400); + } + + _logger.LogInfo("Fetching allocations for ProjectID: {ProjectId} for user {UserId}", projectId, loggedInEmployee.Id); + + try + { + // --- Step 2: Security and Existence Checks --- + // Before fetching data, you MUST verify the user has permission to see it. + // This is a placeholder for your actual permission logic. + var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.Value); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, projectId); + return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to view this project's team.", 403); + } + + // --- Step 3: Execute a Single, Optimized Database Query --- + // This query projects directly to a new object on the database server, which is highly efficient. + var allocations = await _context.ProjectAllocations + // Filter down to the relevant records first. + .Where(pa => pa.ProjectId == projectId && pa.TenantId == tenantId && pa.Employee != null) + // Project directly to the final shape. This tells EF Core which columns to select. + // The redundant .Include() is removed as EF Core infers the JOIN from this Select. + .Select(pa => new + { + // Fields from ProjectAllocation + ID = pa.Id, + pa.EmployeeId, + pa.ProjectId, + pa.AllocationDate, + pa.ReAllocationDate, + pa.IsActive, + + // Fields from the joined Employee table (no null checks needed due to the 'Where' clause) + FirstName = pa.Employee!.FirstName, + LastName = pa.Employee.LastName, + MiddleName = pa.Employee.MiddleName, + + // Simplified JobRoleId logic: Use the allocation's role if it exists, otherwise fall back to the employee's default role. + JobRoleId = pa.JobRoleId ?? pa.Employee.JobRoleId + }) + .ToListAsync(); + + _logger.LogInfo("Successfully fetched {AllocationCount} allocations for project {ProjectId}.", allocations.Count, projectId); + + return ApiResponse.SuccessResponse(allocations, "Project allocations retrieved successfully.", 200); + } + catch (Exception ex) + { + // --- Step 4: Graceful Error Handling --- + // Log the full exception for debugging, but return a generic, safe error message. + _logger.LogError(ex, "An error occurred while fetching allocations for project {ProjectId}.", projectId); + return ApiResponse.ErrorResponse("An internal server error occurred.", "Database query failed.", 500); + } + } + #endregion + #region =================================================================== Helper Functions =================================================================== /// @@ -661,7 +868,7 @@ namespace Marco.Pms.Services.Service } catch (Exception ex) { - _logger.LogWarning("Failed to update cache for project {ProjectId} : \n {Error}", projectId, ex.Message); + _logger.LogError(ex, "Failed to update cache for project {ProjectId} : ", projectId); } // Map from the database entity to the response ViewModel. @@ -682,7 +889,7 @@ namespace Marco.Pms.Services.Service } catch (Exception ex) { - _logger.LogError("Background cache update failed for project {ProjectId} \n {Error}", project.Id, ex.Message); + _logger.LogError(ex, "Background cache update failed for project {ProjectId} ", project.Id); } } diff --git a/Marco.Pms.Services/Service/RefreshTokenService.cs b/Marco.Pms.Services/Service/RefreshTokenService.cs index 231e27c..84ef3fd 100644 --- a/Marco.Pms.Services/Service/RefreshTokenService.cs +++ b/Marco.Pms.Services/Service/RefreshTokenService.cs @@ -1,11 +1,11 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Text; -using Marco.Pms.DataAccess.Data; +using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Authentication; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; #nullable disable namespace MarcoBMS.Services.Service @@ -94,7 +94,7 @@ namespace MarcoBMS.Services.Service } catch (Exception ex) { - _logger.LogError("{Error}", ex.Message); + _logger.LogError(ex, "Error occured while creating new JWT token for user {UserId}", userId); throw; } } @@ -132,7 +132,7 @@ namespace MarcoBMS.Services.Service } catch (Exception ex) { - _logger.LogError("Error creating MPIN token for userId: {UserId}, tenantId: {TenantId}, error : {Error}", userId, tenantId, ex.Message); + _logger.LogError(ex, "Error creating MPIN token for userId: {UserId}, tenantId: {TenantId}", userId, tenantId); throw; } } @@ -218,7 +218,7 @@ namespace MarcoBMS.Services.Service catch (Exception ex) { // Token is invalid - _logger.LogError($"Token validation failed: {ex.Message}"); + _logger.LogError(ex, "Token validation failed"); return null; } } diff --git a/Marco.Pms.Services/Service/S3UploadService.cs b/Marco.Pms.Services/Service/S3UploadService.cs index c29cfdd..4ce7a4b 100644 --- a/Marco.Pms.Services/Service/S3UploadService.cs +++ b/Marco.Pms.Services/Service/S3UploadService.cs @@ -64,7 +64,7 @@ namespace Marco.Pms.Services.Service } catch (Exception ex) { - _logger.LogError("{error} while uploading file to S3", ex.Message); + _logger.LogError(ex, "error occured while uploading file to S3"); } @@ -87,7 +87,7 @@ namespace Marco.Pms.Services.Service } catch (Exception ex) { - _logger.LogError("{error} while requesting presigned url from Amazon S3", ex.Message); + _logger.LogError(ex, "error occured while requesting presigned url from Amazon S3", ex.Message); return string.Empty; } } @@ -107,7 +107,7 @@ namespace Marco.Pms.Services.Service } catch (Exception ex) { - _logger.LogError("{error} while deleting from Amazon S3", ex.Message); + _logger.LogError(ex, "error ocured while deleting from Amazon S3"); return false; } } @@ -202,7 +202,7 @@ namespace Marco.Pms.Services.Service } else { - _logger.LogError("Warning: Could not find MimeType, Type, or ContentType property in Definition."); + _logger.LogWarning("Warning: Could not find MimeType, Type, or ContentType property in Definition."); return "application/octet-stream"; } } @@ -211,16 +211,16 @@ namespace Marco.Pms.Services.Service return "application/octet-stream"; // Default if type cannot be determined } } - catch (FormatException) + catch (FormatException fEx) { // Handle cases where the input string is not valid Base64 - _logger.LogError("Invalid Base64 string."); + _logger.LogError(fEx, "Invalid Base64 string."); return string.Empty; } catch (Exception ex) { // Handle other potential errors during decoding or inspection - _logger.LogError($"An error occurred: {ex.Message}"); + _logger.LogError(ex, "errors during decoding or inspection"); return string.Empty; } } diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs index a23eba0..d0539b0 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs @@ -13,5 +13,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> GetProjectDetailsOldAsync(Guid id, Guid tenantId, Employee loggedInEmployee); Task> CreateProjectAsync(CreateProjectDto projectDto, Guid tenantId, Employee loggedInEmployee); Task> UpdateProjectAsync(Guid id, UpdateProjectDto updateProjectDto, Guid tenantId, Employee loggedInEmployee); + Task> GetEmployeeByProjectIdAsync(Guid? projectId, bool includeInactive, Guid tenantId, Employee loggedInEmployee); + Task> GetProjectAllocationAsync(Guid? projectId, Guid tenantId, Employee loggedInEmployee); } } diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/ISignalRService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/ISignalRService.cs new file mode 100644 index 0000000..c37322b --- /dev/null +++ b/Marco.Pms.Services/Service/ServiceInterfaces/ISignalRService.cs @@ -0,0 +1,7 @@ +namespace Marco.Pms.Services.Service.ServiceInterfaces +{ + public interface ISignalRService + { + Task SendNotificationAsync(object notification); + } +} diff --git a/Marco.Pms.Services/Service/SignalRService.cs b/Marco.Pms.Services/Service/SignalRService.cs new file mode 100644 index 0000000..fecc9b0 --- /dev/null +++ b/Marco.Pms.Services/Service/SignalRService.cs @@ -0,0 +1,29 @@ +using Marco.Pms.Services.Hubs; +using Marco.Pms.Services.Service.ServiceInterfaces; +using MarcoBMS.Services.Service; +using Microsoft.AspNetCore.SignalR; + +namespace Marco.Pms.Services.Service +{ + public class SignalRService : ISignalRService + { + private readonly IHubContext _signalR; + private readonly ILoggingService _logger; + public SignalRService(IHubContext signalR, ILoggingService logger) + { + _signalR = signalR ?? throw new ArgumentNullException(nameof(signalR)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + public async Task SendNotificationAsync(object notification) + { + try + { + await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception occured during sending notification through signalR"); + } + } + } +} From 5369bbae297e424bf97e0f9faad274cf2c674727 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 15 Jul 2025 13:09:27 +0530 Subject: [PATCH 071/124] Solved the issue of project is not updating properly --- Marco.Pms.CacheHelper/ProjectCache.cs | 4 +- .../Controllers/ProjectController.cs | 101 +++-------- Marco.Pms.Services/Service/ProjectServices.cs | 158 ++++++++++-------- 3 files changed, 111 insertions(+), 152 deletions(-) diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index 183bbc4..c7d7e84 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -42,8 +42,8 @@ namespace Marco.Pms.CacheHelper Builders.Update.Set(r => r.ShortName, project.ShortName), Builders.Update.Set(r => r.ProjectStatus, new StatusMasterMongoDB { - Id = projectStatus?.Id.ToString(), - Status = projectStatus?.Status + Id = projectStatus.Id.ToString(), + Status = projectStatus.Status }), Builders.Update.Set(r => r.StartDate, project.StartDate), Builders.Update.Set(r => r.EndDate, project.EndDate), diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 236e0cb..0122003 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -252,89 +252,28 @@ namespace MarcoBMS.Services.Controllers return StatusCode(response.StatusCode, response); } - [HttpPost("allocation")] - public async Task ManageAllocation(List projectAllocationDot) - { - if (projectAllocationDot != null) - { - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + //[HttpPost("allocation")] + //public async Task ManageAllocation(List projectAllocationDot) + //{ + // // --- Step 1: Input Validation --- + // if (!ModelState.IsValid) + // { + // var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + // _logger.LogWarning("Update project called with invalid model state for ID {ProjectId}. Errors: {Errors}", id, string.Join(", ", errors)); + // return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); + // } - List? result = new List(); - List employeeIds = new List(); - List projectIds = new List(); + // // --- Step 2: Prepare data without I/O --- + // Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + // var response = await _projectServices.UpdateProjectAsync(id, updateProjectDto, tenantId, loggedInEmployee); + // if (response.Success) + // { + // var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeList = employeeIds }; + // await _signalR.SendNotificationAsync(notification); + // } + // return StatusCode(response.StatusCode, response); - foreach (var item in projectAllocationDot) - { - try - { - ProjectAllocation projectAllocation = item.ToProjectAllocationFromProjectAllocationDto(tenantId); - ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == projectAllocation.EmployeeId - && c.ProjectId == projectAllocation.ProjectId - && c.ReAllocationDate == null - && c.TenantId == tenantId).SingleOrDefaultAsync(); - - if (projectAllocationFromDb != null) - { - _context.ProjectAllocations.Attach(projectAllocationFromDb); - - if (item.Status) - { - projectAllocationFromDb.JobRoleId = projectAllocation.JobRoleId; ; - projectAllocationFromDb.IsActive = true; - _context.Entry(projectAllocationFromDb).Property(e => e.JobRoleId).IsModified = true; - _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; - } - else - { - projectAllocationFromDb.ReAllocationDate = DateTime.Now; - projectAllocationFromDb.IsActive = false; - _context.Entry(projectAllocationFromDb).Property(e => e.ReAllocationDate).IsModified = true; - _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; - - employeeIds.Add(projectAllocation.EmployeeId); - projectIds.Add(projectAllocation.ProjectId); - } - await _context.SaveChangesAsync(); - var result1 = new - { - Id = projectAllocationFromDb.Id, - EmployeeId = projectAllocation.EmployeeId, - JobRoleId = projectAllocation.JobRoleId, - IsActive = projectAllocation.IsActive, - ProjectId = projectAllocation.ProjectId, - AllocationDate = projectAllocation.AllocationDate, - ReAllocationDate = projectAllocation.ReAllocationDate, - TenantId = projectAllocation.TenantId - }; - result.Add(result1); - } - else - { - projectAllocation.AllocationDate = DateTime.Now; - projectAllocation.IsActive = true; - _context.ProjectAllocations.Add(projectAllocation); - await _context.SaveChangesAsync(); - - employeeIds.Add(projectAllocation.EmployeeId); - projectIds.Add(projectAllocation.ProjectId); - } - await _cache.ClearAllProjectIds(item.EmpID); - - } - catch (Exception ex) - { - return Ok(ApiResponse.ErrorResponse(ex.Message, ex, 400)); - } - } - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeList = employeeIds }; - - await _signalR.SendNotificationAsync(notification); - return Ok(ApiResponse.SuccessResponse(result, "Data saved successfully", 200)); - - } - return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Work Item Details are not valid.", 400)); - - } + //} [HttpGet("assigned-projects/{employeeId}")] public async Task GetProjectsByEmployee([FromRoute] Guid employeeId) diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs index dcaf20e..7717584 100644 --- a/Marco.Pms.Services/Service/ProjectServices.cs +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -443,21 +443,16 @@ namespace Marco.Pms.Services.Service return ApiResponse.ErrorResponse("Conflict occurred.", "This project has been modified by someone else. Please refresh and try again.", 409); } - // --- Step 4: Perform Side-Effects in the Background (Fire and Forget) --- - // The core database operation is done. Now, we perform non-blocking cache and notification updates. - _ = Task.Run(async () => - { - // Create a DTO of the updated project to pass to background tasks. - var projectDto = _mapper.Map(existingProject); + // --- Step 4: Perform Side-Effects (Fire and Forget) --- + // Create a DTO of the updated project to pass to background tasks. + var projectDto = _mapper.Map(existingProject); - // 4a. Update Cache - await UpdateCacheInBackground(existingProject); - - }); + // 4a. Update Cache + await UpdateCacheInBackground(existingProject); // --- Step 5: Return Success Response Immediately --- // The client gets a fast response without waiting for caching or SignalR. - return ApiResponse.SuccessResponse(_mapper.Map(existingProject), "Project updated successfully.", 200); + return ApiResponse.SuccessResponse(projectDto, "Project updated successfully.", 200); } catch (Exception ex) { @@ -471,41 +466,6 @@ namespace Marco.Pms.Services.Service #region =================================================================== Project Allocation APIs =================================================================== - public async Task> GetEmployeeByProjectID(Guid? projectid, bool includeInactive, Guid tenantId, Employee loggedInEmployee) - { - if (projectid == null) - { - return ApiResponse.ErrorResponse("Invalid Input Parameter", 404); - } - // Fetch assigned project - List result = new List(); - - var employeeQuery = _context.ProjectAllocations - .Include(pa => pa.Employee) - .Where(pa => pa.ProjectId == projectid && pa.TenantId == tenantId && pa.Employee != null); - - if (includeInactive) - { - - result = await employeeQuery.Select(pa => pa.Employee ?? new Employee()).ToListAsync(); - } - else - { - result = await employeeQuery - .Where(pa => pa.IsActive) - .Select(pa => pa.Employee ?? new Employee()).ToListAsync(); - } - - List resultVM = new List(); - foreach (Employee employee in result) - { - EmployeeVM vm = _mapper.Map(employee); - resultVM.Add(vm); - } - - return ApiResponse.SuccessResponse(resultVM, "Successfully fetched the list of employees for seleted project", 200); - } - /// /// Retrieves a list of employees for a specific project. /// This method is optimized to perform all filtering and mapping on the database server. @@ -578,28 +538,6 @@ namespace Marco.Pms.Services.Service } } - public async Task> GetProjectAllocation(Guid? projectId, Guid tenantId, Employee loggedInEmployee) - { - var employees = await _context.ProjectAllocations - .Where(c => c.TenantId == tenantId && c.ProjectId == projectId && c.Employee != null) - .Include(e => e.Employee) - .Select(e => new - { - ID = e.Id, - EmployeeId = e.EmployeeId, - ProjectId = e.ProjectId, - AllocationDate = e.AllocationDate, - ReAllocationDate = e.ReAllocationDate, - FirstName = e.Employee != null ? e.Employee.FirstName : string.Empty, - LastName = e.Employee != null ? e.Employee.LastName : string.Empty, - MiddleName = e.Employee != null ? e.Employee.MiddleName : string.Empty, - IsActive = e.IsActive, - JobRoleId = (e.JobRoleId != null ? e.JobRoleId : e.Employee != null ? e.Employee.JobRoleId : null) - }).ToListAsync(); - - return ApiResponse.SuccessResponse(employees, "Success.", 200); - } - /// /// Retrieves project allocation details for a specific project. /// This method is optimized for performance and includes security checks. @@ -670,6 +608,87 @@ namespace Marco.Pms.Services.Service return ApiResponse.ErrorResponse("An internal server error occurred.", "Database query failed.", 500); } } + + //public async Task> ManageAllocation(List projectAllocationDot, Guid tenantId, Employee loggedInEmployee) + //{ + // if (projectAllocationDot != null) + // { + // List? result = new List(); + // List employeeIds = new List(); + // List projectIds = new List(); + + // foreach (var item in projectAllocationDot) + // { + // try + // { + // //ProjectAllocation projectAllocation = item.ToProjectAllocationFromProjectAllocationDto(tenantId); + // ProjectAllocation projectAllocation = item.ToProjectAllocationFromProjectAllocationDto(tenantId); + // ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == projectAllocation.EmployeeId + // && c.ProjectId == projectAllocation.ProjectId + // && c.ReAllocationDate == null + // && c.TenantId == tenantId).SingleOrDefaultAsync(); + + // if (projectAllocationFromDb != null) + // { + // _context.ProjectAllocations.Attach(projectAllocationFromDb); + + // if (item.Status) + // { + // projectAllocationFromDb.JobRoleId = projectAllocation.JobRoleId; ; + // projectAllocationFromDb.IsActive = true; + // _context.Entry(projectAllocationFromDb).Property(e => e.JobRoleId).IsModified = true; + // _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; + // } + // else + // { + // projectAllocationFromDb.ReAllocationDate = DateTime.Now; + // projectAllocationFromDb.IsActive = false; + // _context.Entry(projectAllocationFromDb).Property(e => e.ReAllocationDate).IsModified = true; + // _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; + + // employeeIds.Add(projectAllocation.EmployeeId); + // projectIds.Add(projectAllocation.ProjectId); + // } + // await _context.SaveChangesAsync(); + // var result1 = new + // { + // Id = projectAllocationFromDb.Id, + // EmployeeId = projectAllocation.EmployeeId, + // JobRoleId = projectAllocation.JobRoleId, + // IsActive = projectAllocation.IsActive, + // ProjectId = projectAllocation.ProjectId, + // AllocationDate = projectAllocation.AllocationDate, + // ReAllocationDate = projectAllocation.ReAllocationDate, + // TenantId = projectAllocation.TenantId + // }; + // result.Add(result1); + // } + // else + // { + // projectAllocation.AllocationDate = DateTime.Now; + // projectAllocation.IsActive = true; + // _context.ProjectAllocations.Add(projectAllocation); + // await _context.SaveChangesAsync(); + + // employeeIds.Add(projectAllocation.EmployeeId); + // projectIds.Add(projectAllocation.ProjectId); + // } + // await _cache.ClearAllProjectIds(item.EmpID); + + // } + // catch (Exception ex) + // { + // return ApiResponse.ErrorResponse(ex.Message, ex, 400); + // } + // } + + // return ApiResponse.SuccessResponse(result, "Data saved successfully", 200); + + // } + // return ApiResponse.ErrorResponse("Invalid details.", "Work Item Details are not valid.", 400); + + //} + #endregion #region =================================================================== Helper Functions =================================================================== @@ -881,7 +900,8 @@ namespace Marco.Pms.Services.Service try { // This logic can be more complex, but the idea is to update or add. - if (!await _cache.UpdateProjectDetailsOnly(project)) + var demo = await _cache.UpdateProjectDetailsOnly(project); + if (!demo) { await _cache.AddProjectDetails(project); } From a64ce4fb0246e2f536580989a0e8b4b8444a53b0 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 15 Jul 2025 14:34:26 +0530 Subject: [PATCH 072/124] Removed unused code from employee cache class --- Marco.Pms.CacheHelper/EmployeeCache.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Marco.Pms.CacheHelper/EmployeeCache.cs b/Marco.Pms.CacheHelper/EmployeeCache.cs index 4a668f0..2211393 100644 --- a/Marco.Pms.CacheHelper/EmployeeCache.cs +++ b/Marco.Pms.CacheHelper/EmployeeCache.cs @@ -1,5 +1,4 @@ -using Marco.Pms.DataAccess.Data; -using Marco.Pms.Model.MongoDBModels; +using Marco.Pms.Model.MongoDBModels; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using MongoDB.Driver; @@ -8,13 +7,10 @@ namespace Marco.Pms.CacheHelper { public class EmployeeCache { - private readonly ApplicationDbContext _context; - //private readonly IMongoDatabase _mongoDB; private readonly IMongoCollection _collection; - public EmployeeCache(ApplicationDbContext context, IConfiguration configuration) + public EmployeeCache(IConfiguration configuration) { var connectionString = configuration["MongoDB:ConnectionString"]; - _context = context; var mongoUrl = new MongoUrl(connectionString); var client = new MongoClient(mongoUrl); // Your MongoDB connection string var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name From f406a15508656c302bba536b1204bec7ab940362 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 15 Jul 2025 15:21:48 +0530 Subject: [PATCH 073/124] Added Employee ID of creater to bucket in Employee IDs --- Marco.Pms.Services/Helpers/DirectoryHelper.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Marco.Pms.Services/Helpers/DirectoryHelper.cs b/Marco.Pms.Services/Helpers/DirectoryHelper.cs index f8e1b07..33460b2 100644 --- a/Marco.Pms.Services/Helpers/DirectoryHelper.cs +++ b/Marco.Pms.Services/Helpers/DirectoryHelper.cs @@ -1184,7 +1184,11 @@ namespace Marco.Pms.Services.Helpers var emplyeeIds = employeeBucketMappings.Select(eb => eb.EmployeeId).ToList(); List? contactBuckets = contactBucketMappings.Where(cb => cb.BucketId == bucket.Id).ToList(); AssignBucketVM bucketVM = bucket.ToAssignBucketVMFromBucket(); - bucketVM.EmployeeIds = emplyeeIds; + if (bucketVM.CreatedBy != null) + { + emplyeeIds.Add(bucketVM.CreatedBy.Id); + } + bucketVM.EmployeeIds = emplyeeIds.Distinct().ToList(); bucketVM.NumberOfContacts = contactBuckets.Count; bucketVMs.Add(bucketVM); } From 823deb17ccbc7f61f8404e08270541e38c20e597 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 15 Jul 2025 15:30:41 +0530 Subject: [PATCH 074/124] Optimized the Project Allocation API --- Marco.Pms.CacheHelper/EmployeeCache.cs | 2 +- .../Projects/ProjectAllocationVM.cs | 13 ++ .../Controllers/ProjectController.cs | 43 ++--- .../MappingProfiles/MappingProfile.cs | 6 + Marco.Pms.Services/Service/ProjectServices.cs | 167 ++++++++++-------- .../ServiceInterfaces/IProjectServices.cs | 2 + 6 files changed, 142 insertions(+), 91 deletions(-) create mode 100644 Marco.Pms.Model/ViewModels/Projects/ProjectAllocationVM.cs diff --git a/Marco.Pms.CacheHelper/EmployeeCache.cs b/Marco.Pms.CacheHelper/EmployeeCache.cs index 2211393..f7b7066 100644 --- a/Marco.Pms.CacheHelper/EmployeeCache.cs +++ b/Marco.Pms.CacheHelper/EmployeeCache.cs @@ -97,7 +97,7 @@ namespace Marco.Pms.CacheHelper var result = await _collection.UpdateOneAsync(filter, update); - if (result.MatchedCount == 0) + if (result.ModifiedCount == 0) return false; return true; diff --git a/Marco.Pms.Model/ViewModels/Projects/ProjectAllocationVM.cs b/Marco.Pms.Model/ViewModels/Projects/ProjectAllocationVM.cs new file mode 100644 index 0000000..6d9138e --- /dev/null +++ b/Marco.Pms.Model/ViewModels/Projects/ProjectAllocationVM.cs @@ -0,0 +1,13 @@ +namespace Marco.Pms.Model.ViewModels.Projects +{ + public class ProjectAllocationVM + { + public Guid Id { get; set; } + public Guid EmployeeId { get; set; } + public Guid? JobRoleId { get; set; } + public bool IsActive { get; set; } = true; + public Guid ProjectId { get; set; } + public DateTime AllocationDate { get; set; } + public DateTime? ReAllocationDate { get; set; } + } +} diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 0122003..b833064 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -252,28 +252,31 @@ namespace MarcoBMS.Services.Controllers return StatusCode(response.StatusCode, response); } - //[HttpPost("allocation")] - //public async Task ManageAllocation(List projectAllocationDot) - //{ - // // --- Step 1: Input Validation --- - // if (!ModelState.IsValid) - // { - // var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); - // _logger.LogWarning("Update project called with invalid model state for ID {ProjectId}. Errors: {Errors}", id, string.Join(", ", errors)); - // return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); - // } + [HttpPost("allocation")] + public async Task ManageAllocation([FromBody] List projectAllocationDot) + { + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) + { + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); + } - // // --- Step 2: Prepare data without I/O --- - // Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - // var response = await _projectServices.UpdateProjectAsync(id, updateProjectDto, tenantId, loggedInEmployee); - // if (response.Success) - // { - // var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeList = employeeIds }; - // await _signalR.SendNotificationAsync(notification); - // } - // return StatusCode(response.StatusCode, response); + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.ManageAllocationAsync(projectAllocationDot, tenantId, loggedInEmployee); + if (response.Success) + { + List employeeIds = response.Data.Select(pa => pa.EmployeeId).ToList(); + List projectIds = response.Data.Select(pa => pa.ProjectId).ToList(); - //} + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeList = employeeIds }; + await _signalR.SendNotificationAsync(notification); + } + return StatusCode(response.StatusCode, response); + + } [HttpGet("assigned-projects/{employeeId}")] public async Task GetProjectsByEmployee([FromRoute] Guid employeeId) diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index 7d627bc..3ca1271 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -43,6 +43,12 @@ namespace Marco.Pms.Services.MappingProfiles CreateMap(); CreateMap(); CreateMap(); + CreateMap() + .ForMember( + dest => dest.EmployeeId, + // Explicitly and safely convert string ProjectStatusId to Guid ProjectStatusId + opt => opt.MapFrom(src => src.EmpID)); + CreateMap(); #endregion #region ======================================================= Projects ======================================================= diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs index 7717584..33df2c0 100644 --- a/Marco.Pms.Services/Service/ProjectServices.cs +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -609,85 +609,112 @@ namespace Marco.Pms.Services.Service } } - //public async Task> ManageAllocation(List projectAllocationDot, Guid tenantId, Employee loggedInEmployee) - //{ - // if (projectAllocationDot != null) - // { - // List? result = new List(); - // List employeeIds = new List(); - // List projectIds = new List(); + /// + /// Manages project allocations for a list of employees, either adding new allocations or deactivating existing ones. + /// This method is optimized to perform all database operations in a single transaction. + /// + /// The list of allocation changes to process. + /// The ID of the current tenant. + /// The current authenticated employee for permission checks. + /// An ApiResponse containing the list of processed allocations. + public async Task>> ManageAllocationAsync(List allocationsDto, Guid tenantId, Employee loggedInEmployee) + { + // --- Step 1: Input Validation --- + if (allocationsDto == null || !allocationsDto.Any()) + { + return ApiResponse>.ErrorResponse("Invalid details.", "Allocation details list cannot be null or empty.", 400); + } - // foreach (var item in projectAllocationDot) - // { - // try - // { - // //ProjectAllocation projectAllocation = item.ToProjectAllocationFromProjectAllocationDto(tenantId); - // ProjectAllocation projectAllocation = item.ToProjectAllocationFromProjectAllocationDto(tenantId); - // ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == projectAllocation.EmployeeId - // && c.ProjectId == projectAllocation.ProjectId - // && c.ReAllocationDate == null - // && c.TenantId == tenantId).SingleOrDefaultAsync(); + _logger.LogInfo("Starting to manage {AllocationCount} allocations for user {UserId}.", allocationsDto.Count, loggedInEmployee.Id); - // if (projectAllocationFromDb != null) - // { - // _context.ProjectAllocations.Attach(projectAllocationFromDb); + // --- (Placeholder) Security Check --- + // In a real application, you would check if the loggedInEmployee has permission + // to manage allocations for ALL projects involved in this batch. + var projectIdsInBatch = allocationsDto.Select(a => a.ProjectId).Distinct().ToList(); + var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageTeam, loggedInEmployee.Id); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} trying to manage allocations for projects.", loggedInEmployee.Id); + return ApiResponse>.ErrorResponse("Access Denied.", "You do not have permission to manage one or more projects in this request.", 403); + } - // if (item.Status) - // { - // projectAllocationFromDb.JobRoleId = projectAllocation.JobRoleId; ; - // projectAllocationFromDb.IsActive = true; - // _context.Entry(projectAllocationFromDb).Property(e => e.JobRoleId).IsModified = true; - // _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; - // } - // else - // { - // projectAllocationFromDb.ReAllocationDate = DateTime.Now; - // projectAllocationFromDb.IsActive = false; - // _context.Entry(projectAllocationFromDb).Property(e => e.ReAllocationDate).IsModified = true; - // _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; + // --- Step 2: Fetch all relevant existing data in ONE database call --- + var employeeProjectPairs = allocationsDto.Select(a => new { a.EmpID, a.ProjectId }).ToList(); + List employeeIds = allocationsDto.Select(a => a.EmpID).Distinct().ToList(); - // employeeIds.Add(projectAllocation.EmployeeId); - // projectIds.Add(projectAllocation.ProjectId); - // } - // await _context.SaveChangesAsync(); - // var result1 = new - // { - // Id = projectAllocationFromDb.Id, - // EmployeeId = projectAllocation.EmployeeId, - // JobRoleId = projectAllocation.JobRoleId, - // IsActive = projectAllocation.IsActive, - // ProjectId = projectAllocation.ProjectId, - // AllocationDate = projectAllocation.AllocationDate, - // ReAllocationDate = projectAllocation.ReAllocationDate, - // TenantId = projectAllocation.TenantId - // }; - // result.Add(result1); - // } - // else - // { - // projectAllocation.AllocationDate = DateTime.Now; - // projectAllocation.IsActive = true; - // _context.ProjectAllocations.Add(projectAllocation); - // await _context.SaveChangesAsync(); + // Fetch all currently active allocations for the employees and projects in this batch. + // We use a dictionary for fast O(1) lookups inside the loop. + var existingAllocations = await _context.ProjectAllocations + .Where(pa => pa.TenantId == tenantId && + employeeIds.Contains(pa.EmployeeId) && + pa.ReAllocationDate == null) + .ToDictionaryAsync(pa => (pa.EmployeeId, pa.ProjectId)); - // employeeIds.Add(projectAllocation.EmployeeId); - // projectIds.Add(projectAllocation.ProjectId); - // } - // await _cache.ClearAllProjectIds(item.EmpID); + var processedAllocations = new List(); - // } - // catch (Exception ex) - // { - // return ApiResponse.ErrorResponse(ex.Message, ex, 400); - // } - // } + // --- Step 3: Process logic IN MEMORY --- + foreach (var dto in allocationsDto) + { + var key = (dto.EmpID, dto.ProjectId); + existingAllocations.TryGetValue(key, out var existingAllocation); - // return ApiResponse.SuccessResponse(result, "Data saved successfully", 200); + if (dto.Status == false) // User wants to DEACTIVATE the allocation + { + if (existingAllocation != null) + { + // Mark the existing allocation for deactivation + existingAllocation.ReAllocationDate = DateTime.UtcNow; // Use UtcNow for servers + existingAllocation.IsActive = false; + _context.ProjectAllocations.Update(existingAllocation); + processedAllocations.Add(existingAllocation); + } + // If it doesn't exist, we do nothing. The desired state is "not allocated". + } + else // User wants to ACTIVATE the allocation + { + if (existingAllocation == null) + { + // Create a new allocation because one doesn't exist + var newAllocation = _mapper.Map(dto); + newAllocation.TenantId = tenantId; + newAllocation.AllocationDate = DateTime.UtcNow; + newAllocation.IsActive = true; + _context.ProjectAllocations.Add(newAllocation); + processedAllocations.Add(newAllocation); + } + // If it already exists and is active, we do nothing. The state is already correct. + } + try + { + await _cache.ClearAllProjectIds(dto.EmpID); + _logger.LogInfo("Successfully completed cache invalidation for employee {EmployeeId}.", dto.EmpID); + } + catch (Exception ex) + { + // Log the error but don't fail the entire request, as the primary DB operation succeeded. + _logger.LogError(ex, "Cache invalidation failed for employees after a successful database update."); + } + } - // } - // return ApiResponse.ErrorResponse("Invalid details.", "Work Item Details are not valid.", 400); + try + { + // --- Step 4: Save all changes in a SINGLE TRANSACTION --- + // All Adds and Updates are sent to the database in one batch. + // If any part fails, the entire transaction is rolled back. + await _context.SaveChangesAsync(); + _logger.LogInfo("Successfully saved {ChangeCount} allocation changes to the database.", processedAllocations.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to save allocation changes to the database."); + return ApiResponse>.ErrorResponse("Database Error.", "An error occurred while saving the changes.", 500); + } - //} + + // --- Step 5: Map results and return success --- + var resultVm = _mapper.Map>(processedAllocations); + return ApiResponse>.SuccessResponse(resultVm, "Allocations managed successfully.", 200); + } #endregion diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs index d0539b0..2552444 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs @@ -1,6 +1,7 @@ using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Utilities; +using Marco.Pms.Model.ViewModels.Projects; namespace Marco.Pms.Services.Service.ServiceInterfaces { @@ -15,5 +16,6 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> UpdateProjectAsync(Guid id, UpdateProjectDto updateProjectDto, Guid tenantId, Employee loggedInEmployee); Task> GetEmployeeByProjectIdAsync(Guid? projectId, bool includeInactive, Guid tenantId, Employee loggedInEmployee); Task> GetProjectAllocationAsync(Guid? projectId, Guid tenantId, Employee loggedInEmployee); + Task>> ManageAllocationAsync(List projectAllocationDots, Guid tenantId, Employee loggedInEmployee); } } From 9d0c16b88703305c379d8355b810ed47acfb7d1b Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 15 Jul 2025 15:37:15 +0530 Subject: [PATCH 075/124] Added Sonar files in git ignore --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9491a2f..a6a47c3 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,7 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd + +# Sonar +/.sonarqube \ No newline at end of file From c3da83d165d2ac3376da2bf539c22cdce2521371 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 15 Jul 2025 16:37:57 +0530 Subject: [PATCH 076/124] Optimized the project allocation by employee Id Apis --- .../Controllers/ProjectController.cs | 130 +++---------- .../MappingProfiles/MappingProfile.cs | 1 + Marco.Pms.Services/Service/ProjectServices.cs | 180 ++++++++++++++++++ .../ServiceInterfaces/IProjectServices.cs | 2 + 4 files changed, 207 insertions(+), 106 deletions(-) diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index b833064..82ce0dd 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -17,7 +17,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.CodeAnalysis; using Microsoft.EntityFrameworkCore; using MongoDB.Driver; -using Project = Marco.Pms.Model.Projects.Project; namespace MarcoBMS.Services.Controllers { @@ -281,123 +280,42 @@ namespace MarcoBMS.Services.Controllers [HttpGet("assigned-projects/{employeeId}")] public async Task GetProjectsByEmployee([FromRoute] Guid employeeId) { - if (employeeId == Guid.Empty) + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) { - return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Employee id not valid.", 400)); + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("Get project list by employee Id called with invalid model state \n Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - List projectList = await _context.ProjectAllocations - .Where(c => c.TenantId == tenantId && c.EmployeeId == employeeId && c.IsActive) - .Select(c => c.ProjectId).Distinct() - .ToListAsync(); - - if (!projectList.Any()) - { - return NotFound(ApiResponse.SuccessResponse(new List(), "No projects found.", 200)); - } - - - List projectlist = await _context.Projects - .Where(p => projectList.Contains(p.Id)) - .ToListAsync(); - - List projects = new List(); - - - foreach (var project in projectlist) - { - - projects.Add(project.ToProjectListVMFromProject()); - } - - - - return Ok(ApiResponse.SuccessResponse(projects, "Success.", 200)); + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetProjectsByEmployeeAsync(employeeId, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } [HttpPost("assign-projects/{employeeId}")] - public async Task AssigneProjectsToEmployee([FromBody] List projectAllocationDtos, [FromRoute] Guid employeeId) + public async Task AssigneProjectsToEmployee([FromBody] List projectAllocationDtos, [FromRoute] Guid employeeId) { - if (projectAllocationDtos != null && employeeId != Guid.Empty) + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) { - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - List? result = new List(); - List projectIds = new List(); + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); + } - foreach (var projectAllocationDto in projectAllocationDtos) - { - try - { - ProjectAllocation projectAllocation = projectAllocationDto.ToProjectAllocationFromProjectsAllocationDto(tenantId, employeeId); - ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.ProjectId == projectAllocationDto.ProjectId && c.ReAllocationDate == null && c.TenantId == tenantId).SingleOrDefaultAsync(); - - if (projectAllocationFromDb != null) - { - - - _context.ProjectAllocations.Attach(projectAllocationFromDb); - - if (projectAllocationDto.Status) - { - projectAllocationFromDb.JobRoleId = projectAllocation.JobRoleId; ; - projectAllocationFromDb.IsActive = true; - _context.Entry(projectAllocationFromDb).Property(e => e.JobRoleId).IsModified = true; - _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; - } - else - { - projectAllocationFromDb.ReAllocationDate = DateTime.UtcNow; - projectAllocationFromDb.IsActive = false; - _context.Entry(projectAllocationFromDb).Property(e => e.ReAllocationDate).IsModified = true; - _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; - - projectIds.Add(projectAllocation.ProjectId); - } - await _context.SaveChangesAsync(); - var result1 = new - { - Id = projectAllocationFromDb.Id, - EmployeeId = projectAllocation.EmployeeId, - JobRoleId = projectAllocation.JobRoleId, - IsActive = projectAllocation.IsActive, - ProjectId = projectAllocation.ProjectId, - AllocationDate = projectAllocation.AllocationDate, - ReAllocationDate = projectAllocation.ReAllocationDate, - TenantId = projectAllocation.TenantId - }; - result.Add(result1); - } - else - { - projectAllocation.AllocationDate = DateTime.Now; - projectAllocation.IsActive = true; - _context.ProjectAllocations.Add(projectAllocation); - await _context.SaveChangesAsync(); - - projectIds.Add(projectAllocation.ProjectId); - - } - - - } - catch (Exception ex) - { - - return Ok(ApiResponse.ErrorResponse(ex.Message, ex, 400)); - } - } - await _cache.ClearAllProjectIds(employeeId); - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeId = employeeId }; + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.AssigneProjectsToEmployeeAsync(projectAllocationDtos, employeeId, tenantId, loggedInEmployee); + if (response.Success) + { + List projectIds = response.Data.Select(pa => pa.ProjectId).ToList(); + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeId = employeeId }; await _signalR.SendNotificationAsync(notification); - - return Ok(ApiResponse.SuccessResponse(result, "Data saved successfully", 200)); } - else - { - return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "All Field is required", 400)); - } - + return StatusCode(response.StatusCode, response); } #endregion diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index 3ca1271..ea42d16 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -48,6 +48,7 @@ namespace Marco.Pms.Services.MappingProfiles dest => dest.EmployeeId, // Explicitly and safely convert string ProjectStatusId to Guid ProjectStatusId opt => opt.MapFrom(src => src.EmpID)); + CreateMap(); CreateMap(); #endregion diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs index 33df2c0..9024112 100644 --- a/Marco.Pms.Services/Service/ProjectServices.cs +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -716,6 +716,186 @@ namespace Marco.Pms.Services.Service return ApiResponse>.SuccessResponse(resultVm, "Allocations managed successfully.", 200); } + /// + /// Retrieves a list of active projects assigned to a specific employee. + /// + /// The ID of the employee whose projects are being requested. + /// The ID of the current tenant. + /// The current authenticated employee for permission checks. + /// An ApiResponse containing a list of basic project details or an error. + public async Task> GetProjectsByEmployeeAsync(Guid employeeId, Guid tenantId, Employee loggedInEmployee) + { + // --- Step 1: Input Validation --- + if (employeeId == Guid.Empty) + { + return ApiResponse.ErrorResponse("Invalid details.", "A valid employee ID is required.", 400); + } + + _logger.LogInfo("Fetching projects for Employee {EmployeeId} by User {UserId}", employeeId, loggedInEmployee.Id); + + try + { + // --- Step 2: Clarified Security Check --- + // The permission should be about viewing another employee's assignments, not a generic "Manage Team". + // This is a placeholder for your actual, more specific permission logic. + // It should also handle the case where a user is requesting their own projects (employeeId == loggedInEmployee.Id). + var hasPermission = await _permission.HasPermission(PermissionsMaster.ViewProject, loggedInEmployee.Id); + var projectIds = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} trying to view projects for employee {TargetEmployeeId}.", loggedInEmployee.Id, employeeId); + return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to view this employee's projects.", 403); + } + + // --- Step 3: Execute a Single, Highly Efficient Database Query --- + // This query projects directly to the ViewModel on the database server. + var projects = await _context.ProjectAllocations + // 1. Filter the linking table down to the relevant records. + .Where(pa => + pa.TenantId == tenantId && + pa.EmployeeId == employeeId && // Target the specified employee + pa.IsActive && // Only active assignments + projectIds.Contains(pa.ProjectId) && + pa.Project != null) // Safety check for data integrity + + // 2. Navigate to the Project entity. + .Select(pa => pa.Project) + + // 3. Ensure the final result set is unique (in case of multiple active allocations to the same project). + .Distinct() + + // 4. Project directly to the ViewModel using AutoMapper's IQueryable Extensions. + // This generates an efficient SQL "SELECT Id, Name, Code FROM..." statement. + .ProjectTo(_mapper.ConfigurationProvider) + + // 5. Execute the query. + .ToListAsync(); + + _logger.LogInfo("Successfully retrieved {ProjectCount} projects for employee {EmployeeId}.", projects.Count, employeeId); + + // The original check for an empty list is still good practice. + if (!projects.Any()) + { + return ApiResponse.SuccessResponse(new List(), "No active projects found for this employee.", 200); + } + + return ApiResponse.SuccessResponse(projects, "Projects retrieved successfully.", 200); + } + catch (Exception ex) + { + // --- Step 4: Graceful Error Handling --- + _logger.LogError(ex, "An error occurred while fetching projects for employee {EmployeeId}.", employeeId); + return ApiResponse.ErrorResponse("An internal server error occurred.", "Database query failed.", 500); + } + } + + /// + /// Manages project assignments for a single employee, processing a batch of projects to activate or deactivate. + /// This method is optimized to perform all database operations in a single, atomic transaction. + /// + /// A list of projects to assign or un-assign. + /// The ID of the employee whose assignments are being managed. + /// The ID of the current tenant. + /// The current authenticated employee for permission checks. + /// An ApiResponse containing the list of processed allocations. + public async Task>> AssigneProjectsToEmployeeAsync(List allocationsDto, Guid employeeId, Guid tenantId, Employee loggedInEmployee) + { + // --- Step 1: Input Validation --- + if (allocationsDto == null || !allocationsDto.Any() || employeeId == Guid.Empty) + { + return ApiResponse>.ErrorResponse("Invalid details.", "A valid employee ID and a list of projects are required.", 400); + } + + _logger.LogInfo("Starting to manage {AllocationCount} project assignments for Employee {EmployeeId}.", allocationsDto.Count, employeeId); + + // --- (Placeholder) Security Check --- + // You MUST verify that the loggedInEmployee has permission to modify the assignments for the target employeeId. + var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageTeam, loggedInEmployee.Id); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} trying to manage assignments for employee {TargetEmployeeId}.", loggedInEmployee.Id, employeeId); + return ApiResponse>.ErrorResponse("Access Denied.", "You do not have permission to manage this employee's assignments.", 403); + } + + // --- Step 2: Fetch all relevant existing data in ONE database call --- + var projectIdsInDto = allocationsDto.Select(p => p.ProjectId).ToList(); + + // Fetch all currently active allocations for this employee for the projects in the request. + // We use a dictionary keyed by ProjectId for fast O(1) lookups inside the loop. + var existingActiveAllocations = await _context.ProjectAllocations + .Where(pa => pa.TenantId == tenantId && + pa.EmployeeId == employeeId && + projectIdsInDto.Contains(pa.ProjectId) && + pa.ReAllocationDate == null) // Only fetch active ones + .ToDictionaryAsync(pa => pa.ProjectId); + + var processedAllocations = new List(); + + // --- Step 3: Process all logic IN MEMORY, tracking changes --- + foreach (var dto in allocationsDto) + { + existingActiveAllocations.TryGetValue(dto.ProjectId, out var existingAllocation); + + if (dto.Status == false) // DEACTIVATE this project assignment + { + if (existingAllocation != null) + { + // Correct Update Pattern: Modify the fetched entity directly. + existingAllocation.ReAllocationDate = DateTime.UtcNow; // Use UTC for servers + existingAllocation.IsActive = false; + _context.ProjectAllocations.Update(existingAllocation); + processedAllocations.Add(existingAllocation); + } + // If it's not in our dictionary, it's already inactive. Do nothing. + } + else // ACTIVATE this project assignment + { + if (existingAllocation == null) + { + // Create a new allocation because an active one doesn't exist. + var newAllocation = _mapper.Map(dto); + newAllocation.EmployeeId = employeeId; + newAllocation.TenantId = tenantId; + newAllocation.AllocationDate = DateTime.UtcNow; + newAllocation.IsActive = true; + _context.ProjectAllocations.Add(newAllocation); + processedAllocations.Add(newAllocation); + } + // If it already exists in our dictionary, it's already active. Do nothing. + } + } + + try + { + // --- Step 4: Save all Adds and Updates in a SINGLE ATOMIC TRANSACTION --- + if (processedAllocations.Any()) + { + await _context.SaveChangesAsync(); + _logger.LogInfo("Successfully saved {ChangeCount} assignment changes for employee {EmployeeId}.", processedAllocations.Count, employeeId); + } + } + catch (DbUpdateException ex) + { + _logger.LogError(ex, "Failed to save assignment changes for employee {EmployeeId}.", employeeId); + return ApiResponse>.ErrorResponse("Database Error.", "An error occurred while saving the changes.", 500); + } + + // --- Step 5: Invalidate Cache ONCE after successful save --- + try + { + await _cache.ClearAllProjectIds(employeeId); + _logger.LogInfo("Successfully queued cache invalidation for employee {EmployeeId}.", employeeId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Background cache invalidation failed for employee {EmployeeId}", employeeId); + } + + // --- Step 6: Map results using AutoMapper and return success --- + var resultVm = _mapper.Map>(processedAllocations); + return ApiResponse>.SuccessResponse(resultVm, "Assignments managed successfully.", 200); + } + #endregion #region =================================================================== Helper Functions =================================================================== diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs index 2552444..bafa582 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs @@ -17,5 +17,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> GetEmployeeByProjectIdAsync(Guid? projectId, bool includeInactive, Guid tenantId, Employee loggedInEmployee); Task> GetProjectAllocationAsync(Guid? projectId, Guid tenantId, Employee loggedInEmployee); Task>> ManageAllocationAsync(List projectAllocationDots, Guid tenantId, Employee loggedInEmployee); + Task> GetProjectsByEmployeeAsync(Guid employeeId, Guid tenantId, Employee loggedInEmployee); + Task>> AssigneProjectsToEmployeeAsync(List projectAllocationDtos, Guid employeeId, Guid tenantId, Employee loggedInEmployee); } } From 08e8e8d75fea2ab069d6f7b15b6fda845aae5af5 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 16 Jul 2025 12:39:16 +0530 Subject: [PATCH 077/124] Changed the business logic of teams and tasks API in DashboardController to accept project ID and provide data according to project ID or project IDs assigned to logged in user --- .../Controllers/DashboardController.cs | 210 +++++++++++++++--- 1 file changed, 176 insertions(+), 34 deletions(-) diff --git a/Marco.Pms.Services/Controllers/DashboardController.cs b/Marco.Pms.Services/Controllers/DashboardController.cs index 8ed0ba0..432459c 100644 --- a/Marco.Pms.Services/Controllers/DashboardController.cs +++ b/Marco.Pms.Services/Controllers/DashboardController.cs @@ -21,12 +21,15 @@ namespace Marco.Pms.Services.Controllers { private readonly ApplicationDbContext _context; private readonly UserHelper _userHelper; + private readonly ProjectsHelper _projectsHelper; private readonly ILoggingService _logger; private readonly PermissionServices _permissionServices; - public DashboardController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, PermissionServices permissionServices) + public static readonly Guid ActiveId = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731"); + public DashboardController(ApplicationDbContext context, UserHelper userHelper, ProjectsHelper projectsHelper, ILoggingService logger, PermissionServices permissionServices) { _context = context; _userHelper = userHelper; + _projectsHelper = projectsHelper; _logger = logger; _permissionServices = permissionServices; } @@ -162,46 +165,185 @@ namespace Marco.Pms.Services.Controllers return Ok(ApiResponse.SuccessResponse(projectDashboardVM, "Success", 200)); } + /// + /// Retrieves a dashboard summary of total employees and today's attendance. + /// If a projectId is provided, it returns totals for that project; otherwise, for all accessible active projects. + /// + /// Optional. The ID of a specific project to get totals for. [HttpGet("teams")] - public async Task GetTotalEmployees() + public async Task GetTotalEmployees([FromQuery] Guid? projectId) { - var tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var date = DateTime.UtcNow.Date; - - var Employees = await _context.Employees.Where(e => e.TenantId == tenantId && e.IsActive == true).Select(e => e.Id).ToListAsync(); - - var checkedInEmployee = await _context.Attendes.Where(e => e.InTime != null ? e.InTime.Value.Date == date : false).Select(e => e.EmployeeID).ToListAsync(); - - TeamDashboardVM teamDashboardVM = new TeamDashboardVM + try { - TotalEmployees = Employees.Count(), - InToday = checkedInEmployee.Distinct().Count() - }; - _logger.LogInfo("Today's total checked in employees fetched by employee {EmployeeId}", LoggedInEmployee.Id); - return Ok(ApiResponse.SuccessResponse(teamDashboardVM, "Success", 200)); - } + var tenantId = _userHelper.GetTenantId(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - [HttpGet("tasks")] - public async Task GetTotalTasks() - { - var tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var Tasks = await _context.WorkItems.Where(t => t.TenantId == tenantId).Select(t => new { PlannedWork = t.PlannedWork, CompletedWork = t.CompletedWork }).ToListAsync(); - TasksDashboardVM tasksDashboardVM = new TasksDashboardVM - { - TotalTasks = 0, - CompletedTasks = 0 - }; - foreach (var task in Tasks) - { - tasksDashboardVM.TotalTasks += task.PlannedWork; - tasksDashboardVM.CompletedTasks += task.CompletedWork; + _logger.LogInfo("GetTotalEmployees called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty); + + // --- Step 1: Get the list of projects the user can access --- + // This query is more efficient as it only selects the IDs needed. + var projects = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); + var accessibleActiveProjectIds = projects + .Where(p => p.ProjectStatusId == ActiveId) + .Select(p => p.Id) + .ToList(); + if (!accessibleActiveProjectIds.Any()) + { + _logger.LogInfo("User {UserId} has no accessible active projects.", loggedInEmployee.Id); + return Ok(ApiResponse.SuccessResponse(new TeamDashboardVM(), "No accessible active projects found.", 200)); + } + + // --- Step 2: Build the list of project IDs to query against --- + List finalProjectIds; + + if (projectId.HasValue) + { + // Security Check: Ensure the requested project is in the user's accessible list. + if (!accessibleActiveProjectIds.Contains(projectId.Value)) + { + _logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId} (not active or not accessible).", loggedInEmployee.Id, projectId.Value); + return StatusCode(403, ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to view this project, or it is not active.", 403)); + } + finalProjectIds = new List { projectId.Value }; + } + else + { + finalProjectIds = accessibleActiveProjectIds; + } + + // --- Step 3: Run efficient aggregation queries SEQUENTIALLY --- + // Since we only have one DbContext instance, we await each query one by one. + + // Query 1: Count total distinct employees allocated to the final project list + int totalEmployees = await _context.ProjectAllocations + .Where(pa => pa.TenantId == tenantId && + finalProjectIds.Contains(pa.ProjectId) && + pa.IsActive) + .Select(pa => pa.EmployeeId) + .Distinct() + .CountAsync(); + + // Query 2: Count total distinct employees who checked in today + // Use an efficient date range check + var today = DateTime.UtcNow.Date; + var tomorrow = today.AddDays(1); + + int inTodays = await _context.Attendes + .Where(a => a.InTime >= today && a.InTime < tomorrow && + finalProjectIds.Contains(a.ProjectID)) + .Select(a => a.EmployeeID) + .Distinct() + .CountAsync(); + + // --- Step 4: Assemble the response --- + var teamDashboardVM = new TeamDashboardVM + { + TotalEmployees = totalEmployees, + InToday = inTodays + }; + + _logger.LogInfo("Successfully fetched team dashboard for user {UserId}. Total: {TotalEmployees}, InToday: {InToday}", + loggedInEmployee.Id, teamDashboardVM.TotalEmployees, teamDashboardVM.InToday); + + return Ok(ApiResponse.SuccessResponse(teamDashboardVM, "Dashboard data retrieved successfully.", 200)); + } + catch (Exception ex) + { + _logger.LogError("An unexpected error occurred in GetTotalEmployees for projectId {ProjectId} \n {Error}", projectId ?? Guid.Empty, ex.Message); + return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred.", null, 500)); } - _logger.LogInfo("Total targeted tasks and total completed tasks fetched by employee {EmployeeId}", LoggedInEmployee.Id); - return Ok(ApiResponse.SuccessResponse(tasksDashboardVM, "Success", 200)); } + /// + /// Retrieves a dashboard summary of total planned and completed tasks. + /// If a projectId is provided, it returns totals for that project; otherwise, for all accessible projects. + /// + /// Optional. The ID of a specific project to get totals for. + /// An ApiResponse containing the task dashboard summary. + [HttpGet("tasks")] // Example route + public async Task GetTotalTasks1([FromQuery] Guid? projectId) // Changed to FromQuery as it's optional + { + try + { + var tenantId = _userHelper.GetTenantId(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + _logger.LogInfo("GetTotalTasks called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty); + + // --- Step 1: Build the base IQueryable for WorkItems --- + // This query is NOT executed yet. We will add more filters to it. + var baseWorkItemQuery = _context.WorkItems.Where(t => t.TenantId == tenantId); + + // --- Step 2: Apply Filters based on the request (Project or All Accessible) --- + if (projectId.HasValue) + { + // --- Logic for a SINGLE Project --- + + // 2a. Security Check: Verify permission for the specific project. + var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value.ToString()); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, projectId.Value); + return StatusCode(403, ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to view this project.", 403)); + } + + // 2b. Add project-specific filter to the base query. + // This is more efficient than fetching workAreaIds separately. + baseWorkItemQuery = baseWorkItemQuery + .Where(wi => wi.WorkArea != null && + wi.WorkArea.Floor != null && + wi.WorkArea.Floor.Building != null && + wi.WorkArea.Floor.Building.ProjectId == projectId.Value); + } + else + { + // --- Logic for ALL Accessible Projects --- + + // 2c. Get a list of all projects the user is allowed to see. + var accessibleProject = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); + var accessibleProjectIds = accessibleProject.Select(p => p.Id).ToList(); + if (!accessibleProjectIds.Any()) + { + _logger.LogInfo("User {UserId} has no accessible projects.", loggedInEmployee.Id); + // Return a zeroed-out dashboard if the user has no projects. + return Ok(ApiResponse.SuccessResponse(new TasksDashboardVM(), "No accessible projects found.", 200)); + } + + // 2d. Add a filter to include all work items from all accessible projects. + baseWorkItemQuery = baseWorkItemQuery + .Where(wi => wi.WorkArea != null && + wi.WorkArea.Floor != null && + wi.WorkArea.Floor.Building != null && + accessibleProjectIds.Contains(wi.WorkArea.Floor.Building.ProjectId)); + } + + // --- Step 3: Execute the Aggregation Query ON THE DATABASE SERVER --- + // This is the most powerful optimization. The database does all the summing. + // EF Core translates this into a single, efficient SQL query like: + // SELECT SUM(PlannedWork), SUM(CompletedWork) FROM WorkItems WHERE ... + var tasksDashboardVM = await baseWorkItemQuery + .GroupBy(x => 1) // Group by a constant to aggregate all rows into one result. + .Select(g => new TasksDashboardVM + { + TotalTasks = g.Sum(wi => wi.PlannedWork), + CompletedTasks = g.Sum(wi => wi.CompletedWork) + }) + .FirstOrDefaultAsync(); // Use FirstOrDefaultAsync as GroupBy might return no rows. + + // If the query returned no work items, the result will be null. Default to a zeroed object. + tasksDashboardVM ??= new TasksDashboardVM(); + + _logger.LogInfo("Successfully fetched task dashboard for user {UserId}. Total: {TotalTasks}, Completed: {CompletedTasks}", + loggedInEmployee.Id, tasksDashboardVM.TotalTasks, tasksDashboardVM.CompletedTasks); + + return Ok(ApiResponse.SuccessResponse(tasksDashboardVM, "Dashboard data retrieved successfully.", 200)); + } + catch (Exception ex) + { + _logger.LogError("An unexpected error occurred in GetTotalTasks for projectId {ProjectId} \n {Error}", projectId ?? Guid.Empty, ex.Message); + return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred.", null, 500)); + } + } [HttpGet("pending-attendance")] public async Task GetPendingAttendance() { From 2889620c1c9ce75e57685741bac9db6bcb8abdba Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 16 Jul 2025 14:49:34 +0530 Subject: [PATCH 078/124] only checking if the user have permission of project or not only --- Marco.Pms.Services/Controllers/DashboardController.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Marco.Pms.Services/Controllers/DashboardController.cs b/Marco.Pms.Services/Controllers/DashboardController.cs index 432459c..3829cdc 100644 --- a/Marco.Pms.Services/Controllers/DashboardController.cs +++ b/Marco.Pms.Services/Controllers/DashboardController.cs @@ -199,7 +199,8 @@ namespace Marco.Pms.Services.Controllers if (projectId.HasValue) { // Security Check: Ensure the requested project is in the user's accessible list. - if (!accessibleActiveProjectIds.Contains(projectId.Value)) + var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value.ToString()); + if (!hasPermission) { _logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId} (not active or not accessible).", loggedInEmployee.Id, projectId.Value); return StatusCode(403, ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to view this project, or it is not active.", 403)); From c79cbf32eab109df6d78165086fc212c74689068 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 16 Jul 2025 15:08:53 +0530 Subject: [PATCH 079/124] Optimized the manage task API in projectController --- Marco.Pms.CacheHelper/ProjectCache.cs | 33 +- .../{WorkItemDot.cs => WorkItemDto.cs} | 2 +- Marco.Pms.Model/Mapper/InfraMapper.cs | 2 +- .../Controllers/ProjectController.cs | 298 ++-------- .../Helpers/CacheUpdateHelper.cs | 17 +- Marco.Pms.Services/Helpers/GeneralHelper.cs | 214 +++++++ Marco.Pms.Services/Helpers/ProjectsHelper.cs | 4 +- .../MappingProfiles/MappingProfile.cs | 5 + Marco.Pms.Services/Program.cs | 1 + Marco.Pms.Services/Service/ProjectServices.cs | 547 +++++++++++++++++- .../ServiceInterfaces/IProjectServices.cs | 4 + 11 files changed, 826 insertions(+), 301 deletions(-) rename Marco.Pms.Model/Dtos/Projects/{WorkItemDot.cs => WorkItemDto.cs} (94%) create mode 100644 Marco.Pms.Services/Helpers/GeneralHelper.cs diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index c7d7e84..833e1a0 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -406,45 +406,22 @@ namespace Marco.Pms.CacheHelper return workItems; } - public async Task ManageWorkItemDetailsToCache(List workItems) + public async Task ManageWorkItemDetailsToCache(List workItems) { - var activityIds = workItems.Select(wi => wi.ActivityId).ToList(); - var workCategoryIds = workItems.Select(wi => wi.WorkCategoryId).ToList(); - var workItemIds = workItems.Select(wi => wi.Id).ToList(); - // fetching Activity master - var activities = await _context.ActivityMasters.Where(a => activityIds.Contains(a.Id)).ToListAsync() ?? new List(); - - // Fetching Work Category - var workCategories = await _context.WorkCategoryMasters.Where(wc => workCategoryIds.Contains(wc.Id)).ToListAsync() ?? new List(); - var task = await _context.TaskAllocations.Where(t => workItemIds.Contains(t.WorkItemId) && t.AssignmentDate == DateTime.UtcNow).ToListAsync(); - var todaysAssign = task.Sum(t => t.PlannedTask); - foreach (WorkItem workItem in workItems) + foreach (WorkItemMongoDB workItem in workItems) { - var activity = activities.FirstOrDefault(a => a.Id == workItem.ActivityId) ?? new ActivityMaster(); - var workCategory = workCategories.FirstOrDefault(a => a.Id == workItem.WorkCategoryId) ?? new WorkCategoryMaster(); - var filter = Builders.Filter.Eq(p => p.Id, workItem.Id.ToString()); var updates = Builders.Update.Combine( Builders.Update.Set(r => r.WorkAreaId, workItem.WorkAreaId.ToString()), Builders.Update.Set(r => r.ParentTaskId, (workItem.ParentTaskId != null ? workItem.ParentTaskId.ToString() : null)), Builders.Update.Set(r => r.PlannedWork, workItem.PlannedWork), - Builders.Update.Set(r => r.TodaysAssigned, todaysAssign), + Builders.Update.Set(r => r.TodaysAssigned, workItem.TodaysAssigned), Builders.Update.Set(r => r.CompletedWork, workItem.CompletedWork), Builders.Update.Set(r => r.Description, workItem.Description), Builders.Update.Set(r => r.TaskDate, workItem.TaskDate), Builders.Update.Set(r => r.ExpireAt, DateTime.UtcNow.Date.AddDays(1)), - Builders.Update.Set(r => r.ActivityMaster, new ActivityMasterMongoDB - { - Id = activity.Id.ToString(), - ActivityName = activity.ActivityName, - UnitOfMeasurement = activity.UnitOfMeasurement - }), - Builders.Update.Set(r => r.WorkCategoryMaster, new WorkCategoryMasterMongoDB - { - Id = workCategory.Id.ToString(), - Name = workCategory.Name, - Description = workCategory.Description, - }) + Builders.Update.Set(r => r.ActivityMaster, workItem.ActivityMaster), + Builders.Update.Set(r => r.WorkCategoryMaster, workItem.WorkCategoryMaster) ); var options = new UpdateOptions { IsUpsert = true }; var result = await _taskCollection.UpdateOneAsync(filter, updates, options); diff --git a/Marco.Pms.Model/Dtos/Projects/WorkItemDot.cs b/Marco.Pms.Model/Dtos/Projects/WorkItemDto.cs similarity index 94% rename from Marco.Pms.Model/Dtos/Projects/WorkItemDot.cs rename to Marco.Pms.Model/Dtos/Projects/WorkItemDto.cs index e6ba436..7c98051 100644 --- a/Marco.Pms.Model/Dtos/Projects/WorkItemDot.cs +++ b/Marco.Pms.Model/Dtos/Projects/WorkItemDto.cs @@ -2,7 +2,7 @@ namespace Marco.Pms.Model.Dtos.Project { - public class WorkItemDot + public class WorkItemDto { [Key] public Guid? Id { get; set; } diff --git a/Marco.Pms.Model/Mapper/InfraMapper.cs b/Marco.Pms.Model/Mapper/InfraMapper.cs index 4ccb7c8..89097d1 100644 --- a/Marco.Pms.Model/Mapper/InfraMapper.cs +++ b/Marco.Pms.Model/Mapper/InfraMapper.cs @@ -48,7 +48,7 @@ namespace Marco.Pms.Model.Mapper } public static class WorkItemMapper { - public static WorkItem ToWorkItemFromWorkItemDto(this WorkItemDot model, Guid tenantId) + public static WorkItem ToWorkItemFromWorkItemDto(this WorkItemDto model, Guid tenantId) { return new WorkItem { diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 82ce0dd..a10fc66 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -1,9 +1,7 @@ using Marco.Pms.DataAccess.Data; 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.MongoDBModels; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Projects; @@ -325,188 +323,36 @@ namespace MarcoBMS.Services.Controllers [HttpGet("infra-details/{projectId}")] public async Task GetInfraDetails(Guid projectId) { - _logger.LogInfo("GetInfraDetails called for ProjectId: {ProjectId}", projectId); - - // Step 1: Get logged-in employee - var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - // Step 2: Check project-specific permission - var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId); - if (!hasProjectPermission) + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) { - _logger.LogWarning("Project access denied for EmployeeId: {EmployeeId} on ProjectId: {ProjectId}", loggedInEmployee.Id, projectId); - return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have access to this project", 403)); + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("Get Project Infrastructure by ProjectId called with invalid model state \n Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - // Step 3: Check 'ViewInfra' permission - var hasViewInfraPermission = await _permission.HasPermission(PermissionsMaster.ViewProjectInfra, loggedInEmployee.Id); - if (!hasViewInfraPermission) - { - _logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); - return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have access to view infra", 403)); - } - var result = await _cache.GetBuildingInfra(projectId); - if (result == null) - { + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetInfraDetailsAsync(projectId, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); - // Step 4: Fetch buildings for the project - var buildings = await _context.Buildings - .Where(b => b.ProjectId == projectId) - .ToListAsync(); - - var buildingIds = buildings.Select(b => b.Id).ToList(); - - // Step 5: Fetch floors associated with the buildings - var floors = await _context.Floor - .Where(f => buildingIds.Contains(f.BuildingId)) - .ToListAsync(); - - var floorIds = floors.Select(f => f.Id).ToList(); - - // Step 6: Fetch work areas associated with the floors - var workAreas = await _context.WorkAreas - .Where(wa => floorIds.Contains(wa.FloorId)) - .ToListAsync(); - var workAreaIds = workAreas.Select(wa => wa.Id).ToList(); - - // Step 7: Fetch work items associated with the work area - var workItems = await _context.WorkItems - .Where(wi => workAreaIds.Contains(wi.WorkAreaId)) - .ToListAsync(); - - // Step 8: Build the infra hierarchy (Building > Floors > Work Areas) - List Buildings = new List(); - foreach (var building in buildings) - { - double buildingPlannedWorks = 0; - double buildingCompletedWorks = 0; - - var selectedFloors = floors.Where(f => f.BuildingId == building.Id).ToList(); - List Floors = new List(); - foreach (var floor in selectedFloors) - { - double floorPlannedWorks = 0; - double floorCompletedWorks = 0; - var selectedWorkAreas = workAreas.Where(wa => wa.FloorId == floor.Id).ToList(); - List WorkAreas = new List(); - foreach (var workArea in selectedWorkAreas) - { - double workAreaPlannedWorks = 0; - double workAreaCompletedWorks = 0; - var selectedWorkItems = workItems.Where(wi => wi.WorkAreaId == workArea.Id).ToList(); - foreach (var workItem in selectedWorkItems) - { - workAreaPlannedWorks += workItem.PlannedWork; - workAreaCompletedWorks += workItem.CompletedWork; - } - WorkAreaMongoDB workAreaMongo = new WorkAreaMongoDB - { - Id = workArea.Id.ToString(), - AreaName = workArea.AreaName, - PlannedWork = workAreaPlannedWorks, - CompletedWork = workAreaCompletedWorks - }; - WorkAreas.Add(workAreaMongo); - floorPlannedWorks += workAreaPlannedWorks; - floorCompletedWorks += workAreaCompletedWorks; - } - FloorMongoDB floorMongoDB = new FloorMongoDB - { - Id = floor.Id.ToString(), - FloorName = floor.FloorName, - PlannedWork = floorPlannedWorks, - CompletedWork = floorCompletedWorks, - WorkAreas = WorkAreas - }; - Floors.Add(floorMongoDB); - buildingPlannedWorks += floorPlannedWorks; - buildingCompletedWorks += floorCompletedWorks; - } - - var buildingMongo = new BuildingMongoDB - { - Id = building.Id.ToString(), - BuildingName = building.Name, - Description = building.Description, - PlannedWork = buildingPlannedWorks, - CompletedWork = buildingCompletedWorks, - Floors = Floors - }; - Buildings.Add(buildingMongo); - } - result = Buildings; - } - - _logger.LogInfo("Infra details fetched successfully for ProjectId: {ProjectId}, EmployeeId: {EmployeeId}, Buildings: {Count}", - projectId, loggedInEmployee.Id, result.Count); - - return Ok(ApiResponse.SuccessResponse(result, "Infra details fetched successfully", 200)); } [HttpGet("tasks/{workAreaId}")] public async Task GetWorkItems(Guid workAreaId) { - _logger.LogInfo("GetWorkItems called for WorkAreaId: {WorkAreaId}", workAreaId); - - // Step 1: Get the currently logged-in employee - var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - // Step 2: Check if the employee has ViewInfra permission - var hasViewInfraPermission = await _permission.HasPermission(PermissionsMaster.ViewProjectInfra, loggedInEmployee.Id); - if (!hasViewInfraPermission) + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) { - _logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); - return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have permission to view infrastructure", 403)); + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("Get Work Items by WorkAreaId called with invalid model state \n Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - // Step 3: Check if the specified Work Area exists - var isWorkAreaExist = await _context.WorkAreas.AnyAsync(wa => wa.Id == workAreaId); - if (!isWorkAreaExist) - { - _logger.LogWarning("Work Area not found for WorkAreaId: {WorkAreaId}", workAreaId); - return NotFound(ApiResponse.ErrorResponse("Work Area not found", "Work Area not found in database", 404)); - } - - // Step 4: Fetch WorkItems with related Activity and Work Category data - var workItemVMs = await _cache.GetWorkItemDetailsByWorkArea(workAreaId); - if (workItemVMs == null) - { - var workItems = await _context.WorkItems - .Include(wi => wi.ActivityMaster) - .Include(wi => wi.WorkCategoryMaster) - .Where(wi => wi.WorkAreaId == workAreaId) - .ToListAsync(); - - workItemVMs = workItems.Select(wi => new WorkItemMongoDB - { - Id = wi.Id.ToString(), - WorkAreaId = wi.WorkAreaId.ToString(), - ParentTaskId = wi.ParentTaskId.ToString(), - ActivityMaster = new ActivityMasterMongoDB - { - Id = wi.ActivityId.ToString(), - ActivityName = wi.ActivityMaster != null ? wi.ActivityMaster.ActivityName : null, - UnitOfMeasurement = wi.ActivityMaster != null ? wi.ActivityMaster.UnitOfMeasurement : null - }, - WorkCategoryMaster = new WorkCategoryMasterMongoDB - { - Id = wi.WorkCategoryId.ToString() ?? "", - Name = wi.WorkCategoryMaster != null ? wi.WorkCategoryMaster.Name : "", - Description = wi.WorkCategoryMaster != null ? wi.WorkCategoryMaster.Description : "" - }, - PlannedWork = wi.PlannedWork, - CompletedWork = wi.CompletedWork, - Description = wi.Description, - TaskDate = wi.TaskDate, - }).ToList(); - - await _cache.ManageWorkItemDetails(workItems); - } - - _logger.LogInfo("{Count} work items fetched successfully for WorkAreaId: {WorkAreaId}", workItemVMs.Count, workAreaId); - - // Step 5: Return result - return Ok(ApiResponse.SuccessResponse(workItemVMs, $"{workItemVMs.Count} records of tasks fetched successfully", 200)); + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetWorkItemsAsync(workAreaId, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } #endregion @@ -514,107 +360,29 @@ namespace MarcoBMS.Services.Controllers #region =================================================================== Project Infrastructre Manage APIs =================================================================== [HttpPost("task")] - public async Task CreateProjectTask(List workItemDtos) + public async Task CreateProjectTask([FromBody] List workItemDtos) { - _logger.LogInfo("CreateProjectTask called with {Count} items", workItemDtos?.Count ?? 0); - - // Validate request - if (workItemDtos == null || !workItemDtos.Any()) + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) { - _logger.LogWarning("No work items provided in the request."); - return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Work Item details are not valid.", 400)); + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - var workItemsToCreate = new List(); - var workItemsToUpdate = new List(); - var responseList = new List(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - string message = ""; - List workAreaIds = new List(); - var workItemIds = workItemDtos.Where(wi => wi.Id != null && wi.Id != Guid.Empty).Select(wi => wi.Id).ToList(); - var workItems = await _context.WorkItems.AsNoTracking().Where(wi => workItemIds.Contains(wi.Id)).ToListAsync(); - - foreach (var itemDto in workItemDtos) + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.CreateProjectTaskAsync(workItemDtos, tenantId, loggedInEmployee); + if (response.Success) { - var workItem = itemDto.ToWorkItemFromWorkItemDto(tenantId); - var workArea = await _context.WorkAreas.Include(a => a.Floor).FirstOrDefaultAsync(a => a.Id == workItem.WorkAreaId) ?? new WorkArea(); - - Building building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == (workArea.Floor != null ? workArea.Floor.BuildingId : Guid.Empty)) ?? new Building(); - - if (itemDto.Id != null && itemDto.Id != Guid.Empty) - { - // Update existing - workItemsToUpdate.Add(workItem); - message = $"Task Updated in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}"; - var existingWorkItem = workItems.FirstOrDefault(wi => wi.Id == workItem.Id); - double plannedWork = 0; - double completedWork = 0; - if (existingWorkItem != null) - { - if (existingWorkItem.PlannedWork != workItem.PlannedWork && existingWorkItem.CompletedWork != workItem.CompletedWork) - { - plannedWork = workItem.PlannedWork - existingWorkItem.PlannedWork; - completedWork = workItem.CompletedWork - existingWorkItem.CompletedWork; - } - else if (existingWorkItem.PlannedWork == workItem.PlannedWork && existingWorkItem.CompletedWork != workItem.CompletedWork) - { - plannedWork = 0; - completedWork = workItem.CompletedWork - existingWorkItem.CompletedWork; - } - else if (existingWorkItem.PlannedWork != workItem.PlannedWork && existingWorkItem.CompletedWork == workItem.CompletedWork) - { - plannedWork = workItem.PlannedWork - existingWorkItem.PlannedWork; - completedWork = 0; - } - await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, plannedWork, completedWork); - } - } - else - { - // Create new - workItem.Id = Guid.NewGuid(); - workItemsToCreate.Add(workItem); - message = $"Task Added in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}"; - await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, workItem.PlannedWork, workItem.CompletedWork); - } - - responseList.Add(new WorkItemVM - { - WorkItemId = workItem.Id, - WorkItem = workItem - }); - workAreaIds.Add(workItem.WorkAreaId); + List workAreaIds = response.Data.Select(pa => pa.WorkItem?.WorkAreaId ?? Guid.Empty).ToList(); + string message = response.Message; + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = message }; + await _signalR.SendNotificationAsync(notification); } - string responseMessage = ""; - // Apply DB changes - if (workItemsToCreate.Any()) - { - _logger.LogInfo("Adding {Count} new work items", workItemsToCreate.Count); - await _context.WorkItems.AddRangeAsync(workItemsToCreate); - responseMessage = "Task Added Successfully"; - await _cache.ManageWorkItemDetails(workItemsToCreate); - } + return StatusCode(response.StatusCode, response); - if (workItemsToUpdate.Any()) - { - _logger.LogInfo("Updating {Count} existing work items", workItemsToUpdate.Count); - _context.WorkItems.UpdateRange(workItemsToUpdate); - responseMessage = "Task Updated Successfully"; - await _cache.ManageWorkItemDetails(workItemsToUpdate); - } - - await _context.SaveChangesAsync(); - - _logger.LogInfo("CreateProjectTask completed successfully. Created: {Created}, Updated: {Updated}", workItemsToCreate.Count, workItemsToUpdate.Count); - - - - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = message }; - - await _signalR.SendNotificationAsync(notification); - - return Ok(ApiResponse.SuccessResponse(responseList, responseMessage, 200)); } [HttpDelete("task/{id}")] diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index aca439b..9a01b83 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -17,9 +17,10 @@ namespace Marco.Pms.Services.Helpers private readonly ILoggingService _logger; private readonly IDbContextFactory _dbContextFactory; private readonly ApplicationDbContext _context; + private readonly GeneralHelper _generalHelper; public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache, ReportCache reportCache, ILoggingService logger, - IDbContextFactory dbContextFactory, ApplicationDbContext context) + IDbContextFactory dbContextFactory, ApplicationDbContext context, GeneralHelper generalHelper) { _projectCache = projectCache; _employeeCache = employeeCache; @@ -27,6 +28,7 @@ namespace Marco.Pms.Services.Helpers _logger = logger; _dbContextFactory = dbContextFactory; _context = context; + _generalHelper = generalHelper; } // ------------------------------------ Project Details Cache --------------------------------------- @@ -563,6 +565,19 @@ namespace Marco.Pms.Services.Helpers } } public async Task ManageWorkItemDetails(List workItems) + { + try + { + var workAreaId = workItems.First().WorkAreaId; + var workItemDB = await _generalHelper.GetWorkItemsListFromDB(workAreaId); + await _projectCache.ManageWorkItemDetailsToCache(workItemDB); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while saving workItems form Cache: {Error}", ex.Message); + } + } + public async Task ManageWorkItemDetailsByVM(List workItems) { try { diff --git a/Marco.Pms.Services/Helpers/GeneralHelper.cs b/Marco.Pms.Services/Helpers/GeneralHelper.cs new file mode 100644 index 0000000..c2f8fe4 --- /dev/null +++ b/Marco.Pms.Services/Helpers/GeneralHelper.cs @@ -0,0 +1,214 @@ +using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.MongoDBModels; +using MarcoBMS.Services.Service; +using Microsoft.EntityFrameworkCore; + +namespace Marco.Pms.Services.Helpers +{ + public class GeneralHelper + { + private readonly IDbContextFactory _dbContextFactory; + private readonly ApplicationDbContext _context; // Keeping this for direct scoped context use where appropriate + private readonly ILoggingService _logger; + public GeneralHelper(IDbContextFactory dbContextFactory, + ApplicationDbContext context, + ILoggingService logger) + { + _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); + _context = context ?? throw new ArgumentNullException(nameof(context)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + public async Task> GetProjectInfraFromDB(Guid projectId) + { + // Each task uses its own DbContext instance for thread safety. Projections are used for efficiency. + + // Task to fetch Buildings, Floors, and WorkAreas using projections + var hierarchyTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + var buildings = await context.Buildings.AsNoTracking().Where(b => b.ProjectId == projectId).Select(b => new { b.Id, b.Name, b.Description }).ToListAsync(); + var buildingIds = buildings.Select(b => b.Id).ToList(); + var floors = await context.Floor.AsNoTracking().Where(f => buildingIds.Contains(f.BuildingId)).Select(f => new { f.Id, f.BuildingId, f.FloorName }).ToListAsync(); + var floorIds = floors.Select(f => f.Id).ToList(); + var workAreas = await context.WorkAreas.AsNoTracking().Where(wa => floorIds.Contains(wa.FloorId)).Select(wa => new { wa.Id, wa.FloorId, wa.AreaName }).ToListAsync(); + return (buildings, floors, workAreas); + }); + + // Task to get work summaries, AGGREGATED ON THE DATABASE SERVER + var workSummaryTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + // This is the most powerful optimization. It avoids pulling all WorkItem rows. + return await context.WorkItems.AsNoTracking() + .Where(wi => wi.WorkArea != null && wi.WorkArea.Floor != null && wi.WorkArea.Floor.Building != null && wi.WorkArea.Floor.Building.ProjectId == projectId) + .GroupBy(wi => wi.WorkAreaId) // Group by the parent WorkArea + .Select(g => new + { + WorkAreaId = g.Key, + PlannedWork = g.Sum(i => i.PlannedWork), + CompletedWork = g.Sum(i => i.CompletedWork) + }) + .ToDictionaryAsync(x => x.WorkAreaId); // Return a ready-to-use dictionary for fast lookups + }); + + await Task.WhenAll(hierarchyTask, workSummaryTask); + + var (buildings, floors, workAreas) = await hierarchyTask; + var workSummariesByWorkAreaId = await workSummaryTask; + + // --- Step 4: Build the hierarchy efficiently using Lookups --- + // Using lookups is much faster (O(1)) than repeated .Where() calls (O(n)). + var floorsByBuildingId = floors.ToLookup(f => f.BuildingId); + var workAreasByFloorId = workAreas.ToLookup(wa => wa.FloorId); + + var buildingMongoList = new List(); + foreach (var building in buildings) + { + 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 workArea in workAreasByFloorId[floor.Id]) // Fast lookup + { + // Get the pre-calculated summary from the dictionary. O(1) operation. + workSummariesByWorkAreaId.TryGetValue(workArea.Id, out var summary); + var waPlanned = summary?.PlannedWork ?? 0; + var waCompleted = summary?.CompletedWork ?? 0; + + workAreaMongoList.Add(new WorkAreaMongoDB + { + Id = workArea.Id.ToString(), + AreaName = workArea.AreaName, + PlannedWork = waPlanned, + CompletedWork = waCompleted + }); + + floorPlanned += waPlanned; + floorCompleted += waCompleted; + } + + floorMongoList.Add(new FloorMongoDB + { + Id = floor.Id.ToString(), + FloorName = floor.FloorName, + PlannedWork = floorPlanned, + CompletedWork = floorCompleted, + WorkAreas = workAreaMongoList + }); + + buildingPlanned += floorPlanned; + buildingCompleted += floorCompleted; + } + + buildingMongoList.Add(new BuildingMongoDB + { + Id = building.Id.ToString(), + BuildingName = building.Name, + Description = building.Description, + PlannedWork = buildingPlanned, + CompletedWork = buildingCompleted, + Floors = floorMongoList + }); + } + return buildingMongoList; + } + + /// + /// Retrieves a list of work items for a specific work area, including a summary of tasks assigned for the current day. + /// This method is highly optimized to run database operations in parallel and perform aggregations on the server. + /// + /// The ID of the work area. + /// A list of WorkItemMongoDB objects with calculated daily assignments. + public async Task> GetWorkItemsListFromDB(Guid workAreaId) + { + _logger.LogInfo("Fetching DB work items for WorkAreaId: {WorkAreaId}", workAreaId); + + try + { + // --- Step 1: Run independent database queries in PARALLEL --- + // We can fetch the WorkItems and the aggregated TaskAllocations at the same time. + + // Task 1: Fetch the WorkItem entities and their related data. + var workItemsTask = _context.WorkItems + .Include(wi => wi.ActivityMaster) + .Include(wi => wi.WorkCategoryMaster) + .Where(wi => wi.WorkAreaId == workAreaId) + .AsNoTracking() + .ToListAsync(); + + // Task 2: Fetch and AGGREGATE today's task allocations ON THE DATABASE SERVER. + var todaysAssignmentsTask = Task.Run(async () => + { + // Correctly define "today's" date range to avoid precision issues. + var today = DateTime.UtcNow.Date; + var tomorrow = today.AddDays(1); + + using var context = _dbContextFactory.CreateDbContext(); // Use a factory for thread safety + + // This is the most powerful optimization: + // 1. It filters by WorkAreaId directly, making it independent of the first query. + // 2. It filters by a correct date range. + // 3. It groups and sums on the DB server, returning only a small summary. + return await context.TaskAllocations + .Where(t => t.WorkItem != null && t.WorkItem.WorkAreaId == workAreaId && + t.AssignmentDate >= today && t.AssignmentDate < tomorrow) + .GroupBy(t => t.WorkItemId) + .Select(g => new + { + WorkItemId = g.Key, + TodaysAssigned = g.Sum(x => x.PlannedTask) + }) + // Return a dictionary for instant O(1) lookups later. + .ToDictionaryAsync(x => x.WorkItemId, x => x.TodaysAssigned); + }); + + // Await both parallel database operations to complete. + await Task.WhenAll(workItemsTask, todaysAssignmentsTask); + + // Retrieve the results from the completed tasks. + var workItemsFromDb = await workItemsTask; + var todaysAssignments = await todaysAssignmentsTask; + + // --- Step 2: Map to the ViewModel/MongoDB model efficiently --- + var workItemVMs = workItemsFromDb.Select(wi => new WorkItemMongoDB + { + Id = wi.Id.ToString(), + WorkAreaId = wi.WorkAreaId.ToString(), + ParentTaskId = wi.ParentTaskId.ToString(), + ActivityMaster = wi.ActivityMaster != null ? new ActivityMasterMongoDB + { + Id = wi.ActivityMaster.Id.ToString(), + ActivityName = wi.ActivityMaster.ActivityName, + UnitOfMeasurement = wi.ActivityMaster.UnitOfMeasurement + } : null, + WorkCategoryMaster = wi.WorkCategoryMaster != null ? new WorkCategoryMasterMongoDB + { + Id = wi.WorkCategoryMaster.Id.ToString(), + Name = wi.WorkCategoryMaster.Name, + Description = wi.WorkCategoryMaster.Description + } : null, + PlannedWork = wi.PlannedWork, + CompletedWork = wi.CompletedWork, + Description = wi.Description, + TaskDate = wi.TaskDate, + // Use the fast dictionary lookup instead of the slow in-memory Where/Sum. + TodaysAssigned = todaysAssignments.GetValueOrDefault(wi.Id, 0) + }).ToList(); + + _logger.LogInfo("Successfully processed {WorkItemCount} work items for WorkAreaId: {WorkAreaId}", workItemVMs.Count, workAreaId); + + return workItemVMs; + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while fetching DB work items for WorkAreaId: {WorkAreaId}", workAreaId); + // Return an empty list or re-throw, depending on your application's error handling strategy. + return new List(); + } + } + } +} diff --git a/Marco.Pms.Services/Helpers/ProjectsHelper.cs b/Marco.Pms.Services/Helpers/ProjectsHelper.cs index fe70a0a..e7e1dd6 100644 --- a/Marco.Pms.Services/Helpers/ProjectsHelper.cs +++ b/Marco.Pms.Services/Helpers/ProjectsHelper.cs @@ -11,14 +11,12 @@ namespace MarcoBMS.Services.Helpers public class ProjectsHelper { private readonly ApplicationDbContext _context; - private readonly RolesHelper _rolesHelper; private readonly CacheUpdateHelper _cache; private readonly PermissionServices _permission; - public ProjectsHelper(ApplicationDbContext context, RolesHelper rolesHelper, CacheUpdateHelper cache, PermissionServices permission) + public ProjectsHelper(ApplicationDbContext context, CacheUpdateHelper cache, PermissionServices permission) { _context = context; - _rolesHelper = rolesHelper; _cache = cache; _permission = permission; } diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index ea42d16..50d2ea9 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -50,6 +50,11 @@ namespace Marco.Pms.Services.MappingProfiles opt => opt.MapFrom(src => src.EmpID)); CreateMap(); CreateMap(); + + CreateMap() + .ForMember( + dest => dest.Description, + opt => opt.MapFrom(src => src.Comment)); #endregion #region ======================================================= Projects ======================================================= diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index 26d8eba..3c73416 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -163,6 +163,7 @@ builder.Services.AddScoped(); #endregion #region Helpers +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs index 9024112..6d811fc 100644 --- a/Marco.Pms.Services/Service/ProjectServices.cs +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -29,6 +29,7 @@ namespace Marco.Pms.Services.Service private readonly PermissionServices _permission; private readonly CacheUpdateHelper _cache; private readonly IMapper _mapper; + private readonly GeneralHelper _generalHelper; public ProjectServices( IDbContextFactory dbContextFactory, ApplicationDbContext context, @@ -36,7 +37,8 @@ namespace Marco.Pms.Services.Service ProjectsHelper projectsHelper, PermissionServices permission, CacheUpdateHelper cache, - IMapper mapper) + IMapper mapper, + GeneralHelper generalHelper) { _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); _context = context ?? throw new ArgumentNullException(nameof(context)); @@ -45,6 +47,7 @@ namespace Marco.Pms.Services.Service _permission = permission ?? throw new ArgumentNullException(nameof(permission)); _cache = cache ?? throw new ArgumentNullException(nameof(cache)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + _generalHelper = generalHelper ?? throw new ArgumentNullException(nameof(generalHelper)); } #region =================================================================== Project Get APIs =================================================================== @@ -898,6 +901,525 @@ namespace Marco.Pms.Services.Service #endregion + #region =================================================================== Project InfraStructure Get APIs =================================================================== + + /// + /// Retrieves the full infrastructure hierarchy (Buildings, Floors, Work Areas) for a project, + /// including aggregated work summaries. + /// + public async Task> GetInfraDetailsAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee) + { + _logger.LogInfo("GetInfraDetails called for ProjectId: {ProjectId}", projectId); + + try + { + // --- Step 1: Run independent permission checks in PARALLEL --- + var projectPermissionTask = _permission.HasProjectPermission(loggedInEmployee, projectId); + var viewInfraPermissionTask = _permission.HasPermission(PermissionsMaster.ViewProjectInfra, loggedInEmployee.Id); + + await Task.WhenAll(projectPermissionTask, viewInfraPermissionTask); + + if (!await projectPermissionTask) + { + _logger.LogWarning("Project access denied for EmployeeId: {EmployeeId} on ProjectId: {ProjectId}", loggedInEmployee.Id, projectId); + return ApiResponse.ErrorResponse("Access denied", "You don't have access to this project", 403); + } + if (!await viewInfraPermissionTask) + { + _logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Access denied", "You don't have access to view this project's infrastructure", 403); + } + + // --- Step 2: Cache-First Strategy --- + var cachedResult = await _cache.GetBuildingInfra(projectId); + if (cachedResult != null) + { + _logger.LogInfo("Cache HIT for infra details for ProjectId: {ProjectId}", projectId); + return ApiResponse.SuccessResponse(cachedResult, "Infra details fetched successfully from cache.", 200); + } + + _logger.LogInfo("Cache MISS for infra details for ProjectId: {ProjectId}. Fetching from database.", projectId); + + // --- Step 3: Fetch all required data from the database --- + + var buildingMongoList = await _generalHelper.GetProjectInfraFromDB(projectId); + // --- Step 5: Proactively update the cache --- + //await _cache.SetBuildingInfra(projectId, buildingMongoList); + + _logger.LogInfo("Infra details fetched successfully for ProjectId: {ProjectId}, Buildings: {Count}", projectId, buildingMongoList.Count); + return ApiResponse.SuccessResponse(buildingMongoList, "Infra details fetched successfully", 200); + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while fetching infra details for ProjectId: {ProjectId}", projectId); + return ApiResponse.ErrorResponse("An internal server error occurred.", "An error occurred while processing your request.", 500); + } + } + + /// + /// Retrieves a list of work items for a specific work area, ensuring the user has appropriate permissions. + /// + /// The ID of the work area. + /// The ID of the current tenant. + /// The current authenticated employee for permission checks. + /// An ApiResponse containing a list of work items or an error. + public async Task> GetWorkItemsAsync(Guid workAreaId, Guid tenantId, Employee loggedInEmployee) + { + _logger.LogInfo("GetWorkItems called for WorkAreaId: {WorkAreaId} by User: {UserId}", workAreaId, loggedInEmployee.Id); + + try + { + // --- Step 1: Cache-First Strategy --- + var cachedWorkItems = await _cache.GetWorkItemDetailsByWorkArea(workAreaId); + if (cachedWorkItems != null) + { + _logger.LogInfo("Cache HIT for WorkAreaId: {WorkAreaId}. Returning {Count} items from cache.", workAreaId, cachedWorkItems.Count); + return ApiResponse.SuccessResponse(cachedWorkItems, $"{cachedWorkItems.Count} tasks retrieved successfully from cache.", 200); + } + + _logger.LogInfo("Cache MISS for WorkAreaId: {WorkAreaId}. Fetching from database.", workAreaId); + + // --- Step 2: Security Check First --- + // This pattern remains the most robust: verify permissions before fetching a large list. + var projectInfo = await _context.WorkAreas + .Where(wa => wa.Id == workAreaId && wa.TenantId == tenantId && wa.Floor != null && wa.Floor.Building != null) + .Select(wa => new { wa.Floor!.Building!.ProjectId }) + .FirstOrDefaultAsync(); + + if (projectInfo == null) + { + _logger.LogWarning("Work Area not found for WorkAreaId: {WorkAreaId}", workAreaId); + return ApiResponse.ErrorResponse("Not Found", $"Work Area with ID {workAreaId} not found.", 404); + } + + var hasProjectAccess = await _permission.HasProjectPermission(loggedInEmployee, projectInfo.ProjectId); + var hasGenericViewInfraPermission = await _permission.HasPermission(PermissionsMaster.ViewProjectInfra, loggedInEmployee.Id); + + if (!hasProjectAccess || !hasGenericViewInfraPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} on WorkAreaId {WorkAreaId}.", loggedInEmployee.Id, workAreaId); + return ApiResponse.ErrorResponse("Access Denied", "You do not have sufficient permissions to view these work items.", 403); + } + + // --- Step 3: Fetch Full Entities for Caching and Mapping --- + var workItemVMs = await _generalHelper.GetWorkItemsListFromDB(workAreaId); + + // --- Step 5: Proactively Update the Cache with the Correct Object Type --- + // We now pass the 'workItemsFromDb' list, which is the required List. + + try + { + await _cache.ManageWorkItemDetailsByVM(workItemVMs); + _logger.LogInfo("Successfully queued cache update for WorkAreaId: {WorkAreaId}", workAreaId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Background cache update failed for WorkAreaId: {WorkAreaId}", workAreaId); + } + + + _logger.LogInfo("{Count} work items fetched successfully for WorkAreaId: {WorkAreaId}", workItemVMs.Count, workAreaId); + return ApiResponse.SuccessResponse(workItemVMs, $"{workItemVMs.Count} tasks fetched successfully.", 200); + } + catch (Exception ex) + { + // --- Step 6: Graceful Error Handling --- + _logger.LogError(ex, "An unexpected error occurred while getting work items for WorkAreaId: {WorkAreaId}", workAreaId); + return ApiResponse.ErrorResponse("An internal server error occurred.", null, 500); + } + } + + #endregion + + #region =================================================================== Project Infrastructre Manage APIs =================================================================== + + public async Task> CreateProjectTask1(List workItemDtos, Guid tenantId, Employee loggedInEmployee) + { + _logger.LogInfo("CreateProjectTask called with {Count} items", workItemDtos?.Count ?? 0); + + // Validate request + if (workItemDtos == null || !workItemDtos.Any()) + { + _logger.LogWarning("No work items provided in the request."); + return ApiResponse.ErrorResponse("Invalid details.", "Work Item details are not valid.", 400); + } + + var workItemsToCreate = new List(); + var workItemsToUpdate = new List(); + var responseList = new List(); + string message = ""; + List workAreaIds = new List(); + var workItemIds = workItemDtos.Where(wi => wi.Id != null && wi.Id != Guid.Empty).Select(wi => wi.Id).ToList(); + var workItems = await _context.WorkItems.AsNoTracking().Where(wi => workItemIds.Contains(wi.Id)).ToListAsync(); + + foreach (var itemDto in workItemDtos) + { + var workItem = _mapper.Map(itemDto); + workItem.TenantId = tenantId; + var workArea = await _context.WorkAreas.Include(a => a.Floor).FirstOrDefaultAsync(a => a.Id == workItem.WorkAreaId) ?? new WorkArea(); + + Building building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == (workArea.Floor != null ? workArea.Floor.BuildingId : Guid.Empty)) ?? new Building(); + + if (itemDto.Id != null && itemDto.Id != Guid.Empty) + { + // Update existing + workItemsToUpdate.Add(workItem); + message = $"Task Updated in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; + var existingWorkItem = workItems.FirstOrDefault(wi => wi.Id == workItem.Id); + if (existingWorkItem != null) + { + double plannedWork = workItem.PlannedWork - existingWorkItem.PlannedWork; + double completedWork = workItem.CompletedWork - existingWorkItem.CompletedWork; + await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, plannedWork, completedWork); + } + } + else + { + // Create new + workItem.Id = Guid.NewGuid(); + workItemsToCreate.Add(workItem); + message = $"Task Added in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; + await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, workItem.PlannedWork, workItem.CompletedWork); + } + + responseList.Add(new WorkItemVM + { + WorkItemId = workItem.Id, + WorkItem = workItem + }); + workAreaIds.Add(workItem.WorkAreaId); + + } + // Apply DB changes + if (workItemsToCreate.Any()) + { + _logger.LogInfo("Adding {Count} new work items", workItemsToCreate.Count); + await _context.WorkItems.AddRangeAsync(workItemsToCreate); + await _cache.ManageWorkItemDetails(workItemsToCreate); + } + + if (workItemsToUpdate.Any()) + { + _logger.LogInfo("Updating {Count} existing work items", workItemsToUpdate.Count); + _context.WorkItems.UpdateRange(workItemsToUpdate); + await _cache.ManageWorkItemDetails(workItemsToUpdate); + } + + await _context.SaveChangesAsync(); + + _logger.LogInfo("CreateProjectTask completed successfully. Created: {Created}, Updated: {Updated}", workItemsToCreate.Count, workItemsToUpdate.Count); + + return ApiResponse.SuccessResponse(responseList, message, 200); + } + + /// + /// Creates or updates a batch of work items. + /// This method is optimized to perform all database operations in a single, atomic transaction. + /// + public async Task>> CreateProjectTaskAsync(List workItemDtos, Guid tenantId, Employee loggedInEmployee) + { + _logger.LogInfo("CreateProjectTask called with {Count} items by user {UserId}", workItemDtos?.Count ?? 0, loggedInEmployee.Id); + + // --- Step 1: Input Validation --- + if (workItemDtos == null || !workItemDtos.Any()) + { + _logger.LogWarning("No work items provided in the request."); + return ApiResponse>.ErrorResponse("Invalid details.", "Work Item details list cannot be empty.", 400); + } + + // --- Step 2: Fetch all required existing data in bulk --- + var workAreaIds = workItemDtos.Select(d => d.WorkAreaID).Distinct().ToList(); + var workItemIdsToUpdate = workItemDtos.Where(d => d.Id.HasValue).Select(d => d.Id!.Value).ToList(); + + // Fetch all relevant WorkAreas and their parent hierarchy in ONE query + var workAreasFromDb = await _context.WorkAreas + .Where(wa => wa.Floor != null && wa.Floor.Building != null && workAreaIds.Contains(wa.Id) && wa.TenantId == tenantId) + .Include(wa => wa.Floor!.Building) // Eagerly load the entire path + .ToDictionaryAsync(wa => wa.Id); // Dictionary for fast lookups + + // Fetch all existing WorkItems that need updating in ONE query + var existingWorkItemsToUpdate = await _context.WorkItems + .Where(wi => workItemIdsToUpdate.Contains(wi.Id) && wi.TenantId == tenantId) + .ToDictionaryAsync(wi => wi.Id); // Dictionary for fast lookups + + // --- (Placeholder) Security Check --- + // You MUST verify the user has permission to modify ALL WorkAreas in the batch. + var projectIdsInBatch = workAreasFromDb.Values.Select(wa => wa.Floor!.Building!.ProjectId).Distinct(); + var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageProjectInfra, loggedInEmployee.Id); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} trying to create/update tasks.", loggedInEmployee.Id); + return ApiResponse>.ErrorResponse("Access Denied.", "You do not have permission to modify tasks in one or more of the specified work areas.", 403); + } + + var workItemsToCreate = new List(); + var workItemsToModify = new List(); + var workDeltaForCache = new Dictionary(); // WorkAreaId -> (Delta) + string message = ""; + + // --- Step 3: Process all logic IN MEMORY, tracking changes --- + foreach (var dto in workItemDtos) + { + if (!workAreasFromDb.TryGetValue(dto.WorkAreaID, out var workArea)) + { + _logger.LogWarning("Skipping item because WorkAreaId {WorkAreaId} was not found or is invalid.", dto.WorkAreaID); + continue; // Skip this item as its parent WorkArea is invalid + } + + if (dto.Id.HasValue && existingWorkItemsToUpdate.TryGetValue(dto.Id.Value, out var existingWorkItem)) + { + // --- UPDATE Logic --- + var plannedDelta = dto.PlannedWork - existingWorkItem.PlannedWork; + var completedDelta = dto.CompletedWork - existingWorkItem.CompletedWork; + + // Apply changes from DTO to the fetched entity to prevent data loss + _mapper.Map(dto, existingWorkItem); + workItemsToModify.Add(existingWorkItem); + + // Track the change in work for cache update + workDeltaForCache[workArea.Id] = ( + workDeltaForCache.GetValueOrDefault(workArea.Id).Planned + plannedDelta, + workDeltaForCache.GetValueOrDefault(workArea.Id).Completed + completedDelta + ); + message = $"Task Updated in Building: {workArea.Floor?.Building?.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; + } + else + { + // --- CREATE Logic --- + var newWorkItem = _mapper.Map(dto); + newWorkItem.Id = Guid.NewGuid(); // Ensure new GUID is set + newWorkItem.TenantId = tenantId; + workItemsToCreate.Add(newWorkItem); + + // Track the change in work for cache update + workDeltaForCache[workArea.Id] = ( + workDeltaForCache.GetValueOrDefault(workArea.Id).Planned + newWorkItem.PlannedWork, + workDeltaForCache.GetValueOrDefault(workArea.Id).Completed + newWorkItem.CompletedWork + ); + message = $"Task Added in Building: {workArea.Floor?.Building?.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; + } + } + + try + { + // --- Step 4: Save all database changes in a SINGLE TRANSACTION --- + if (workItemsToCreate.Any()) _context.WorkItems.AddRange(workItemsToCreate); + if (workItemsToModify.Any()) _context.WorkItems.UpdateRange(workItemsToModify); // EF Core handles individual updates correctly here + + if (workItemsToCreate.Any() || workItemsToModify.Any()) + { + await _context.SaveChangesAsync(); + _logger.LogInfo("Successfully saved {CreatedCount} new and {UpdatedCount} updated work items.", workItemsToCreate.Count, workItemsToModify.Count); + + // --- Step 5: Update Cache and SignalR AFTER successful DB save (non-blocking) --- + var allAffectedItems = workItemsToCreate.Concat(workItemsToModify).ToList(); + _ = Task.Run(async () => + { + await UpdateCacheAndNotify(workDeltaForCache, allAffectedItems); + }); + } + } + catch (DbUpdateException ex) + { + _logger.LogError(ex, "A database error occurred while creating/updating tasks."); + return ApiResponse>.ErrorResponse("Database Error", "Failed to save changes.", 500); + } + + // --- Step 6: Prepare and return the response --- + var allProcessedItems = workItemsToCreate.Concat(workItemsToModify).ToList(); + var responseList = allProcessedItems.Select(wi => new WorkItemVM + { + WorkItemId = wi.Id, + WorkItem = wi + }).ToList(); + + + return ApiResponse>.SuccessResponse(responseList, message, 200); + } + + + //public async Task DeleteProjectTask(Guid id) + //{ + // var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + // List workAreaIds = new List(); + // WorkItem? task = await _context.WorkItems.AsNoTracking().Include(t => t.WorkArea).FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId); + // if (task != null) + // { + // if (task.CompletedWork == 0) + // { + // var assignedTask = await _context.TaskAllocations.Where(t => t.WorkItemId == id).ToListAsync(); + // if (assignedTask.Count == 0) + // { + // _context.WorkItems.Remove(task); + // await _context.SaveChangesAsync(); + // _logger.LogInfo("Task with ID {WorkItemId} has been successfully deleted.", task.Id); + + // var floorId = task.WorkArea?.FloorId; + // var floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == floorId); + + + // workAreaIds.Add(task.WorkAreaId); + // var projectId = floor?.Building?.ProjectId; + + // var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = $"Task Deleted in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}, in Area: {task.WorkArea?.AreaName} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}" }; + // await _signalR.SendNotificationAsync(notification); + // await _cache.DeleteWorkItemByIdAsync(task.Id); + // if (projectId != null) + // { + // await _cache.DeleteProjectByIdAsync(projectId.Value); + // } + // } + // else + // { + // _logger.LogWarning("Task with ID {WorkItemId} is currently assigned and cannot be deleted.", task.Id); + // return BadRequest(ApiResponse.ErrorResponse("Task is currently assigned and cannot be deleted.", "Task is currently assigned and cannot be deleted.", 400)); + // } + // } + // else + // { + // double percentage = (task.CompletedWork / task.PlannedWork) * 100; + // percentage = Math.Round(percentage, 2); + // _logger.LogWarning("Task with ID {WorkItemId} is {CompletionPercentage}% complete and cannot be deleted", task.Id, percentage); + // return BadRequest(ApiResponse.ErrorResponse(System.String.Format("Task is {0}% complete and cannot be deleted", percentage), System.String.Format("Task is {0}% complete and cannot be deleted", percentage), 400)); + + // } + // } + // else + // { + // _logger.LogWarning("Task with ID {WorkItemId} not found ID database", id); + // } + // return Ok(ApiResponse.SuccessResponse(new { }, "Task deleted successfully", 200)); + //} + + //public async Task ManageProjectInfra(List infraDots) + //{ + // var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // var responseData = new InfraVM { }; + // string responseMessage = ""; + // string message = ""; + // List projectIds = new List(); + // if (infraDots != null) + // { + // foreach (var item in infraDots) + // { + // if (item.Building != null) + // { + + // Building building = item.Building.ToBuildingFromBuildingDto(tenantId); + // building.TenantId = tenantId; + + // if (item.Building.Id == null) + // { + // //create + // _context.Buildings.Add(building); + // await _context.SaveChangesAsync(); + // responseData.building = building; + // responseMessage = "Buliding Added Successfully"; + // message = "Building Added"; + // await _cache.AddBuildngInfra(building.ProjectId, building); + // } + // else + // { + // //update + // _context.Buildings.Update(building); + // await _context.SaveChangesAsync(); + // responseData.building = building; + // responseMessage = "Buliding Updated Successfully"; + // message = "Building Updated"; + // await _cache.UpdateBuildngInfra(building.ProjectId, building); + // } + // projectIds.Add(building.ProjectId); + // } + // if (item.Floor != null) + // { + // Floor floor = item.Floor.ToFloorFromFloorDto(tenantId); + // floor.TenantId = tenantId; + // bool isCreated = false; + + // if (item.Floor.Id == null) + // { + // //create + // _context.Floor.Add(floor); + // await _context.SaveChangesAsync(); + // responseData.floor = floor; + // responseMessage = "Floor Added Successfully"; + // message = "Floor Added"; + // isCreated = true; + // } + // else + // { + // //update + // _context.Floor.Update(floor); + // await _context.SaveChangesAsync(); + // responseData.floor = floor; + // responseMessage = "Floor Updated Successfully"; + // message = "Floor Updated"; + // } + // Building? building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == floor.BuildingId); + // var projectId = building?.ProjectId ?? Guid.Empty; + // projectIds.Add(projectId); + // message = $"{message} in Building: {building?.Name}"; + // if (isCreated) + // { + // await _cache.AddBuildngInfra(projectId, floor: floor); + // } + // else + // { + // await _cache.UpdateBuildngInfra(projectId, floor: floor); + // } + // } + // if (item.WorkArea != null) + // { + // WorkArea workArea = item.WorkArea.ToWorkAreaFromWorkAreaDto(tenantId); + // workArea.TenantId = tenantId; + // bool isCreated = false; + + // if (item.WorkArea.Id == null) + // { + // //create + // _context.WorkAreas.Add(workArea); + // await _context.SaveChangesAsync(); + // responseData.workArea = workArea; + // responseMessage = "Work Area Added Successfully"; + // message = "Work Area Added"; + // isCreated = true; + // } + // else + // { + // //update + // _context.WorkAreas.Update(workArea); + // await _context.SaveChangesAsync(); + // responseData.workArea = workArea; + // responseMessage = "Work Area Updated Successfully"; + // message = "Work Area Updated"; + // } + // Floor? floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == workArea.FloorId); + // var projectId = floor?.Building?.ProjectId ?? Guid.Empty; + // projectIds.Add(projectId); + // message = $"{message} in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}"; + // if (isCreated) + // { + // await _cache.AddBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); + // } + // else + // { + // await _cache.UpdateBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); + // } + // } + // } + // message = $"{message} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; + // var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = message }; + + // await _signalR.SendNotificationAsync(notification); + // return Ok(ApiResponse.SuccessResponse(responseData, responseMessage, 200)); + // } + // return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Infra Details are not valid.", 400)); + + //} + + #endregion + #region =================================================================== Helper Functions =================================================================== /// @@ -1101,7 +1623,6 @@ namespace Marco.Pms.Services.Service return dbProject; } - // Helper method for background cache update private async Task UpdateCacheInBackground(Project project) { try @@ -1120,6 +1641,28 @@ namespace Marco.Pms.Services.Service } } + private async Task UpdateCacheAndNotify(Dictionary workDelta, List affectedItems) + { + try + { + // Update planned/completed work totals + var cacheUpdateTasks = workDelta.Select(kvp => + _cache.UpdatePlannedAndCompleteWorksInBuilding(kvp.Key, kvp.Value.Planned, kvp.Value.Completed)); + await Task.WhenAll(cacheUpdateTasks); + _logger.LogInfo("Background cache work totals update completed for {AreaCount} areas.", workDelta.Count); + + // Update the details of the individual work items in the cache + await _cache.ManageWorkItemDetails(affectedItems); + _logger.LogInfo("Background cache work item details update completed for {ItemCount} items.", affectedItems.Count); + + // Add SignalR notification logic here if needed + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred during background cache update/notification."); + } + } + #endregion } } diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs index bafa582..2db004d 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs @@ -19,5 +19,9 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task>> ManageAllocationAsync(List projectAllocationDots, Guid tenantId, Employee loggedInEmployee); Task> GetProjectsByEmployeeAsync(Guid employeeId, Guid tenantId, Employee loggedInEmployee); Task>> AssigneProjectsToEmployeeAsync(List projectAllocationDtos, Guid employeeId, Guid tenantId, Employee loggedInEmployee); + Task> GetInfraDetailsAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee); + Task> GetWorkItemsAsync(Guid workAreaId, Guid tenantId, Employee loggedInEmployee); + Task>> CreateProjectTaskAsync(List workItemDtos, Guid tenantId, Employee loggedInEmployee); + } } From 237b178107ea3b574d0e0766510adbee4db85c4b Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 16 Jul 2025 17:52:25 +0530 Subject: [PATCH 080/124] Replace lazy loading with eager loading --- Marco.Pms.Services/Helpers/EmployeeHelper.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Marco.Pms.Services/Helpers/EmployeeHelper.cs b/Marco.Pms.Services/Helpers/EmployeeHelper.cs index 03184e5..926e7fd 100644 --- a/Marco.Pms.Services/Helpers/EmployeeHelper.cs +++ b/Marco.Pms.Services/Helpers/EmployeeHelper.cs @@ -76,14 +76,13 @@ namespace MarcoBMS.Services.Helpers try { List result = new List(); - if (ProjectId != null) + if (ProjectId.HasValue) { - result = await (from pa in _context.ProjectAllocations.Where(c => c.ProjectId == ProjectId) - join em in _context.Employees.Where(c => c.TenantId == TenentId && c.IsActive == true).Include(fp => fp.JobRole) // Include Feature - on pa.EmployeeId equals em.Id - select em.ToEmployeeVMFromEmployee() - ) + result = await _context.ProjectAllocations + .Include(pa => pa.Employee) + .Where(c => c.ProjectId == ProjectId.Value && c.IsActive && c.Employee != null) + .Select(pa => pa.Employee!.ToEmployeeVMFromEmployee()) .ToListAsync(); } From e4246df315f5f36a5e44abee3504028cc21a1a8c Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 16 Jul 2025 12:39:16 +0530 Subject: [PATCH 081/124] Changed the business logic of teams and tasks API in DashboardController to accept project ID and provide data according to project ID or project IDs assigned to logged in user --- .../Controllers/DashboardController.cs | 210 +++++++++++++++--- 1 file changed, 176 insertions(+), 34 deletions(-) diff --git a/Marco.Pms.Services/Controllers/DashboardController.cs b/Marco.Pms.Services/Controllers/DashboardController.cs index 8ed0ba0..432459c 100644 --- a/Marco.Pms.Services/Controllers/DashboardController.cs +++ b/Marco.Pms.Services/Controllers/DashboardController.cs @@ -21,12 +21,15 @@ namespace Marco.Pms.Services.Controllers { private readonly ApplicationDbContext _context; private readonly UserHelper _userHelper; + private readonly ProjectsHelper _projectsHelper; private readonly ILoggingService _logger; private readonly PermissionServices _permissionServices; - public DashboardController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, PermissionServices permissionServices) + public static readonly Guid ActiveId = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731"); + public DashboardController(ApplicationDbContext context, UserHelper userHelper, ProjectsHelper projectsHelper, ILoggingService logger, PermissionServices permissionServices) { _context = context; _userHelper = userHelper; + _projectsHelper = projectsHelper; _logger = logger; _permissionServices = permissionServices; } @@ -162,46 +165,185 @@ namespace Marco.Pms.Services.Controllers return Ok(ApiResponse.SuccessResponse(projectDashboardVM, "Success", 200)); } + /// + /// Retrieves a dashboard summary of total employees and today's attendance. + /// If a projectId is provided, it returns totals for that project; otherwise, for all accessible active projects. + /// + /// Optional. The ID of a specific project to get totals for. [HttpGet("teams")] - public async Task GetTotalEmployees() + public async Task GetTotalEmployees([FromQuery] Guid? projectId) { - var tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var date = DateTime.UtcNow.Date; - - var Employees = await _context.Employees.Where(e => e.TenantId == tenantId && e.IsActive == true).Select(e => e.Id).ToListAsync(); - - var checkedInEmployee = await _context.Attendes.Where(e => e.InTime != null ? e.InTime.Value.Date == date : false).Select(e => e.EmployeeID).ToListAsync(); - - TeamDashboardVM teamDashboardVM = new TeamDashboardVM + try { - TotalEmployees = Employees.Count(), - InToday = checkedInEmployee.Distinct().Count() - }; - _logger.LogInfo("Today's total checked in employees fetched by employee {EmployeeId}", LoggedInEmployee.Id); - return Ok(ApiResponse.SuccessResponse(teamDashboardVM, "Success", 200)); - } + var tenantId = _userHelper.GetTenantId(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - [HttpGet("tasks")] - public async Task GetTotalTasks() - { - var tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var Tasks = await _context.WorkItems.Where(t => t.TenantId == tenantId).Select(t => new { PlannedWork = t.PlannedWork, CompletedWork = t.CompletedWork }).ToListAsync(); - TasksDashboardVM tasksDashboardVM = new TasksDashboardVM - { - TotalTasks = 0, - CompletedTasks = 0 - }; - foreach (var task in Tasks) - { - tasksDashboardVM.TotalTasks += task.PlannedWork; - tasksDashboardVM.CompletedTasks += task.CompletedWork; + _logger.LogInfo("GetTotalEmployees called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty); + + // --- Step 1: Get the list of projects the user can access --- + // This query is more efficient as it only selects the IDs needed. + var projects = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); + var accessibleActiveProjectIds = projects + .Where(p => p.ProjectStatusId == ActiveId) + .Select(p => p.Id) + .ToList(); + if (!accessibleActiveProjectIds.Any()) + { + _logger.LogInfo("User {UserId} has no accessible active projects.", loggedInEmployee.Id); + return Ok(ApiResponse.SuccessResponse(new TeamDashboardVM(), "No accessible active projects found.", 200)); + } + + // --- Step 2: Build the list of project IDs to query against --- + List finalProjectIds; + + if (projectId.HasValue) + { + // Security Check: Ensure the requested project is in the user's accessible list. + if (!accessibleActiveProjectIds.Contains(projectId.Value)) + { + _logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId} (not active or not accessible).", loggedInEmployee.Id, projectId.Value); + return StatusCode(403, ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to view this project, or it is not active.", 403)); + } + finalProjectIds = new List { projectId.Value }; + } + else + { + finalProjectIds = accessibleActiveProjectIds; + } + + // --- Step 3: Run efficient aggregation queries SEQUENTIALLY --- + // Since we only have one DbContext instance, we await each query one by one. + + // Query 1: Count total distinct employees allocated to the final project list + int totalEmployees = await _context.ProjectAllocations + .Where(pa => pa.TenantId == tenantId && + finalProjectIds.Contains(pa.ProjectId) && + pa.IsActive) + .Select(pa => pa.EmployeeId) + .Distinct() + .CountAsync(); + + // Query 2: Count total distinct employees who checked in today + // Use an efficient date range check + var today = DateTime.UtcNow.Date; + var tomorrow = today.AddDays(1); + + int inTodays = await _context.Attendes + .Where(a => a.InTime >= today && a.InTime < tomorrow && + finalProjectIds.Contains(a.ProjectID)) + .Select(a => a.EmployeeID) + .Distinct() + .CountAsync(); + + // --- Step 4: Assemble the response --- + var teamDashboardVM = new TeamDashboardVM + { + TotalEmployees = totalEmployees, + InToday = inTodays + }; + + _logger.LogInfo("Successfully fetched team dashboard for user {UserId}. Total: {TotalEmployees}, InToday: {InToday}", + loggedInEmployee.Id, teamDashboardVM.TotalEmployees, teamDashboardVM.InToday); + + return Ok(ApiResponse.SuccessResponse(teamDashboardVM, "Dashboard data retrieved successfully.", 200)); + } + catch (Exception ex) + { + _logger.LogError("An unexpected error occurred in GetTotalEmployees for projectId {ProjectId} \n {Error}", projectId ?? Guid.Empty, ex.Message); + return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred.", null, 500)); } - _logger.LogInfo("Total targeted tasks and total completed tasks fetched by employee {EmployeeId}", LoggedInEmployee.Id); - return Ok(ApiResponse.SuccessResponse(tasksDashboardVM, "Success", 200)); } + /// + /// Retrieves a dashboard summary of total planned and completed tasks. + /// If a projectId is provided, it returns totals for that project; otherwise, for all accessible projects. + /// + /// Optional. The ID of a specific project to get totals for. + /// An ApiResponse containing the task dashboard summary. + [HttpGet("tasks")] // Example route + public async Task GetTotalTasks1([FromQuery] Guid? projectId) // Changed to FromQuery as it's optional + { + try + { + var tenantId = _userHelper.GetTenantId(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + _logger.LogInfo("GetTotalTasks called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty); + + // --- Step 1: Build the base IQueryable for WorkItems --- + // This query is NOT executed yet. We will add more filters to it. + var baseWorkItemQuery = _context.WorkItems.Where(t => t.TenantId == tenantId); + + // --- Step 2: Apply Filters based on the request (Project or All Accessible) --- + if (projectId.HasValue) + { + // --- Logic for a SINGLE Project --- + + // 2a. Security Check: Verify permission for the specific project. + var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value.ToString()); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, projectId.Value); + return StatusCode(403, ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to view this project.", 403)); + } + + // 2b. Add project-specific filter to the base query. + // This is more efficient than fetching workAreaIds separately. + baseWorkItemQuery = baseWorkItemQuery + .Where(wi => wi.WorkArea != null && + wi.WorkArea.Floor != null && + wi.WorkArea.Floor.Building != null && + wi.WorkArea.Floor.Building.ProjectId == projectId.Value); + } + else + { + // --- Logic for ALL Accessible Projects --- + + // 2c. Get a list of all projects the user is allowed to see. + var accessibleProject = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); + var accessibleProjectIds = accessibleProject.Select(p => p.Id).ToList(); + if (!accessibleProjectIds.Any()) + { + _logger.LogInfo("User {UserId} has no accessible projects.", loggedInEmployee.Id); + // Return a zeroed-out dashboard if the user has no projects. + return Ok(ApiResponse.SuccessResponse(new TasksDashboardVM(), "No accessible projects found.", 200)); + } + + // 2d. Add a filter to include all work items from all accessible projects. + baseWorkItemQuery = baseWorkItemQuery + .Where(wi => wi.WorkArea != null && + wi.WorkArea.Floor != null && + wi.WorkArea.Floor.Building != null && + accessibleProjectIds.Contains(wi.WorkArea.Floor.Building.ProjectId)); + } + + // --- Step 3: Execute the Aggregation Query ON THE DATABASE SERVER --- + // This is the most powerful optimization. The database does all the summing. + // EF Core translates this into a single, efficient SQL query like: + // SELECT SUM(PlannedWork), SUM(CompletedWork) FROM WorkItems WHERE ... + var tasksDashboardVM = await baseWorkItemQuery + .GroupBy(x => 1) // Group by a constant to aggregate all rows into one result. + .Select(g => new TasksDashboardVM + { + TotalTasks = g.Sum(wi => wi.PlannedWork), + CompletedTasks = g.Sum(wi => wi.CompletedWork) + }) + .FirstOrDefaultAsync(); // Use FirstOrDefaultAsync as GroupBy might return no rows. + + // If the query returned no work items, the result will be null. Default to a zeroed object. + tasksDashboardVM ??= new TasksDashboardVM(); + + _logger.LogInfo("Successfully fetched task dashboard for user {UserId}. Total: {TotalTasks}, Completed: {CompletedTasks}", + loggedInEmployee.Id, tasksDashboardVM.TotalTasks, tasksDashboardVM.CompletedTasks); + + return Ok(ApiResponse.SuccessResponse(tasksDashboardVM, "Dashboard data retrieved successfully.", 200)); + } + catch (Exception ex) + { + _logger.LogError("An unexpected error occurred in GetTotalTasks for projectId {ProjectId} \n {Error}", projectId ?? Guid.Empty, ex.Message); + return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred.", null, 500)); + } + } [HttpGet("pending-attendance")] public async Task GetPendingAttendance() { From bbd20548677a9af4183be24f867fa66476562a8c Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 16 Jul 2025 14:49:34 +0530 Subject: [PATCH 082/124] only checking if the user have permission of project or not only --- Marco.Pms.Services/Controllers/DashboardController.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Marco.Pms.Services/Controllers/DashboardController.cs b/Marco.Pms.Services/Controllers/DashboardController.cs index 432459c..3829cdc 100644 --- a/Marco.Pms.Services/Controllers/DashboardController.cs +++ b/Marco.Pms.Services/Controllers/DashboardController.cs @@ -199,7 +199,8 @@ namespace Marco.Pms.Services.Controllers if (projectId.HasValue) { // Security Check: Ensure the requested project is in the user's accessible list. - if (!accessibleActiveProjectIds.Contains(projectId.Value)) + var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value.ToString()); + if (!hasPermission) { _logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId} (not active or not accessible).", loggedInEmployee.Id, projectId.Value); return StatusCode(403, ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to view this project, or it is not active.", 403)); From 3f7925aa72e06174b0beefa4b914c5cf221bb9c6 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 16 Jul 2025 18:15:43 +0530 Subject: [PATCH 083/124] Optimized the Manage infra API in Project Controller --- Marco.Pms.CacheHelper/ProjectCache.cs | 7 + .../{BuildingDot.cs => BuildingDto.cs} | 2 +- .../Projects/{FloorDot.cs => FloorDto.cs} | 2 +- Marco.Pms.Model/Dtos/Projects/InfraDot.cs | 9 - Marco.Pms.Model/Dtos/Projects/InfraDto.cs | 9 + .../{WorkAreaDot.cs => WorkAreaDto.cs} | 2 +- Marco.Pms.Model/Mapper/InfraMapper.cs | 6 +- Marco.Pms.Model/Utilities/ServiceResponse.cs | 8 + .../Controllers/ProjectController.cs | 154 +---- .../Helpers/CacheUpdateHelper.cs | 12 + .../MappingProfiles/MappingProfile.cs | 3 + Marco.Pms.Services/Service/ProjectServices.cs | 612 ++++++++++++------ .../ServiceInterfaces/IProjectServices.cs | 1 + 13 files changed, 488 insertions(+), 339 deletions(-) rename Marco.Pms.Model/Dtos/Projects/{BuildingDot.cs => BuildingDto.cs} (92%) rename Marco.Pms.Model/Dtos/Projects/{FloorDot.cs => FloorDto.cs} (92%) delete mode 100644 Marco.Pms.Model/Dtos/Projects/InfraDot.cs create mode 100644 Marco.Pms.Model/Dtos/Projects/InfraDto.cs rename Marco.Pms.Model/Dtos/Projects/{WorkAreaDot.cs => WorkAreaDto.cs} (91%) create mode 100644 Marco.Pms.Model/Utilities/ServiceResponse.cs diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index 833e1a0..9417724 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -95,6 +95,13 @@ namespace Marco.Pms.CacheHelper var result = await _projetCollection.DeleteOneAsync(filter); return result.DeletedCount > 0; } + public async Task RemoveProjectsFromCacheAsync(List projectIds) + { + var stringIds = projectIds.Select(id => id.ToString()).ToList(); + var filter = Builders.Filter.In(p => p.Id, stringIds); + var result = await _projetCollection.DeleteManyAsync(filter); + return result.DeletedCount > 0; + } // ------------------------------------------------------- Project InfraStructure ------------------------------------------------------- diff --git a/Marco.Pms.Model/Dtos/Projects/BuildingDot.cs b/Marco.Pms.Model/Dtos/Projects/BuildingDto.cs similarity index 92% rename from Marco.Pms.Model/Dtos/Projects/BuildingDot.cs rename to Marco.Pms.Model/Dtos/Projects/BuildingDto.cs index a5b160b..e6a7b89 100644 --- a/Marco.Pms.Model/Dtos/Projects/BuildingDot.cs +++ b/Marco.Pms.Model/Dtos/Projects/BuildingDto.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; namespace Marco.Pms.Model.Dtos.Project { - public class BuildingDot + public class BuildingDto { [Key] public Guid? Id { get; set; } diff --git a/Marco.Pms.Model/Dtos/Projects/FloorDot.cs b/Marco.Pms.Model/Dtos/Projects/FloorDto.cs similarity index 92% rename from Marco.Pms.Model/Dtos/Projects/FloorDot.cs rename to Marco.Pms.Model/Dtos/Projects/FloorDto.cs index a3d1c86..3dbe06f 100644 --- a/Marco.Pms.Model/Dtos/Projects/FloorDot.cs +++ b/Marco.Pms.Model/Dtos/Projects/FloorDto.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; namespace Marco.Pms.Model.Dtos.Project { - public class FloorDot + public class FloorDto { public Guid? Id { get; set; } diff --git a/Marco.Pms.Model/Dtos/Projects/InfraDot.cs b/Marco.Pms.Model/Dtos/Projects/InfraDot.cs deleted file mode 100644 index 7c16c09..0000000 --- a/Marco.Pms.Model/Dtos/Projects/InfraDot.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Marco.Pms.Model.Dtos.Project -{ - public class InfraDot - { - public BuildingDot? Building { get; set; } - public FloorDot? Floor { get; set; } - public WorkAreaDot? WorkArea { get; set; } - } -} diff --git a/Marco.Pms.Model/Dtos/Projects/InfraDto.cs b/Marco.Pms.Model/Dtos/Projects/InfraDto.cs new file mode 100644 index 0000000..09d1462 --- /dev/null +++ b/Marco.Pms.Model/Dtos/Projects/InfraDto.cs @@ -0,0 +1,9 @@ +namespace Marco.Pms.Model.Dtos.Project +{ + public class InfraDto + { + public BuildingDto? Building { get; set; } + public FloorDto? Floor { get; set; } + public WorkAreaDto? WorkArea { get; set; } + } +} diff --git a/Marco.Pms.Model/Dtos/Projects/WorkAreaDot.cs b/Marco.Pms.Model/Dtos/Projects/WorkAreaDto.cs similarity index 91% rename from Marco.Pms.Model/Dtos/Projects/WorkAreaDot.cs rename to Marco.Pms.Model/Dtos/Projects/WorkAreaDto.cs index 604ee3e..ffc80c4 100644 --- a/Marco.Pms.Model/Dtos/Projects/WorkAreaDot.cs +++ b/Marco.Pms.Model/Dtos/Projects/WorkAreaDto.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; namespace Marco.Pms.Model.Dtos.Project { - public class WorkAreaDot + public class WorkAreaDto { [Key] public Guid? Id { get; set; } diff --git a/Marco.Pms.Model/Mapper/InfraMapper.cs b/Marco.Pms.Model/Mapper/InfraMapper.cs index 89097d1..5364494 100644 --- a/Marco.Pms.Model/Mapper/InfraMapper.cs +++ b/Marco.Pms.Model/Mapper/InfraMapper.cs @@ -5,7 +5,7 @@ namespace Marco.Pms.Model.Mapper { public static class BuildingMapper { - public static Building ToBuildingFromBuildingDto(this BuildingDot model, Guid tenantId) + public static Building ToBuildingFromBuildingDto(this BuildingDto model, Guid tenantId) { return new Building { @@ -20,7 +20,7 @@ namespace Marco.Pms.Model.Mapper public static class FloorMapper { - public static Floor ToFloorFromFloorDto(this FloorDot model, Guid tenantId) + public static Floor ToFloorFromFloorDto(this FloorDto model, Guid tenantId) { return new Floor { @@ -34,7 +34,7 @@ namespace Marco.Pms.Model.Mapper public static class WorAreaMapper { - public static WorkArea ToWorkAreaFromWorkAreaDto(this WorkAreaDot model, Guid tenantId) + public static WorkArea ToWorkAreaFromWorkAreaDto(this WorkAreaDto model, Guid tenantId) { return new WorkArea { diff --git a/Marco.Pms.Model/Utilities/ServiceResponse.cs b/Marco.Pms.Model/Utilities/ServiceResponse.cs new file mode 100644 index 0000000..a76c45c --- /dev/null +++ b/Marco.Pms.Model/Utilities/ServiceResponse.cs @@ -0,0 +1,8 @@ +namespace Marco.Pms.Model.Utilities +{ + public class ServiceResponse + { + public object? Notification { get; set; } + public ApiResponse Response { get; set; } = ApiResponse.ErrorResponse(""); + } +} diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index a10fc66..71ef1a5 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -1,10 +1,8 @@ using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Employees; -using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; -using Marco.Pms.Model.ViewModels.Projects; using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Service; using Marco.Pms.Services.Service.ServiceInterfaces; @@ -359,6 +357,30 @@ namespace MarcoBMS.Services.Controllers #region =================================================================== Project Infrastructre Manage APIs =================================================================== + [HttpPost("manage-infra")] + public async Task ManageProjectInfra(List infraDtos) + { + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) + { + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); + } + + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var serviceResponse = await _projectServices.ManageProjectInfraAsync(infraDtos, tenantId, loggedInEmployee); + var response = serviceResponse.Response; + var notification = serviceResponse.Notification; + if (notification != null) + { + await _signalR.SendNotificationAsync(notification); + } + return StatusCode(response.StatusCode, response); + + } + [HttpPost("task")] public async Task CreateProjectTask([FromBody] List workItemDtos) { @@ -439,134 +461,6 @@ namespace MarcoBMS.Services.Controllers return Ok(ApiResponse.SuccessResponse(new { }, "Task deleted successfully", 200)); } - [HttpPost("manage-infra")] - public async Task ManageProjectInfra(List infraDots) - { - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - var responseData = new InfraVM { }; - string responseMessage = ""; - string message = ""; - List projectIds = new List(); - if (infraDots != null) - { - foreach (var item in infraDots) - { - if (item.Building != null) - { - - Building building = item.Building.ToBuildingFromBuildingDto(tenantId); - building.TenantId = tenantId; - - if (item.Building.Id == null) - { - //create - _context.Buildings.Add(building); - await _context.SaveChangesAsync(); - responseData.building = building; - responseMessage = "Buliding Added Successfully"; - message = "Building Added"; - await _cache.AddBuildngInfra(building.ProjectId, building); - } - else - { - //update - _context.Buildings.Update(building); - await _context.SaveChangesAsync(); - responseData.building = building; - responseMessage = "Buliding Updated Successfully"; - message = "Building Updated"; - await _cache.UpdateBuildngInfra(building.ProjectId, building); - } - projectIds.Add(building.ProjectId); - } - if (item.Floor != null) - { - Floor floor = item.Floor.ToFloorFromFloorDto(tenantId); - floor.TenantId = tenantId; - bool isCreated = false; - - if (item.Floor.Id == null) - { - //create - _context.Floor.Add(floor); - await _context.SaveChangesAsync(); - responseData.floor = floor; - responseMessage = "Floor Added Successfully"; - message = "Floor Added"; - isCreated = true; - } - else - { - //update - _context.Floor.Update(floor); - await _context.SaveChangesAsync(); - responseData.floor = floor; - responseMessage = "Floor Updated Successfully"; - message = "Floor Updated"; - } - Building? building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == floor.BuildingId); - var projectId = building?.ProjectId ?? Guid.Empty; - projectIds.Add(projectId); - message = $"{message} in Building: {building?.Name}"; - if (isCreated) - { - await _cache.AddBuildngInfra(projectId, floor: floor); - } - else - { - await _cache.UpdateBuildngInfra(projectId, floor: floor); - } - } - if (item.WorkArea != null) - { - WorkArea workArea = item.WorkArea.ToWorkAreaFromWorkAreaDto(tenantId); - workArea.TenantId = tenantId; - bool isCreated = false; - - if (item.WorkArea.Id == null) - { - //create - _context.WorkAreas.Add(workArea); - await _context.SaveChangesAsync(); - responseData.workArea = workArea; - responseMessage = "Work Area Added Successfully"; - message = "Work Area Added"; - isCreated = true; - } - else - { - //update - _context.WorkAreas.Update(workArea); - await _context.SaveChangesAsync(); - responseData.workArea = workArea; - responseMessage = "Work Area Updated Successfully"; - message = "Work Area Updated"; - } - Floor? floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == workArea.FloorId); - var projectId = floor?.Building?.ProjectId ?? Guid.Empty; - projectIds.Add(projectId); - message = $"{message} in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}"; - if (isCreated) - { - await _cache.AddBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); - } - else - { - await _cache.UpdateBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); - } - } - } - message = $"{message} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}"; - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = message }; - - await _signalR.SendNotificationAsync(notification); - return Ok(ApiResponse.SuccessResponse(responseData, responseMessage, 200)); - } - return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Infra Details are not valid.", 400)); - - } - #endregion } diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 9a01b83..b0b1e06 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -478,6 +478,18 @@ namespace Marco.Pms.Services.Helpers } } + public async Task RemoveProjectsAsync(List projectIds) + { + try + { + var response = await _projectCache.RemoveProjectsFromCacheAsync(projectIds); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occured while deleting project list from to Cache"); + + } + } // ------------------------------------ Project Infrastructure Cache --------------------------------------- diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index 50d2ea9..bf3777c 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -51,6 +51,9 @@ namespace Marco.Pms.Services.MappingProfiles CreateMap(); CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); CreateMap() .ForMember( dest => dest.Description, diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs index 6d811fc..32e1285 100644 --- a/Marco.Pms.Services/Service/ProjectServices.cs +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -1033,83 +1033,360 @@ namespace Marco.Pms.Services.Service #region =================================================================== Project Infrastructre Manage APIs =================================================================== - public async Task> CreateProjectTask1(List workItemDtos, Guid tenantId, Employee loggedInEmployee) + public async Task> ManageProjectInfra(List infraDots, Guid tenantId, Employee loggedInEmployee) { - _logger.LogInfo("CreateProjectTask called with {Count} items", workItemDtos?.Count ?? 0); - - // Validate request - if (workItemDtos == null || !workItemDtos.Any()) - { - _logger.LogWarning("No work items provided in the request."); - return ApiResponse.ErrorResponse("Invalid details.", "Work Item details are not valid.", 400); - } - - var workItemsToCreate = new List(); - var workItemsToUpdate = new List(); - var responseList = new List(); + var responseData = new InfraVM { }; + string responseMessage = ""; string message = ""; - List workAreaIds = new List(); - var workItemIds = workItemDtos.Where(wi => wi.Id != null && wi.Id != Guid.Empty).Select(wi => wi.Id).ToList(); - var workItems = await _context.WorkItems.AsNoTracking().Where(wi => workItemIds.Contains(wi.Id)).ToListAsync(); - - foreach (var itemDto in workItemDtos) + List projectIds = new List(); + if (infraDots != null) { - var workItem = _mapper.Map(itemDto); - workItem.TenantId = tenantId; - var workArea = await _context.WorkAreas.Include(a => a.Floor).FirstOrDefaultAsync(a => a.Id == workItem.WorkAreaId) ?? new WorkArea(); - - Building building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == (workArea.Floor != null ? workArea.Floor.BuildingId : Guid.Empty)) ?? new Building(); - - if (itemDto.Id != null && itemDto.Id != Guid.Empty) + foreach (var item in infraDots) { - // Update existing - workItemsToUpdate.Add(workItem); - message = $"Task Updated in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; - var existingWorkItem = workItems.FirstOrDefault(wi => wi.Id == workItem.Id); - if (existingWorkItem != null) + if (item.Building != null) { - double plannedWork = workItem.PlannedWork - existingWorkItem.PlannedWork; - double completedWork = workItem.CompletedWork - existingWorkItem.CompletedWork; - await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, plannedWork, completedWork); + + Building building = _mapper.Map(item.Building); + building.TenantId = tenantId; + + if (item.Building.Id == null) + { + //create + _context.Buildings.Add(building); + await _context.SaveChangesAsync(); + responseData.building = building; + responseMessage = "Buliding Added Successfully"; + message = "Building Added"; + await _cache.AddBuildngInfra(building.ProjectId, building); + } + else + { + //update + _context.Buildings.Update(building); + await _context.SaveChangesAsync(); + responseData.building = building; + responseMessage = "Buliding Updated Successfully"; + message = "Building Updated"; + await _cache.UpdateBuildngInfra(building.ProjectId, building); + } + projectIds.Add(building.ProjectId); + } + if (item.Floor != null) + { + Floor floor = _mapper.Map(item.Floor); + floor.TenantId = tenantId; + bool isCreated = false; + + if (item.Floor.Id == null) + { + //create + _context.Floor.Add(floor); + await _context.SaveChangesAsync(); + responseData.floor = floor; + responseMessage = "Floor Added Successfully"; + message = "Floor Added"; + isCreated = true; + } + else + { + //update + _context.Floor.Update(floor); + await _context.SaveChangesAsync(); + responseData.floor = floor; + responseMessage = "Floor Updated Successfully"; + message = "Floor Updated"; + } + Building? building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == floor.BuildingId); + var projectId = building?.ProjectId ?? Guid.Empty; + projectIds.Add(projectId); + message = $"{message} in Building: {building?.Name}"; + if (isCreated) + { + await _cache.AddBuildngInfra(projectId, floor: floor); + } + else + { + await _cache.UpdateBuildngInfra(projectId, floor: floor); + } + } + if (item.WorkArea != null) + { + WorkArea workArea = _mapper.Map(item.WorkArea); + workArea.TenantId = tenantId; + bool isCreated = false; + + if (item.WorkArea.Id == null) + { + //create + _context.WorkAreas.Add(workArea); + await _context.SaveChangesAsync(); + responseData.workArea = workArea; + responseMessage = "Work Area Added Successfully"; + message = "Work Area Added"; + isCreated = true; + } + else + { + //update + _context.WorkAreas.Update(workArea); + await _context.SaveChangesAsync(); + responseData.workArea = workArea; + responseMessage = "Work Area Updated Successfully"; + message = "Work Area Updated"; + } + Floor? floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == workArea.FloorId); + var projectId = floor?.Building?.ProjectId ?? Guid.Empty; + projectIds.Add(projectId); + message = $"{message} in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}"; + if (isCreated) + { + await _cache.AddBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); + } + else + { + await _cache.UpdateBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); + } } } - else + message = $"{message} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = message }; + + return ApiResponse.SuccessResponse(responseData, responseMessage, 200); + } + return ApiResponse.ErrorResponse("Invalid details.", "Infra Details are not valid.", 400); + + } + + public async Task ManageProjectInfraAsync(List infraDtos, Guid tenantId, Employee loggedInEmployee) + { + // 1. Guard Clause: Handle null or empty input gracefully. + if (infraDtos == null || !infraDtos.Any()) + { + return new ServiceResponse { - // Create new - workItem.Id = Guid.NewGuid(); - workItemsToCreate.Add(workItem); - message = $"Task Added in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; - await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, workItem.PlannedWork, workItem.CompletedWork); + Response = ApiResponse.ErrorResponse("Invalid details.", "No infrastructure details were provided.", 400) + }; + } + + var responseData = new InfraVM(); + var messages = new List(); + var projectIds = new HashSet(); // Use HashSet for automatic duplicate handling. + var cacheUpdateTasks = new List(); + + // --- Pre-fetch parent entities to avoid N+1 query problem --- + // 2. Gather all parent IDs needed for validation and context. + var requiredBuildingIds = infraDtos + .Where(i => i.Floor?.BuildingId != null) + .Select(i => i.Floor!.BuildingId) + .Distinct() + .ToList(); + + var requiredFloorIds = infraDtos + .Where(i => i.WorkArea?.FloorId != null) + .Select(i => i.WorkArea!.FloorId) + .Distinct() + .ToList(); + + // 3. Fetch all required parent entities in single batch queries. + var buildingsDict = await _context.Buildings + .Where(b => requiredBuildingIds.Contains(b.Id)) + .ToDictionaryAsync(b => b.Id); + + var floorsDict = await _context.Floor + .Include(f => f.Building) // Eagerly load Building for later use + .Where(f => requiredFloorIds.Contains(f.Id)) + .ToDictionaryAsync(f => f.Id); + // --- End Pre-fetching --- + + // 4. Process all entities and add them to the context's change tracker. + foreach (var item in infraDtos) + { + if (item.Building != null) + { + ProcessBuilding(item.Building, tenantId, responseData, messages, projectIds, cacheUpdateTasks); + } + if (item.Floor != null) + { + ProcessFloor(item.Floor, tenantId, responseData, messages, projectIds, cacheUpdateTasks, buildingsDict); + } + if (item.WorkArea != null) + { + ProcessWorkArea(item.WorkArea, tenantId, responseData, messages, projectIds, cacheUpdateTasks, floorsDict); + } + } + + // 5. Save all changes to the database in a single transaction. + var changedRecordCount = await _context.SaveChangesAsync(); + + // If no changes were actually made, we can exit early. + if (changedRecordCount == 0) + { + return new ServiceResponse + { + Response = ApiResponse.SuccessResponse(responseData, "No changes detected in the provided infrastructure details.", 200) + }; + } + + // 6. Execute all cache updates concurrently after the DB save is successful. + await Task.WhenAll(cacheUpdateTasks); + + // 7. Consolidate messages and create notification payload. + string finalResponseMessage = messages.LastOrDefault() ?? "Infrastructure managed successfully."; + string logMessage = $"{string.Join(", ", messages)} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds.ToList(), Message = logMessage }; + + // TODO: Dispatch the 'notification' object to your notification service. + + return new ServiceResponse + { + Notification = notification, + Response = ApiResponse.SuccessResponse(responseData, finalResponseMessage, 200) + }; + } + + /// + /// Manages a batch of infrastructure changes (creates/updates for Buildings, Floors, and WorkAreas). + /// This method is optimized to perform all database operations in a single, atomic transaction. + /// + public async Task> ManageProjectInfraAsync1(List infraDtos, Guid tenantId, Employee loggedInEmployee) + { + // --- Step 1: Input Validation --- + if (infraDtos == null || !infraDtos.Any()) + { + _logger.LogWarning("ManageProjectInfraAsync called with null or empty DTO list."); + return ApiResponse.ErrorResponse("Invalid details.", "Infrastructure data cannot be empty.", 400); + } + + _logger.LogInfo("Begin ManageProjectInfraAsync for {DtoCount} items, TenantId: {TenantId}, User: {UserId}", infraDtos.Count, tenantId, loggedInEmployee.Id); + + // --- Step 2: Categorize DTOs by Type and Action --- + var buildingsToCreateDto = infraDtos.Where(i => i.Building != null && i.Building.Id == null).Select(i => i.Building!).ToList(); + var buildingsToUpdateDto = infraDtos.Where(i => i.Building != null && i.Building.Id != null).Select(i => i.Building!).ToList(); + var floorsToCreateDto = infraDtos.Where(i => i.Floor != null && i.Floor.Id == null).Select(i => i.Floor!).ToList(); + var floorsToUpdateDto = infraDtos.Where(i => i.Floor != null && i.Floor.Id != null).Select(i => i.Floor!).ToList(); + var workAreasToCreateDto = infraDtos.Where(i => i.WorkArea != null && i.WorkArea.Id == null).Select(i => i.WorkArea!).ToList(); + var workAreasToUpdateDto = infraDtos.Where(i => i.WorkArea != null && i.WorkArea.Id != null).Select(i => i.WorkArea!).ToList(); + + _logger.LogDebug("Categorized DTOs..."); + + try + { + // --- Step 3: Fetch all required existing data in bulk --- + + // Fetch existing entities to be updated + var buildingIdsToUpdate = buildingsToUpdateDto.Select(d => d.Id!.Value).ToList(); + var existingBuildings = await _context.Buildings.Where(b => buildingIdsToUpdate.Contains(b.Id) && b.TenantId == tenantId).ToDictionaryAsync(b => b.Id); + + var floorIdsToUpdate = floorsToUpdateDto.Select(d => d.Id!.Value).ToList(); + var existingFloors = await _context.Floor.Include(f => f.Building).Where(f => floorIdsToUpdate.Contains(f.Id) && f.TenantId == tenantId).ToDictionaryAsync(f => f.Id); + + var workAreaIdsToUpdate = workAreasToUpdateDto.Select(d => d.Id!.Value).ToList(); + var existingWorkAreas = await _context.WorkAreas.Include(wa => wa.Floor!.Building).Where(wa => workAreaIdsToUpdate.Contains(wa.Id) && wa.TenantId == tenantId).ToDictionaryAsync(wa => wa.Id); + + // Fetch parent entities for items being created to get their ProjectIds + var buildingIdsForNewFloors = floorsToCreateDto.Select(f => f.BuildingId).ToList(); + var parentBuildingsForNewFloors = await _context.Buildings.Where(b => buildingIdsForNewFloors.Contains(b.Id)).ToDictionaryAsync(b => b.Id); + + var floorIdsForNewWorkAreas = workAreasToCreateDto.Select(wa => wa.FloorId).ToList(); + var parentFloorsForNewWorkAreas = await _context.Floor.Include(f => f.Building).Where(f => floorIdsForNewWorkAreas.Contains(f.Id)).ToDictionaryAsync(f => f.Id); + + _logger.LogInfo("Fetched existing entities and parents for new items."); + + // --- Step 4: Aggregate all affected ProjectIds for Security Check --- + var affectedProjectIds = new HashSet(); + + // From buildings being created/updated + buildingsToCreateDto.ForEach(b => affectedProjectIds.Add(b.ProjectId)); + foreach (var b in existingBuildings.Values) { affectedProjectIds.Add(b.ProjectId); } + + // From floors being created/updated + foreach (var f in floorsToCreateDto) { if (parentBuildingsForNewFloors.TryGetValue(f.BuildingId, out var b)) affectedProjectIds.Add(b.ProjectId); } + foreach (var f in existingFloors.Values) { if (f.Building != null) affectedProjectIds.Add(f.Building.ProjectId); } + + // From work areas being created/updated + foreach (var wa in workAreasToCreateDto) { if (parentFloorsForNewWorkAreas.TryGetValue(wa.FloorId, out var f) && f.Building != null) affectedProjectIds.Add(f.Building.ProjectId); } + foreach (var wa in existingWorkAreas.Values) { if (wa.Floor?.Building != null) affectedProjectIds.Add(wa.Floor.Building.ProjectId); } + + // Security Check against the complete list of affected projects + var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageProjectInfra, loggedInEmployee.Id); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} trying to manage infrastructure for projects.", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to manage infrastructure for one or more of the specified projects.", 403); } - responseList.Add(new WorkItemVM + // --- Step 5: Process all logic IN MEMORY, tracking changes --- + + // Process Buildings + var createdBuildings = new List(); + foreach (var dto in buildingsToCreateDto) { - WorkItemId = workItem.Id, - WorkItem = workItem - }); - workAreaIds.Add(workItem.WorkAreaId); + var newBuilding = _mapper.Map(dto); + newBuilding.TenantId = tenantId; + createdBuildings.Add(newBuilding); + } + foreach (var dto in buildingsToUpdateDto) { if (existingBuildings.TryGetValue(dto.Id!.Value, out var b)) _mapper.Map(dto, b); } + // Process Floors + var createdFloors = new List(); + foreach (var dto in floorsToCreateDto) + { + var newFloor = _mapper.Map(dto); + newFloor.TenantId = tenantId; + createdFloors.Add(newFloor); + } + foreach (var dto in floorsToUpdateDto) { if (existingFloors.TryGetValue(dto.Id!.Value, out var f)) _mapper.Map(dto, f); } + + // Process WorkAreas + var createdWorkAreas = new List(); + foreach (var dto in workAreasToCreateDto) + { + var newWorkArea = _mapper.Map(dto); + newWorkArea.TenantId = tenantId; + createdWorkAreas.Add(newWorkArea); + } + foreach (var dto in workAreasToUpdateDto) { if (existingWorkAreas.TryGetValue(dto.Id!.Value, out var wa)) _mapper.Map(dto, wa); } + + // --- Step 6: Save all database changes in a SINGLE TRANSACTION --- + if (createdBuildings.Any()) _context.Buildings.AddRange(createdBuildings); + if (createdFloors.Any()) _context.Floor.AddRange(createdFloors); + if (createdWorkAreas.Any()) _context.WorkAreas.AddRange(createdWorkAreas); + + if (_context.ChangeTracker.HasChanges()) + { + await _context.SaveChangesAsync(); + _logger.LogInfo("Database save successful."); + } + + // --- Step 7: Update Cache using the aggregated ProjectIds (Non-blocking) --- + var finalProjectIds = affectedProjectIds.ToList(); + if (finalProjectIds.Any()) + { + _ = Task.Run(async () => + { + try + { + _logger.LogInfo("Queuing background cache update for {ProjectCount} projects.", finalProjectIds.Count); + // Assuming your cache service has a method to handle this. + await _cache.RemoveProjectsAsync(finalProjectIds); + _logger.LogInfo("Background cache update task completed for projects: {ProjectIds}", string.Join(", ", finalProjectIds)); + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred during the background cache update task for projects: {ProjectIds}", string.Join(", ", finalProjectIds)); + } + }); + } + + // --- Step 8: Prepare and return a clear response --- + var responseVm = new { /* ... as before ... */ }; + return ApiResponse.SuccessResponse(responseVm, "Infrastructure changes processed successfully.", 200); } - // Apply DB changes - if (workItemsToCreate.Any()) + catch (Exception ex) { - _logger.LogInfo("Adding {Count} new work items", workItemsToCreate.Count); - await _context.WorkItems.AddRangeAsync(workItemsToCreate); - await _cache.ManageWorkItemDetails(workItemsToCreate); + _logger.LogError(ex, "An unexpected error occurred in ManageProjectInfraAsync."); + return ApiResponse.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500); } - - if (workItemsToUpdate.Any()) - { - _logger.LogInfo("Updating {Count} existing work items", workItemsToUpdate.Count); - _context.WorkItems.UpdateRange(workItemsToUpdate); - await _cache.ManageWorkItemDetails(workItemsToUpdate); - } - - await _context.SaveChangesAsync(); - - _logger.LogInfo("CreateProjectTask completed successfully. Created: {Created}, Updated: {Updated}", workItemsToCreate.Count, workItemsToUpdate.Count); - - return ApiResponse.SuccessResponse(responseList, message, 200); } /// @@ -1211,12 +1488,10 @@ namespace Marco.Pms.Services.Service await _context.SaveChangesAsync(); _logger.LogInfo("Successfully saved {CreatedCount} new and {UpdatedCount} updated work items.", workItemsToCreate.Count, workItemsToModify.Count); - // --- Step 5: Update Cache and SignalR AFTER successful DB save (non-blocking) --- + // --- Step 5: Update Cache and SignalR AFTER successful DB save --- var allAffectedItems = workItemsToCreate.Concat(workItemsToModify).ToList(); - _ = Task.Run(async () => - { - await UpdateCacheAndNotify(workDeltaForCache, allAffectedItems); - }); + + await UpdateCacheAndNotify(workDeltaForCache, allAffectedItems); } } catch (DbUpdateException ex) @@ -1291,133 +1566,6 @@ namespace Marco.Pms.Services.Service // return Ok(ApiResponse.SuccessResponse(new { }, "Task deleted successfully", 200)); //} - //public async Task ManageProjectInfra(List infraDots) - //{ - // var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - // var responseData = new InfraVM { }; - // string responseMessage = ""; - // string message = ""; - // List projectIds = new List(); - // if (infraDots != null) - // { - // foreach (var item in infraDots) - // { - // if (item.Building != null) - // { - - // Building building = item.Building.ToBuildingFromBuildingDto(tenantId); - // building.TenantId = tenantId; - - // if (item.Building.Id == null) - // { - // //create - // _context.Buildings.Add(building); - // await _context.SaveChangesAsync(); - // responseData.building = building; - // responseMessage = "Buliding Added Successfully"; - // message = "Building Added"; - // await _cache.AddBuildngInfra(building.ProjectId, building); - // } - // else - // { - // //update - // _context.Buildings.Update(building); - // await _context.SaveChangesAsync(); - // responseData.building = building; - // responseMessage = "Buliding Updated Successfully"; - // message = "Building Updated"; - // await _cache.UpdateBuildngInfra(building.ProjectId, building); - // } - // projectIds.Add(building.ProjectId); - // } - // if (item.Floor != null) - // { - // Floor floor = item.Floor.ToFloorFromFloorDto(tenantId); - // floor.TenantId = tenantId; - // bool isCreated = false; - - // if (item.Floor.Id == null) - // { - // //create - // _context.Floor.Add(floor); - // await _context.SaveChangesAsync(); - // responseData.floor = floor; - // responseMessage = "Floor Added Successfully"; - // message = "Floor Added"; - // isCreated = true; - // } - // else - // { - // //update - // _context.Floor.Update(floor); - // await _context.SaveChangesAsync(); - // responseData.floor = floor; - // responseMessage = "Floor Updated Successfully"; - // message = "Floor Updated"; - // } - // Building? building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == floor.BuildingId); - // var projectId = building?.ProjectId ?? Guid.Empty; - // projectIds.Add(projectId); - // message = $"{message} in Building: {building?.Name}"; - // if (isCreated) - // { - // await _cache.AddBuildngInfra(projectId, floor: floor); - // } - // else - // { - // await _cache.UpdateBuildngInfra(projectId, floor: floor); - // } - // } - // if (item.WorkArea != null) - // { - // WorkArea workArea = item.WorkArea.ToWorkAreaFromWorkAreaDto(tenantId); - // workArea.TenantId = tenantId; - // bool isCreated = false; - - // if (item.WorkArea.Id == null) - // { - // //create - // _context.WorkAreas.Add(workArea); - // await _context.SaveChangesAsync(); - // responseData.workArea = workArea; - // responseMessage = "Work Area Added Successfully"; - // message = "Work Area Added"; - // isCreated = true; - // } - // else - // { - // //update - // _context.WorkAreas.Update(workArea); - // await _context.SaveChangesAsync(); - // responseData.workArea = workArea; - // responseMessage = "Work Area Updated Successfully"; - // message = "Work Area Updated"; - // } - // Floor? floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == workArea.FloorId); - // var projectId = floor?.Building?.ProjectId ?? Guid.Empty; - // projectIds.Add(projectId); - // message = $"{message} in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}"; - // if (isCreated) - // { - // await _cache.AddBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); - // } - // else - // { - // await _cache.UpdateBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); - // } - // } - // } - // message = $"{message} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; - // var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = message }; - - // await _signalR.SendNotificationAsync(notification); - // return Ok(ApiResponse.SuccessResponse(responseData, responseMessage, 200)); - // } - // return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Infra Details are not valid.", 400)); - - //} - #endregion #region =================================================================== Helper Functions =================================================================== @@ -1663,6 +1811,82 @@ namespace Marco.Pms.Services.Service } } + private void ProcessBuilding(BuildingDto dto, Guid tenantId, InfraVM responseData, List messages, ISet projectIds, List cacheTasks) + { + Building building = _mapper.Map(dto); + building.TenantId = tenantId; + + bool isNew = dto.Id == null; + if (isNew) + { + _context.Buildings.Add(building); + messages.Add("Building Added"); + cacheTasks.Add(_cache.AddBuildngInfra(building.ProjectId, building)); + } + else + { + _context.Buildings.Update(building); + messages.Add("Building Updated"); + cacheTasks.Add(_cache.UpdateBuildngInfra(building.ProjectId, building)); + } + + responseData.building = building; + projectIds.Add(building.ProjectId); + } + + private void ProcessFloor(FloorDto dto, Guid tenantId, InfraVM responseData, List messages, ISet projectIds, List cacheTasks, IDictionary buildings) + { + Floor floor = _mapper.Map(dto); + floor.TenantId = tenantId; + + // Use the pre-fetched dictionary for parent lookup. + Building? parentBuilding = buildings.TryGetValue(dto.BuildingId, out var b) ? b : null; + + bool isNew = dto.Id == null; + if (isNew) + { + _context.Floor.Add(floor); + messages.Add($"Floor Added in Building: {parentBuilding?.Name ?? "Unknown"}"); + cacheTasks.Add(_cache.AddBuildngInfra(parentBuilding?.ProjectId ?? Guid.Empty, floor: floor)); + } + else + { + _context.Floor.Update(floor); + messages.Add($"Floor Updated in Building: {parentBuilding?.Name ?? "Unknown"}"); + cacheTasks.Add(_cache.UpdateBuildngInfra(parentBuilding?.ProjectId ?? Guid.Empty, floor: floor)); + } + + responseData.floor = floor; + if (parentBuilding != null) projectIds.Add(parentBuilding.ProjectId); + } + + private void ProcessWorkArea(WorkAreaDto dto, Guid tenantId, InfraVM responseData, List messages, ISet projectIds, List cacheTasks, IDictionary floors) + { + WorkArea workArea = _mapper.Map(dto); + workArea.TenantId = tenantId; + + // Use the pre-fetched dictionary for parent lookup. + Floor? parentFloor = floors.TryGetValue(dto.FloorId, out var f) ? f : null; + var parentBuilding = parentFloor?.Building; + + bool isNew = dto.Id == null; + if (isNew) + { + _context.WorkAreas.Add(workArea); + messages.Add($"Work Area Added in Building: {parentBuilding?.Name ?? "Unknown"}, on Floor: {parentFloor?.FloorName ?? "Unknown"}"); + cacheTasks.Add(_cache.AddBuildngInfra(parentBuilding?.ProjectId ?? Guid.Empty, workArea: workArea, buildingId: parentBuilding?.Id)); + } + else + { + _context.WorkAreas.Update(workArea); + messages.Add($"Work Area Updated in Building: {parentBuilding?.Name ?? "Unknown"}, on Floor: {parentFloor?.FloorName ?? "Unknown"}"); + cacheTasks.Add(_cache.UpdateBuildngInfra(parentBuilding?.ProjectId ?? Guid.Empty, workArea: workArea, buildingId: parentBuilding?.Id)); + } + + responseData.workArea = workArea; + if (parentBuilding != null) projectIds.Add(parentBuilding.ProjectId); + } + #endregion } } diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs index 2db004d..f1c89cc 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs @@ -21,6 +21,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task>> AssigneProjectsToEmployeeAsync(List projectAllocationDtos, Guid employeeId, Guid tenantId, Employee loggedInEmployee); Task> GetInfraDetailsAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee); Task> GetWorkItemsAsync(Guid workAreaId, Guid tenantId, Employee loggedInEmployee); + Task ManageProjectInfraAsync(List infraDtos, Guid tenantId, Employee loggedInEmployee); Task>> CreateProjectTaskAsync(List workItemDtos, Guid tenantId, Employee loggedInEmployee); } From 089ae7e9e563b41aa6fd8887ade1c33859fed4cc Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 16 Jul 2025 18:39:29 +0530 Subject: [PATCH 084/124] Optimization of WorkItem Delete API in Project Controller --- .../Controllers/ProjectController.cs | 61 +-- Marco.Pms.Services/Service/ProjectServices.cs | 391 ++++-------------- .../ServiceInterfaces/IProjectServices.cs | 1 + 3 files changed, 90 insertions(+), 363 deletions(-) diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 71ef1a5..362c2af 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -1,7 +1,6 @@ using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Employees; -using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Service; @@ -11,7 +10,6 @@ using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.CodeAnalysis; -using Microsoft.EntityFrameworkCore; using MongoDB.Driver; namespace MarcoBMS.Services.Controllers @@ -410,55 +408,24 @@ namespace MarcoBMS.Services.Controllers [HttpDelete("task/{id}")] public async Task DeleteProjectTask(Guid id) { - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - List workAreaIds = new List(); - WorkItem? task = await _context.WorkItems.AsNoTracking().Include(t => t.WorkArea).FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId); - if (task != null) + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) { - if (task.CompletedWork == 0) - { - var assignedTask = await _context.TaskAllocations.Where(t => t.WorkItemId == id).ToListAsync(); - if (assignedTask.Count == 0) - { - _context.WorkItems.Remove(task); - await _context.SaveChangesAsync(); - _logger.LogInfo("Task with ID {WorkItemId} has been successfully deleted.", task.Id); - - var floorId = task.WorkArea?.FloorId; - var floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == floorId); - - - workAreaIds.Add(task.WorkAreaId); - var projectId = floor?.Building?.ProjectId; - - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = $"Task Deleted in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}, in Area: {task.WorkArea?.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}" }; - await _signalR.SendNotificationAsync(notification); - await _cache.DeleteWorkItemByIdAsync(task.Id); - if (projectId != null) - { - await _cache.DeleteProjectByIdAsync(projectId.Value); - } - } - else - { - _logger.LogWarning("Task with ID {WorkItemId} is currently assigned and cannot be deleted.", task.Id); - return BadRequest(ApiResponse.ErrorResponse("Task is currently assigned and cannot be deleted.", "Task is currently assigned and cannot be deleted.", 400)); - } - } - else - { - double percentage = (task.CompletedWork / task.PlannedWork) * 100; - percentage = Math.Round(percentage, 2); - _logger.LogWarning("Task with ID {WorkItemId} is {CompletionPercentage}% complete and cannot be deleted", task.Id, percentage); - return BadRequest(ApiResponse.ErrorResponse(System.String.Format("Task is {0}% complete and cannot be deleted", percentage), System.String.Format("Task is {0}% complete and cannot be deleted", percentage), 400)); - - } + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - else + + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var serviceResponse = await _projectServices.DeleteProjectTaskAsync(id, tenantId, loggedInEmployee); + var response = serviceResponse.Response; + var notification = serviceResponse.Notification; + if (notification != null) { - _logger.LogWarning("Task with ID {WorkItemId} not found ID database", id); + await _signalR.SendNotificationAsync(notification); } - return Ok(ApiResponse.SuccessResponse(new { }, "Task deleted successfully", 200)); + return StatusCode(response.StatusCode, response); } #endregion diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs index 32e1285..d7ab2ac 100644 --- a/Marco.Pms.Services/Service/ProjectServices.cs +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -1033,130 +1033,6 @@ namespace Marco.Pms.Services.Service #region =================================================================== Project Infrastructre Manage APIs =================================================================== - public async Task> ManageProjectInfra(List infraDots, Guid tenantId, Employee loggedInEmployee) - { - var responseData = new InfraVM { }; - string responseMessage = ""; - string message = ""; - List projectIds = new List(); - if (infraDots != null) - { - foreach (var item in infraDots) - { - if (item.Building != null) - { - - Building building = _mapper.Map(item.Building); - building.TenantId = tenantId; - - if (item.Building.Id == null) - { - //create - _context.Buildings.Add(building); - await _context.SaveChangesAsync(); - responseData.building = building; - responseMessage = "Buliding Added Successfully"; - message = "Building Added"; - await _cache.AddBuildngInfra(building.ProjectId, building); - } - else - { - //update - _context.Buildings.Update(building); - await _context.SaveChangesAsync(); - responseData.building = building; - responseMessage = "Buliding Updated Successfully"; - message = "Building Updated"; - await _cache.UpdateBuildngInfra(building.ProjectId, building); - } - projectIds.Add(building.ProjectId); - } - if (item.Floor != null) - { - Floor floor = _mapper.Map(item.Floor); - floor.TenantId = tenantId; - bool isCreated = false; - - if (item.Floor.Id == null) - { - //create - _context.Floor.Add(floor); - await _context.SaveChangesAsync(); - responseData.floor = floor; - responseMessage = "Floor Added Successfully"; - message = "Floor Added"; - isCreated = true; - } - else - { - //update - _context.Floor.Update(floor); - await _context.SaveChangesAsync(); - responseData.floor = floor; - responseMessage = "Floor Updated Successfully"; - message = "Floor Updated"; - } - Building? building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == floor.BuildingId); - var projectId = building?.ProjectId ?? Guid.Empty; - projectIds.Add(projectId); - message = $"{message} in Building: {building?.Name}"; - if (isCreated) - { - await _cache.AddBuildngInfra(projectId, floor: floor); - } - else - { - await _cache.UpdateBuildngInfra(projectId, floor: floor); - } - } - if (item.WorkArea != null) - { - WorkArea workArea = _mapper.Map(item.WorkArea); - workArea.TenantId = tenantId; - bool isCreated = false; - - if (item.WorkArea.Id == null) - { - //create - _context.WorkAreas.Add(workArea); - await _context.SaveChangesAsync(); - responseData.workArea = workArea; - responseMessage = "Work Area Added Successfully"; - message = "Work Area Added"; - isCreated = true; - } - else - { - //update - _context.WorkAreas.Update(workArea); - await _context.SaveChangesAsync(); - responseData.workArea = workArea; - responseMessage = "Work Area Updated Successfully"; - message = "Work Area Updated"; - } - Floor? floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == workArea.FloorId); - var projectId = floor?.Building?.ProjectId ?? Guid.Empty; - projectIds.Add(projectId); - message = $"{message} in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}"; - if (isCreated) - { - await _cache.AddBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); - } - else - { - await _cache.UpdateBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); - } - } - } - message = $"{message} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; - var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = message }; - - return ApiResponse.SuccessResponse(responseData, responseMessage, 200); - } - return ApiResponse.ErrorResponse("Invalid details.", "Infra Details are not valid.", 400); - - } - public async Task ManageProjectInfraAsync(List infraDtos, Guid tenantId, Employee loggedInEmployee) { // 1. Guard Clause: Handle null or empty input gracefully. @@ -1244,151 +1120,6 @@ namespace Marco.Pms.Services.Service }; } - /// - /// Manages a batch of infrastructure changes (creates/updates for Buildings, Floors, and WorkAreas). - /// This method is optimized to perform all database operations in a single, atomic transaction. - /// - public async Task> ManageProjectInfraAsync1(List infraDtos, Guid tenantId, Employee loggedInEmployee) - { - // --- Step 1: Input Validation --- - if (infraDtos == null || !infraDtos.Any()) - { - _logger.LogWarning("ManageProjectInfraAsync called with null or empty DTO list."); - return ApiResponse.ErrorResponse("Invalid details.", "Infrastructure data cannot be empty.", 400); - } - - _logger.LogInfo("Begin ManageProjectInfraAsync for {DtoCount} items, TenantId: {TenantId}, User: {UserId}", infraDtos.Count, tenantId, loggedInEmployee.Id); - - // --- Step 2: Categorize DTOs by Type and Action --- - var buildingsToCreateDto = infraDtos.Where(i => i.Building != null && i.Building.Id == null).Select(i => i.Building!).ToList(); - var buildingsToUpdateDto = infraDtos.Where(i => i.Building != null && i.Building.Id != null).Select(i => i.Building!).ToList(); - var floorsToCreateDto = infraDtos.Where(i => i.Floor != null && i.Floor.Id == null).Select(i => i.Floor!).ToList(); - var floorsToUpdateDto = infraDtos.Where(i => i.Floor != null && i.Floor.Id != null).Select(i => i.Floor!).ToList(); - var workAreasToCreateDto = infraDtos.Where(i => i.WorkArea != null && i.WorkArea.Id == null).Select(i => i.WorkArea!).ToList(); - var workAreasToUpdateDto = infraDtos.Where(i => i.WorkArea != null && i.WorkArea.Id != null).Select(i => i.WorkArea!).ToList(); - - _logger.LogDebug("Categorized DTOs..."); - - try - { - // --- Step 3: Fetch all required existing data in bulk --- - - // Fetch existing entities to be updated - var buildingIdsToUpdate = buildingsToUpdateDto.Select(d => d.Id!.Value).ToList(); - var existingBuildings = await _context.Buildings.Where(b => buildingIdsToUpdate.Contains(b.Id) && b.TenantId == tenantId).ToDictionaryAsync(b => b.Id); - - var floorIdsToUpdate = floorsToUpdateDto.Select(d => d.Id!.Value).ToList(); - var existingFloors = await _context.Floor.Include(f => f.Building).Where(f => floorIdsToUpdate.Contains(f.Id) && f.TenantId == tenantId).ToDictionaryAsync(f => f.Id); - - var workAreaIdsToUpdate = workAreasToUpdateDto.Select(d => d.Id!.Value).ToList(); - var existingWorkAreas = await _context.WorkAreas.Include(wa => wa.Floor!.Building).Where(wa => workAreaIdsToUpdate.Contains(wa.Id) && wa.TenantId == tenantId).ToDictionaryAsync(wa => wa.Id); - - // Fetch parent entities for items being created to get their ProjectIds - var buildingIdsForNewFloors = floorsToCreateDto.Select(f => f.BuildingId).ToList(); - var parentBuildingsForNewFloors = await _context.Buildings.Where(b => buildingIdsForNewFloors.Contains(b.Id)).ToDictionaryAsync(b => b.Id); - - var floorIdsForNewWorkAreas = workAreasToCreateDto.Select(wa => wa.FloorId).ToList(); - var parentFloorsForNewWorkAreas = await _context.Floor.Include(f => f.Building).Where(f => floorIdsForNewWorkAreas.Contains(f.Id)).ToDictionaryAsync(f => f.Id); - - _logger.LogInfo("Fetched existing entities and parents for new items."); - - // --- Step 4: Aggregate all affected ProjectIds for Security Check --- - var affectedProjectIds = new HashSet(); - - // From buildings being created/updated - buildingsToCreateDto.ForEach(b => affectedProjectIds.Add(b.ProjectId)); - foreach (var b in existingBuildings.Values) { affectedProjectIds.Add(b.ProjectId); } - - // From floors being created/updated - foreach (var f in floorsToCreateDto) { if (parentBuildingsForNewFloors.TryGetValue(f.BuildingId, out var b)) affectedProjectIds.Add(b.ProjectId); } - foreach (var f in existingFloors.Values) { if (f.Building != null) affectedProjectIds.Add(f.Building.ProjectId); } - - // From work areas being created/updated - foreach (var wa in workAreasToCreateDto) { if (parentFloorsForNewWorkAreas.TryGetValue(wa.FloorId, out var f) && f.Building != null) affectedProjectIds.Add(f.Building.ProjectId); } - foreach (var wa in existingWorkAreas.Values) { if (wa.Floor?.Building != null) affectedProjectIds.Add(wa.Floor.Building.ProjectId); } - - // Security Check against the complete list of affected projects - var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageProjectInfra, loggedInEmployee.Id); - if (!hasPermission) - { - _logger.LogWarning("Access DENIED for user {UserId} trying to manage infrastructure for projects.", loggedInEmployee.Id); - return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to manage infrastructure for one or more of the specified projects.", 403); - } - - // --- Step 5: Process all logic IN MEMORY, tracking changes --- - - // Process Buildings - var createdBuildings = new List(); - foreach (var dto in buildingsToCreateDto) - { - var newBuilding = _mapper.Map(dto); - newBuilding.TenantId = tenantId; - createdBuildings.Add(newBuilding); - } - foreach (var dto in buildingsToUpdateDto) { if (existingBuildings.TryGetValue(dto.Id!.Value, out var b)) _mapper.Map(dto, b); } - - // Process Floors - var createdFloors = new List(); - foreach (var dto in floorsToCreateDto) - { - var newFloor = _mapper.Map(dto); - newFloor.TenantId = tenantId; - createdFloors.Add(newFloor); - } - foreach (var dto in floorsToUpdateDto) { if (existingFloors.TryGetValue(dto.Id!.Value, out var f)) _mapper.Map(dto, f); } - - // Process WorkAreas - var createdWorkAreas = new List(); - foreach (var dto in workAreasToCreateDto) - { - var newWorkArea = _mapper.Map(dto); - newWorkArea.TenantId = tenantId; - createdWorkAreas.Add(newWorkArea); - } - foreach (var dto in workAreasToUpdateDto) { if (existingWorkAreas.TryGetValue(dto.Id!.Value, out var wa)) _mapper.Map(dto, wa); } - - // --- Step 6: Save all database changes in a SINGLE TRANSACTION --- - if (createdBuildings.Any()) _context.Buildings.AddRange(createdBuildings); - if (createdFloors.Any()) _context.Floor.AddRange(createdFloors); - if (createdWorkAreas.Any()) _context.WorkAreas.AddRange(createdWorkAreas); - - if (_context.ChangeTracker.HasChanges()) - { - await _context.SaveChangesAsync(); - _logger.LogInfo("Database save successful."); - } - - // --- Step 7: Update Cache using the aggregated ProjectIds (Non-blocking) --- - var finalProjectIds = affectedProjectIds.ToList(); - if (finalProjectIds.Any()) - { - _ = Task.Run(async () => - { - try - { - _logger.LogInfo("Queuing background cache update for {ProjectCount} projects.", finalProjectIds.Count); - // Assuming your cache service has a method to handle this. - await _cache.RemoveProjectsAsync(finalProjectIds); - _logger.LogInfo("Background cache update task completed for projects: {ProjectIds}", string.Join(", ", finalProjectIds)); - } - catch (Exception ex) - { - _logger.LogError(ex, "An error occurred during the background cache update task for projects: {ProjectIds}", string.Join(", ", finalProjectIds)); - } - }); - } - - // --- Step 8: Prepare and return a clear response --- - var responseVm = new { /* ... as before ... */ }; - return ApiResponse.SuccessResponse(responseVm, "Infrastructure changes processed successfully.", 200); - } - catch (Exception ex) - { - _logger.LogError(ex, "An unexpected error occurred in ManageProjectInfraAsync."); - return ApiResponse.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500); - } - } - /// /// Creates or updates a batch of work items. /// This method is optimized to perform all database operations in a single, atomic transaction. @@ -1512,60 +1243,88 @@ namespace Marco.Pms.Services.Service return ApiResponse>.SuccessResponse(responseList, message, 200); } + public async Task DeleteProjectTaskAsync(Guid id, Guid tenantId, Employee loggedInEmployee) + { + // 1. Fetch the task and its parent data in a single query. + // This is still a major optimization, avoiding a separate query for the floor/building. + WorkItem? task = await _context.WorkItems + .AsNoTracking() // Use AsNoTracking because we will re-attach for deletion later. + .Include(t => t.WorkArea) + .ThenInclude(wa => wa!.Floor) + .ThenInclude(f => f!.Building) + .FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId); - //public async Task DeleteProjectTask(Guid id) - //{ - // var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - // List workAreaIds = new List(); - // WorkItem? task = await _context.WorkItems.AsNoTracking().Include(t => t.WorkArea).FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId); - // if (task != null) - // { - // if (task.CompletedWork == 0) - // { - // var assignedTask = await _context.TaskAllocations.Where(t => t.WorkItemId == id).ToListAsync(); - // if (assignedTask.Count == 0) - // { - // _context.WorkItems.Remove(task); - // await _context.SaveChangesAsync(); - // _logger.LogInfo("Task with ID {WorkItemId} has been successfully deleted.", task.Id); + // 2. Guard Clause: Handle non-existent task. + if (task == null) + { + _logger.LogWarning("Attempted to delete a non-existent task with ID {WorkItemId}", id); + return new ServiceResponse + { + Response = ApiResponse.ErrorResponse("Task not found.", $"A task with ID {id} was not found.", 404) + }; + } - // var floorId = task.WorkArea?.FloorId; - // var floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == floorId); + // 3. Guard Clause: Prevent deletion if work has started. + if (task.CompletedWork > 0) + { + double percentage = Math.Round((task.CompletedWork / task.PlannedWork) * 100, 2); + _logger.LogWarning("Task with ID {WorkItemId} is {CompletionPercentage}% complete and cannot be deleted.", task.Id, percentage); + return new ServiceResponse + { + Response = ApiResponse.ErrorResponse($"Task is {percentage}% complete and cannot be deleted.", "Deletion failed because the task has progress.", 400) + }; + } + // 4. Guard Clause: Efficiently check if the task is assigned in a separate, optimized query. + // AnyAsync() is highly efficient and translates to a `SELECT TOP 1` or `EXISTS` in SQL. + bool isAssigned = await _context.TaskAllocations.AnyAsync(t => t.WorkItemId == id); + if (isAssigned) + { + _logger.LogWarning("Task with ID {WorkItemId} is currently assigned and cannot be deleted.", task.Id); + return new ServiceResponse + { + Response = ApiResponse.ErrorResponse("Task is currently assigned and cannot be deleted.", "Deletion failed because the task is assigned to an employee.", 400) + }; + } - // workAreaIds.Add(task.WorkAreaId); - // var projectId = floor?.Building?.ProjectId; + // --- Success Path: All checks passed, proceed with deletion --- - // var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = $"Task Deleted in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}, in Area: {task.WorkArea?.AreaName} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}" }; - // await _signalR.SendNotificationAsync(notification); - // await _cache.DeleteWorkItemByIdAsync(task.Id); - // if (projectId != null) - // { - // await _cache.DeleteProjectByIdAsync(projectId.Value); - // } - // } - // else - // { - // _logger.LogWarning("Task with ID {WorkItemId} is currently assigned and cannot be deleted.", task.Id); - // return BadRequest(ApiResponse.ErrorResponse("Task is currently assigned and cannot be deleted.", "Task is currently assigned and cannot be deleted.", 400)); - // } - // } - // else - // { - // double percentage = (task.CompletedWork / task.PlannedWork) * 100; - // percentage = Math.Round(percentage, 2); - // _logger.LogWarning("Task with ID {WorkItemId} is {CompletionPercentage}% complete and cannot be deleted", task.Id, percentage); - // return BadRequest(ApiResponse.ErrorResponse(System.String.Format("Task is {0}% complete and cannot be deleted", percentage), System.String.Format("Task is {0}% complete and cannot be deleted", percentage), 400)); + var building = task.WorkArea?.Floor?.Building; + var notification = new + { + LoggedInUserId = loggedInEmployee.Id, + Keyword = "WorkItem", + WorkAreaIds = new[] { task.WorkAreaId }, + Message = $"Task Deleted in Building: {building?.Name ?? "N/A"}, on Floor: {task.WorkArea?.Floor?.FloorName ?? "N/A"}, in Area: {task.WorkArea?.AreaName ?? "N/A"} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}" + }; - // } - // } - // else - // { - // _logger.LogWarning("Task with ID {WorkItemId} not found ID database", id); - // } - // return Ok(ApiResponse.SuccessResponse(new { }, "Task deleted successfully", 200)); - //} + // 5. Perform the database deletion. + // We must attach a new instance or the original one without AsNoTracking. + // Since we used AsNoTracking, we create a 'stub' entity for deletion. + // This is more efficient than re-querying. + _context.WorkItems.Remove(new WorkItem { Id = task.Id }); + await _context.SaveChangesAsync(); + _logger.LogInfo("Task with ID {WorkItemId} has been successfully deleted.", task.Id); + // 6. Perform cache operations concurrently. + var cacheTasks = new List + { + _cache.DeleteWorkItemByIdAsync(task.Id) + }; + + if (building?.ProjectId != null) + { + cacheTasks.Add(_cache.DeleteProjectByIdAsync(building.ProjectId)); + } + await Task.WhenAll(cacheTasks); + + // 7. Return the final success response. + return new ServiceResponse + { + Notification = notification, + Response = ApiResponse.SuccessResponse(new { id = task.Id }, "Task deleted successfully.", 200) + }; + } #endregion #region =================================================================== Helper Functions =================================================================== diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs index f1c89cc..0c7c964 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs @@ -23,6 +23,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> GetWorkItemsAsync(Guid workAreaId, Guid tenantId, Employee loggedInEmployee); Task ManageProjectInfraAsync(List infraDtos, Guid tenantId, Employee loggedInEmployee); Task>> CreateProjectTaskAsync(List workItemDtos, Guid tenantId, Employee loggedInEmployee); + Task DeleteProjectTaskAsync(Guid id, Guid tenantId, Employee loggedInEmployee); } } From ccce0d48d5477a9432cac01221b68eff4d0f7bd2 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 17 Jul 2025 10:17:57 +0530 Subject: [PATCH 085/124] Remove the projectHelper and ProjetsHelper and move its bussiness logic to project services --- Marco.Pms.CacheHelper/EmployeeCache.cs | 20 +++ Marco.Pms.CacheHelper/ProjectCache.cs | 74 ++++++++---- .../EmployeePermissionMongoDB.cs | 1 + .../MongoDBModels/ProjectMongoDB.cs | 1 + .../Controllers/AttendanceController.cs | 13 +- .../Controllers/EmployeeController.cs | 9 +- .../Controllers/UserController.cs | 11 +- Marco.Pms.Services/Helpers/ProjectHelper.cs | 37 ------ Marco.Pms.Services/Helpers/ProjectsHelper.cs | 81 ------------- Marco.Pms.Services/Program.cs | 1 - Marco.Pms.Services/Service/ProjectServices.cs | 114 ++++++++++++++++-- .../ServiceInterfaces/IProjectServices.cs | 6 + 12 files changed, 206 insertions(+), 162 deletions(-) delete mode 100644 Marco.Pms.Services/Helpers/ProjectHelper.cs delete mode 100644 Marco.Pms.Services/Helpers/ProjectsHelper.cs diff --git a/Marco.Pms.CacheHelper/EmployeeCache.cs b/Marco.Pms.CacheHelper/EmployeeCache.cs index f7b7066..0079106 100644 --- a/Marco.Pms.CacheHelper/EmployeeCache.cs +++ b/Marco.Pms.CacheHelper/EmployeeCache.cs @@ -33,6 +33,8 @@ namespace Marco.Pms.CacheHelper var result = await _collection.UpdateOneAsync(filter, update, options); + await InitializeCollectionAsync(); + // 6. Return a more accurate result indicating success for both updates and upserts. // The operation is successful if an existing document was modified OR a new one was created. return result.IsAcknowledged && (result.ModifiedCount > 0 || result.UpsertedId != null); @@ -51,6 +53,7 @@ namespace Marco.Pms.CacheHelper { return false; } + await InitializeCollectionAsync(); return true; } public async Task> GetProjectsFromCache(Guid employeeId) @@ -177,5 +180,22 @@ namespace Marco.Pms.CacheHelper return true; } + + // A private method to handle the one-time setup of the collection's indexes. + private async Task InitializeCollectionAsync() + { + // 1. Define the TTL (Time-To-Live) index on the 'ExpireAt' field. + var indexKeys = Builders.IndexKeys.Ascending(x => x.ExpireAt); + var indexOptions = new CreateIndexOptions + { + // This tells MongoDB to automatically delete documents when their 'ExpireAt' time is reached. + ExpireAfter = TimeSpan.FromSeconds(0) + }; + var indexModel = new CreateIndexModel(indexKeys, indexOptions); + + // 2. Create the index. This is an idempotent operation if the index already exists. + // Use CreateOneAsync since we are only creating a single index. + await _collection.Indexes.CreateOneAsync(indexModel); + } } } diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index 9417724..df95419 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -11,27 +11,59 @@ namespace Marco.Pms.CacheHelper { public class ProjectCache { - private readonly ApplicationDbContext _context; - private readonly IMongoCollection _projetCollection; + private readonly IMongoCollection _projectCollection; private readonly IMongoCollection _taskCollection; public ProjectCache(ApplicationDbContext context, IConfiguration configuration) { var connectionString = configuration["MongoDB:ConnectionString"]; - _context = context; var mongoUrl = new MongoUrl(connectionString); var client = new MongoClient(mongoUrl); // Your MongoDB connection string var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name - _projetCollection = mongoDB.GetCollection("ProjectDetails"); + _projectCollection = mongoDB.GetCollection("ProjectDetails"); _taskCollection = mongoDB.GetCollection("WorkItemDetails"); } public async Task AddProjectDetailsToCache(ProjectMongoDB projectDetails) { - await _projetCollection.InsertOneAsync(projectDetails); + await _projectCollection.InsertOneAsync(projectDetails); + + var indexKeys = Builders.IndexKeys.Ascending(x => x.ExpireAt); + var indexOptions = new CreateIndexOptions + { + ExpireAfter = TimeSpan.Zero // required for fixed expiration time + }; + var indexModel = new CreateIndexModel(indexKeys, indexOptions); + await _projectCollection.Indexes.CreateOneAsync(indexModel); + } + // The method should focus only on inserting data. public async Task AddProjectDetailsListToCache(List projectDetailsList) { - await _projetCollection.InsertManyAsync(projectDetailsList); + // 1. Add a guard clause to avoid an unnecessary database call for an empty list. + if (projectDetailsList == null || !projectDetailsList.Any()) + { + return; + } + + // 2. Perform the insert operation. This is the only responsibility of this method. + await _projectCollection.InsertManyAsync(projectDetailsList); + await InitializeCollectionAsync(); + } + // A private method to handle the one-time setup of the collection's indexes. + private async Task InitializeCollectionAsync() + { + // 1. Define the TTL (Time-To-Live) index on the 'ExpireAt' field. + var indexKeys = Builders.IndexKeys.Ascending(x => x.ExpireAt); + var indexOptions = new CreateIndexOptions + { + // This tells MongoDB to automatically delete documents when their 'ExpireAt' time is reached. + ExpireAfter = TimeSpan.FromSeconds(0) + }; + var indexModel = new CreateIndexModel(indexKeys, indexOptions); + + // 2. Create the index. This is an idempotent operation if the index already exists. + // Use CreateOneAsync since we are only creating a single index. + await _projectCollection.Indexes.CreateOneAsync(indexModel); } public async Task UpdateProjectDetailsOnlyToCache(Project project, StatusMaster projectStatus) { @@ -51,7 +83,7 @@ namespace Marco.Pms.CacheHelper ); // Perform the update - var result = await _projetCollection.UpdateOneAsync( + var result = await _projectCollection.UpdateOneAsync( filter: r => r.Id == project.Id.ToString(), update: updates ); @@ -71,7 +103,7 @@ namespace Marco.Pms.CacheHelper var projection = Builders.Projection.Exclude(p => p.Buildings); // Perform query - var project = await _projetCollection + var project = await _projectCollection .Find(filter) .Project(projection) .FirstOrDefaultAsync(); @@ -83,7 +115,7 @@ namespace Marco.Pms.CacheHelper List stringProjectIds = projectIds.Select(p => p.ToString()).ToList(); var filter = Builders.Filter.In(p => p.Id, stringProjectIds); var projection = Builders.Projection.Exclude(p => p.Buildings); - var projects = await _projetCollection + var projects = await _projectCollection .Find(filter) .Project(projection) .ToListAsync(); @@ -92,14 +124,14 @@ namespace Marco.Pms.CacheHelper public async Task DeleteProjectByIdFromCacheAsync(Guid projectId) { var filter = Builders.Filter.Eq(e => e.Id, projectId.ToString()); - var result = await _projetCollection.DeleteOneAsync(filter); + var result = await _projectCollection.DeleteOneAsync(filter); return result.DeletedCount > 0; } public async Task RemoveProjectsFromCacheAsync(List projectIds) { var stringIds = projectIds.Select(id => id.ToString()).ToList(); var filter = Builders.Filter.In(p => p.Id, stringIds); - var result = await _projetCollection.DeleteManyAsync(filter); + var result = await _projectCollection.DeleteManyAsync(filter); return result.DeletedCount > 0; } @@ -125,7 +157,7 @@ namespace Marco.Pms.CacheHelper var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); var update = Builders.Update.Push("Buildings", buildingMongo); - var result = await _projetCollection.UpdateOneAsync(filter, update); + var result = await _projectCollection.UpdateOneAsync(filter, update); if (result.MatchedCount == 0) { @@ -155,7 +187,7 @@ namespace Marco.Pms.CacheHelper ); var update = Builders.Update.Push("Buildings.$.Floors", floorMongo); - var result = await _projetCollection.UpdateOneAsync(filter, update); + var result = await _projectCollection.UpdateOneAsync(filter, update); if (result.MatchedCount == 0) { @@ -189,7 +221,7 @@ namespace Marco.Pms.CacheHelper var update = Builders.Update.Push("Buildings.$[b].Floors.$[f].WorkAreas", workAreaMongo); var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; - var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); + var result = await _projectCollection.UpdateOneAsync(filter, update, updateOptions); if (result.MatchedCount == 0) { @@ -221,7 +253,7 @@ namespace Marco.Pms.CacheHelper Builders.Update.Set("Buildings.$.Description", building.Description) ); - var result = await _projetCollection.UpdateOneAsync(filter, update); + var result = await _projectCollection.UpdateOneAsync(filter, update); if (result.MatchedCount == 0) { @@ -246,7 +278,7 @@ namespace Marco.Pms.CacheHelper var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); - var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); + var result = await _projectCollection.UpdateOneAsync(filter, update, updateOptions); if (result.MatchedCount == 0) { @@ -272,7 +304,7 @@ namespace Marco.Pms.CacheHelper var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); - var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); + var result = await _projectCollection.UpdateOneAsync(filter, update, updateOptions); if (result.MatchedCount == 0) { @@ -296,7 +328,7 @@ namespace Marco.Pms.CacheHelper var filter = Builders.Filter.Eq(p => p.Id, projectId.ToString()); // Project only the "Buildings" field from the document - var buildings = await _projetCollection + var buildings = await _projectCollection .Find(filter) .Project(p => p.Buildings) .FirstOrDefaultAsync(); @@ -315,7 +347,7 @@ namespace Marco.Pms.CacheHelper public async Task UpdatePlannedAndCompleteWorksInBuildingFromCache(Guid workAreaId, double plannedWork, double completedWork) { var filter = Builders.Filter.Eq("Buildings.Floors.WorkAreas._id", workAreaId.ToString()); - var project = await _projetCollection.Find(filter).FirstOrDefaultAsync(); + var project = await _projectCollection.Find(filter).FirstOrDefaultAsync(); string? selectedBuildingId = null; string? selectedFloorId = null; @@ -353,7 +385,7 @@ namespace Marco.Pms.CacheHelper .Inc("Buildings.$[b].CompletedWork", completedWork) .Inc("PlannedWork", plannedWork) .Inc("CompletedWork", completedWork); - var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); + var result = await _projectCollection.UpdateOneAsync(filter, update, updateOptions); } public async Task GetBuildingAndFloorByWorkAreaIdFromCache(Guid workAreaId) @@ -393,7 +425,7 @@ namespace Marco.Pms.CacheHelper { "WorkArea", "$Buildings.Floors.WorkAreas" } }) }; - var result = await _projetCollection.Aggregate(pipeline).FirstOrDefaultAsync(); + var result = await _projectCollection.Aggregate(pipeline).FirstOrDefaultAsync(); if (result == null) return null; return result; diff --git a/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs b/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs index 49c514e..fab2b84 100644 --- a/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs @@ -9,5 +9,6 @@ namespace Marco.Pms.Model.MongoDBModels public List ApplicationRoleIds { get; set; } = new List(); public List PermissionIds { get; set; } = new List(); public List ProjectIds { get; set; } = new List(); + public DateTime ExpireAt { get; set; } = DateTime.UtcNow.Date.AddDays(1); } } diff --git a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs index 7f3a557..aac0e2c 100644 --- a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs @@ -14,5 +14,6 @@ public int TeamSize { get; set; } public double CompletedWork { get; set; } public double PlannedWork { get; set; } + public DateTime ExpireAt { get; set; } = DateTime.UtcNow.Date.AddDays(1); } } diff --git a/Marco.Pms.Services/Controllers/AttendanceController.cs b/Marco.Pms.Services/Controllers/AttendanceController.cs index 1a5e4e7..7339966 100644 --- a/Marco.Pms.Services/Controllers/AttendanceController.cs +++ b/Marco.Pms.Services/Controllers/AttendanceController.cs @@ -9,6 +9,7 @@ using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.AttendanceVM; using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Service; +using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; @@ -28,7 +29,7 @@ namespace MarcoBMS.Services.Controllers { private readonly ApplicationDbContext _context; private readonly EmployeeHelper _employeeHelper; - private readonly ProjectsHelper _projectsHelper; + private readonly IProjectServices _projectServices; private readonly UserHelper _userHelper; private readonly S3UploadService _s3Service; private readonly PermissionServices _permission; @@ -37,11 +38,11 @@ namespace MarcoBMS.Services.Controllers public AttendanceController( - ApplicationDbContext context, EmployeeHelper employeeHelper, ProjectsHelper projectsHelper, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permission, IHubContext signalR) + ApplicationDbContext context, EmployeeHelper employeeHelper, IProjectServices projectServices, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permission, IHubContext signalR) { _context = context; _employeeHelper = employeeHelper; - _projectsHelper = projectsHelper; + _projectServices = projectServices; _userHelper = userHelper; _s3Service = s3Service; _logger = logger; @@ -188,7 +189,7 @@ namespace MarcoBMS.Services.Controllers List lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date >= fromDate.Date && c.AttendanceDate.Date <= toDate.Date && c.TenantId == TenantId).ToListAsync(); - List projectteam = await _projectsHelper.GetTeamByProject(TenantId, projectId, true); + List projectteam = await _projectServices.GetTeamByProject(TenantId, projectId, true); var jobRole = await _context.JobRoles.ToListAsync(); foreach (Attendance? attendance in lstAttendance) { @@ -295,7 +296,7 @@ namespace MarcoBMS.Services.Controllers List lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date == forDate && c.TenantId == TenantId).ToListAsync(); - List projectteam = await _projectsHelper.GetTeamByProject(TenantId, projectId, IncludeInActive); + List projectteam = await _projectServices.GetTeamByProject(TenantId, projectId, IncludeInActive); var idList = projectteam.Select(p => p.EmployeeId).ToList(); //var emp = await _context.Employees.Where(e => idList.Contains(e.Id)).Include(e => e.JobRole).ToListAsync(); var jobRole = await _context.JobRoles.ToListAsync(); @@ -378,7 +379,7 @@ 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); + List projectteam = await _projectServices.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/EmployeeController.cs b/Marco.Pms.Services/Controllers/EmployeeController.cs index c9e19fa..d5d7f3d 100644 --- a/Marco.Pms.Services/Controllers/EmployeeController.cs +++ b/Marco.Pms.Services/Controllers/EmployeeController.cs @@ -9,6 +9,7 @@ using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Service; +using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; @@ -37,13 +38,13 @@ namespace MarcoBMS.Services.Controllers private readonly ILoggingService _logger; private readonly IHubContext _signalR; private readonly PermissionServices _permission; - private readonly ProjectsHelper _projectsHelper; + private readonly IProjectServices _projectServices; private readonly Guid tenantId; public EmployeeController(UserManager userManager, IEmailSender emailSender, ApplicationDbContext context, EmployeeHelper employeeHelper, UserHelper userHelper, IConfiguration configuration, ILoggingService logger, - IHubContext signalR, PermissionServices permission, ProjectsHelper projectsHelper) + IHubContext signalR, PermissionServices permission, IProjectServices projectServices) { _context = context; _userManager = userManager; @@ -54,7 +55,7 @@ namespace MarcoBMS.Services.Controllers _logger = logger; _signalR = signalR; _permission = permission; - _projectsHelper = projectsHelper; + _projectServices = projectServices; tenantId = _userHelper.GetTenantId(); } @@ -119,7 +120,7 @@ namespace MarcoBMS.Services.Controllers loggedInEmployee.Id, projectid ?? Guid.Empty, ShowInactive); // Step 3: Fetch project access and permissions - var projectIds = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); + var projectIds = await _projectServices.GetMyProjectIdsAsync(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/UserController.cs b/Marco.Pms.Services/Controllers/UserController.cs index 4bb4432..8269d3e 100644 --- a/Marco.Pms.Services/Controllers/UserController.cs +++ b/Marco.Pms.Services/Controllers/UserController.cs @@ -4,6 +4,7 @@ using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Employee; +using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -19,14 +20,14 @@ namespace MarcoBMS.Services.Controllers private readonly UserHelper _userHelper; private readonly EmployeeHelper _employeeHelper; - private readonly ProjectsHelper _projectsHelper; + private readonly IProjectServices _projectServices; private readonly RolesHelper _rolesHelper; - public UserController(EmployeeHelper employeeHelper, ProjectsHelper projectsHelper, UserHelper userHelper, RolesHelper rolesHelper) + public UserController(EmployeeHelper employeeHelper, IProjectServices projectServices, UserHelper userHelper, RolesHelper rolesHelper) { _userHelper = userHelper; _employeeHelper = employeeHelper; - _projectsHelper = projectsHelper; + _projectServices = projectServices; _rolesHelper = rolesHelper; } @@ -56,12 +57,12 @@ namespace MarcoBMS.Services.Controllers /* User with permission manage project can see all projects */ if (featurePermission != null && featurePermission.Exists(c => c.Id.ToString() == "172fc9b6-755b-4f62-ab26-55c34a330614")) { - List projects = await _projectsHelper.GetAllProjectByTanentID(emp.TenantId); + List projects = await _projectServices.GetAllProjectByTanentID(emp.TenantId); projectsId = projects.Select(c => c.Id.ToString()).ToArray(); } else { - List allocation = await _projectsHelper.GetProjectByEmployeeID(emp.Id); + List allocation = await _projectServices.GetProjectByEmployeeID(emp.Id); projectsId = allocation.Select(c => c.ProjectId.ToString()).ToArray(); } EmployeeProfile profile = new EmployeeProfile() { }; diff --git a/Marco.Pms.Services/Helpers/ProjectHelper.cs b/Marco.Pms.Services/Helpers/ProjectHelper.cs deleted file mode 100644 index f1b688e..0000000 --- a/Marco.Pms.Services/Helpers/ProjectHelper.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Marco.Pms.DataAccess.Data; -using Marco.Pms.Model.Projects; -using Microsoft.CodeAnalysis; -using Microsoft.EntityFrameworkCore; - - -namespace ModelServices.Helpers -{ - public class ProjectHelper - { - private readonly ApplicationDbContext _context; - public ProjectHelper(ApplicationDbContext context) - { - _context = context; - } - - public async Task> GetTeamByProject(Guid TenantId, Guid ProjectId, bool IncludeInactive) - { - if (IncludeInactive) - { - - var employees = await _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == ProjectId).Include(e => e.Employee).ToListAsync(); - - return employees; - } - else - { - var employees = await _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == ProjectId && c.IsActive == true).Include(e => e.Employee).ToListAsync(); - - return employees; - } - } - - - - } -} diff --git a/Marco.Pms.Services/Helpers/ProjectsHelper.cs b/Marco.Pms.Services/Helpers/ProjectsHelper.cs deleted file mode 100644 index e7e1dd6..0000000 --- a/Marco.Pms.Services/Helpers/ProjectsHelper.cs +++ /dev/null @@ -1,81 +0,0 @@ -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 Marco.Pms.Services.Service; -using Microsoft.EntityFrameworkCore; - -namespace MarcoBMS.Services.Helpers -{ - public class ProjectsHelper - { - private readonly ApplicationDbContext _context; - private readonly CacheUpdateHelper _cache; - private readonly PermissionServices _permission; - - public ProjectsHelper(ApplicationDbContext context, CacheUpdateHelper cache, PermissionServices permission) - { - _context = context; - _cache = cache; - _permission = permission; - } - - public async Task> GetAllProjectByTanentID(Guid tanentID) - { - List alloc = await _context.Projects.Where(c => c.TenantId == tanentID).ToListAsync(); - return alloc; - } - - public async Task> GetProjectByEmployeeID(Guid employeeID) - { - List alloc = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeID && c.IsActive == true).Include(c => c.Project).ToListAsync(); - return alloc; - } - - public async Task> GetTeamByProject(Guid TenantId, Guid ProjectId, bool IncludeInactive) - { - if (IncludeInactive) - { - - var employees = await _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == ProjectId).Include(e => e.Employee).ToListAsync(); - - return employees; - } - else - { - var employees = await _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == ProjectId && c.IsActive == true).Include(e => e.Employee).ToListAsync(); - - return employees; - } - } - - public async Task> GetMyProjects(Guid tenantId, Employee LoggedInEmployee) - { - var projectIds = await _cache.GetProjects(LoggedInEmployee.Id); - - if (projectIds == null) - { - var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageProject, LoggedInEmployee.Id); - if (hasPermission) - { - var projects = await _context.Projects.Where(c => c.TenantId == tenantId).ToListAsync(); - projectIds = projects.Select(p => p.Id).ToList(); - } - else - { - var allocation = await GetProjectByEmployeeID(LoggedInEmployee.Id); - if (!allocation.Any()) - { - return new List(); - } - projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); - } - await _cache.AddProjects(LoggedInEmployee.Id, projectIds); - } - - return projectIds; - } - - } -} \ No newline at end of file diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index 3c73416..3f012e2 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -167,7 +167,6 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs index d7ab2ac..9406ec9 100644 --- a/Marco.Pms.Services/Service/ProjectServices.cs +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -12,7 +12,6 @@ using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Model.ViewModels.Projects; using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Service.ServiceInterfaces; -using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.CodeAnalysis; using Microsoft.EntityFrameworkCore; @@ -25,7 +24,6 @@ namespace Marco.Pms.Services.Service private readonly IDbContextFactory _dbContextFactory; private readonly ApplicationDbContext _context; // Keeping this for direct scoped context use where appropriate private readonly ILoggingService _logger; - private readonly ProjectsHelper _projectsHelper; private readonly PermissionServices _permission; private readonly CacheUpdateHelper _cache; private readonly IMapper _mapper; @@ -34,7 +32,6 @@ namespace Marco.Pms.Services.Service IDbContextFactory dbContextFactory, ApplicationDbContext context, ILoggingService logger, - ProjectsHelper projectsHelper, PermissionServices permission, CacheUpdateHelper cache, IMapper mapper, @@ -43,7 +40,6 @@ namespace Marco.Pms.Services.Service _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); _context = context ?? throw new ArgumentNullException(nameof(context)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _projectsHelper = projectsHelper ?? throw new ArgumentNullException(nameof(projectsHelper)); _permission = permission ?? throw new ArgumentNullException(nameof(permission)); _cache = cache ?? throw new ArgumentNullException(nameof(cache)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); @@ -64,7 +60,7 @@ namespace Marco.Pms.Services.Service _logger.LogInfo("Basic project list requested by EmployeeId {EmployeeId}", loggedInEmployee.Id); // Step 2: Get the list of project IDs the user has access to - List accessibleProjectIds = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); + List accessibleProjectIds = await GetMyProjects(tenantId, loggedInEmployee); if (accessibleProjectIds == null || !accessibleProjectIds.Any()) { @@ -94,7 +90,7 @@ namespace Marco.Pms.Services.Service _logger.LogInfo("Starting GetAllProjects for TenantId: {TenantId}, User: {UserId}", tenantId, loggedInEmployee.Id); // --- Step 1: Get a list of project IDs the user can access --- - List projectIds = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); + List projectIds = await GetMyProjects(tenantId, loggedInEmployee); if (!projectIds.Any()) { _logger.LogInfo("User has no assigned projects. Returning empty list."); @@ -743,7 +739,7 @@ namespace Marco.Pms.Services.Service // This is a placeholder for your actual, more specific permission logic. // It should also handle the case where a user is requesting their own projects (employeeId == loggedInEmployee.Id). var hasPermission = await _permission.HasPermission(PermissionsMaster.ViewProject, loggedInEmployee.Id); - var projectIds = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); + var projectIds = await GetMyProjects(tenantId, loggedInEmployee); if (!hasPermission) { _logger.LogWarning("Access DENIED for user {UserId} trying to view projects for employee {TargetEmployeeId}.", loggedInEmployee.Id, employeeId); @@ -1329,6 +1325,110 @@ namespace Marco.Pms.Services.Service #region =================================================================== Helper Functions =================================================================== + public async Task> GetAllProjectByTanentID(Guid tanentId) + { + List alloc = await _context.Projects.Where(c => c.TenantId == tanentId).ToListAsync(); + return alloc; + } + + public async Task> GetProjectByEmployeeID(Guid employeeId) + { + List alloc = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.IsActive == true).Include(c => c.Project).ToListAsync(); + return alloc; + } + + public async Task> GetTeamByProject(Guid TenantId, Guid ProjectId, bool IncludeInactive) + { + if (IncludeInactive) + { + + var employees = await _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == ProjectId).Include(e => e.Employee).ToListAsync(); + + return employees; + } + else + { + var employees = await _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == ProjectId && c.IsActive == true).Include(e => e.Employee).ToListAsync(); + + return employees; + } + } + + public async Task> GetMyProjects(Guid tenantId, Employee LoggedInEmployee) + { + var projectIds = await _cache.GetProjects(LoggedInEmployee.Id); + + if (projectIds == null) + { + var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageProject, LoggedInEmployee.Id); + if (hasPermission) + { + var projects = await _context.Projects.Where(c => c.TenantId == tenantId).ToListAsync(); + projectIds = projects.Select(p => p.Id).ToList(); + } + else + { + var allocation = await GetProjectByEmployeeID(LoggedInEmployee.Id); + if (!allocation.Any()) + { + return new List(); + } + projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); + } + await _cache.AddProjects(LoggedInEmployee.Id, projectIds); + } + return projectIds; + } + + public async Task> GetMyProjectIdsAsync(Guid tenantId, Employee loggedInEmployee) + { + // 1. Attempt to retrieve the list of project IDs from the cache first. + // This is the "happy path" and should be as fast as possible. + List? projectIds = await _cache.GetProjects(loggedInEmployee.Id); + + if (projectIds != null) + { + // Cache Hit: Return the cached list immediately. + return projectIds; + } + + // 2. Cache Miss: The list was not in the cache, so we must fetch it from the database. + List newProjectIds; + + // Check for the specific permission. + var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageProject, loggedInEmployee.Id); + + if (hasPermission) + { + // 3a. OPTIMIZATION: User has permission to see all projects. + // Fetch *only* the Ids directly from the database. This is far more efficient + // than fetching full Project objects and then selecting the Ids in memory. + newProjectIds = await _context.Projects + .Where(p => p.TenantId == tenantId) + .Select(p => p.Id) // This translates to `SELECT Id FROM Projects...` in SQL. + .ToListAsync(); + } + else + { + // 3b. OPTIMIZATION: User can only see projects they are allocated to. + // We go directly to the source (ProjectAllocations) and ask the database + // for a distinct list of ProjectIds. This is much better than calling a + // helper function that might return full allocation objects. + newProjectIds = await _context.ProjectAllocations + .Where(a => a.EmployeeId == loggedInEmployee.Id && a.ProjectId != Guid.Empty) + .Select(a => a.ProjectId) + .Distinct() // Pushes the DISTINCT operation to the database. + .ToListAsync(); + } + + // 4. Populate the cache with the newly fetched list (even if it's empty). + // This prevents repeated database queries for employees with no projects. + await _cache.AddProjects(loggedInEmployee.Id, newProjectIds); + + return newProjectIds; + } + + /// /// 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 diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs index 0c7c964..b5acccc 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs @@ -1,5 +1,6 @@ using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Employees; +using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Projects; @@ -25,5 +26,10 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task>> CreateProjectTaskAsync(List workItemDtos, Guid tenantId, Employee loggedInEmployee); Task DeleteProjectTaskAsync(Guid id, Guid tenantId, Employee loggedInEmployee); + Task> GetAllProjectByTanentID(Guid tanentId); + Task> GetProjectByEmployeeID(Guid employeeId); + Task> GetTeamByProject(Guid TenantId, Guid ProjectId, bool IncludeInactive); + Task> GetMyProjectIdsAsync(Guid tenantId, Employee LoggedInEmployee); + } } From d1106bc86b362d528ddecd8acb2a9e0550486f19 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 17 Jul 2025 12:01:47 +0530 Subject: [PATCH 086/124] Can able update note of deleted not also --- Marco.Pms.Services/Helpers/DirectoryHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Marco.Pms.Services/Helpers/DirectoryHelper.cs b/Marco.Pms.Services/Helpers/DirectoryHelper.cs index 37f58cf..199a410 100644 --- a/Marco.Pms.Services/Helpers/DirectoryHelper.cs +++ b/Marco.Pms.Services/Helpers/DirectoryHelper.cs @@ -1086,7 +1086,7 @@ namespace Marco.Pms.Services.Helpers var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); if (noteDto != null && id == noteDto.Id) { - Contact? contact = await _context.Contacts.FirstOrDefaultAsync(c => c.Id == noteDto.ContactId && c.IsActive && c.TenantId == tenantId); + Contact? contact = await _context.Contacts.FirstOrDefaultAsync(c => c.Id == noteDto.ContactId && c.TenantId == tenantId); if (contact != null) { ContactNote? contactNote = await _context.ContactNotes.Include(cn => cn.Createdby).Include(cn => cn.Contact).FirstOrDefaultAsync(n => n.Id == noteDto.Id && n.ContactId == contact.Id && n.IsActive); From 2a3c75b0c83ae3190bb7af2c61b555143124518c Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 17 Jul 2025 12:42:02 +0530 Subject: [PATCH 087/124] Removed the reassgining of same object --- Marco.Pms.Services/Helpers/DirectoryHelper.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Marco.Pms.Services/Helpers/DirectoryHelper.cs b/Marco.Pms.Services/Helpers/DirectoryHelper.cs index 33460b2..c762d70 100644 --- a/Marco.Pms.Services/Helpers/DirectoryHelper.cs +++ b/Marco.Pms.Services/Helpers/DirectoryHelper.cs @@ -1157,11 +1157,12 @@ namespace Marco.Pms.Services.Helpers List employeeBuckets = await _context.EmployeeBucketMappings.Where(b => b.EmployeeId == LoggedInEmployee.Id).ToListAsync(); var bucketIds = employeeBuckets.Select(b => b.BucketId).ToList(); - List employeeBucketVM = await _context.EmployeeBucketMappings.Where(b => bucketIds.Contains(b.BucketId)).ToListAsync(); + List bucketList = new List(); if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) { bucketList = await _context.Buckets.Include(b => b.CreatedBy).Where(b => b.TenantId == tenantId).ToListAsync(); + bucketIds = bucketList.Select(b => b.Id).ToList(); } else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) || permissionIds.Contains(PermissionsMaster.DirectoryUser)) { @@ -1173,6 +1174,8 @@ namespace Marco.Pms.Services.Helpers return ApiResponse.ErrorResponse("You don't have permission", "You don't have permission", 401); } + List employeeBucketVM = await _context.EmployeeBucketMappings.Where(b => bucketIds.Contains(b.BucketId)).ToListAsync(); + List bucketVMs = new List(); if (bucketList.Any()) { From 760b4638e607856e132064aa093c5c479580babe Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 17 Jul 2025 12:53:01 +0530 Subject: [PATCH 088/124] Added one more condition to check if active is false while removing the employee from buckets --- Marco.Pms.Services/Helpers/DirectoryHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Marco.Pms.Services/Helpers/DirectoryHelper.cs b/Marco.Pms.Services/Helpers/DirectoryHelper.cs index c762d70..ff66868 100644 --- a/Marco.Pms.Services/Helpers/DirectoryHelper.cs +++ b/Marco.Pms.Services/Helpers/DirectoryHelper.cs @@ -1369,7 +1369,7 @@ namespace Marco.Pms.Services.Helpers _context.EmployeeBucketMappings.Add(employeeBucketMapping); assignedEmployee += 1; } - else + else if (!assignBucket.IsActive) { EmployeeBucketMapping? employeeBucketMapping = employeeBuckets.FirstOrDefault(eb => eb.BucketId == bucketId && eb.EmployeeId == assignBucket.EmployeeId); if (employeeBucketMapping != null) From 0ec507c97c2512faaa8094beb9aa98609322e28b Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 17 Jul 2025 15:31:05 +0530 Subject: [PATCH 089/124] Included the jobrole when feaching employee list --- Marco.Pms.Services/Helpers/EmployeeHelper.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Marco.Pms.Services/Helpers/EmployeeHelper.cs b/Marco.Pms.Services/Helpers/EmployeeHelper.cs index 926e7fd..343144a 100644 --- a/Marco.Pms.Services/Helpers/EmployeeHelper.cs +++ b/Marco.Pms.Services/Helpers/EmployeeHelper.cs @@ -81,6 +81,7 @@ namespace MarcoBMS.Services.Helpers result = await _context.ProjectAllocations .Include(pa => pa.Employee) + .ThenInclude(e => e!.JobRole) .Where(c => c.ProjectId == ProjectId.Value && c.IsActive && c.Employee != null) .Select(pa => pa.Employee!.ToEmployeeVMFromEmployee()) .ToListAsync(); From c6ba233e6de20c800bed9342b39bc44e52713658 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 17 Jul 2025 16:06:54 +0530 Subject: [PATCH 090/124] Added the logs setp in program.cs --- Marco.Pms.Services/Program.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index 3f012e2..5549702 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -23,9 +23,21 @@ var builder = WebApplication.CreateBuilder(args); #region ======================= Service Configuration (Dependency Injection) ======================= #region Logging + +// Add Serilog Configuration +string? mongoConn = builder.Configuration["MongoDB:SerilogDatabaseUrl"]; +string timeString = "00:00:30"; +TimeSpan.TryParse(timeString, out TimeSpan timeSpan); + builder.Host.UseSerilog((context, config) => { - config.ReadFrom.Configuration(context.Configuration); + config.ReadFrom.Configuration(context.Configuration) + .WriteTo.MongoDB( + databaseUrl: mongoConn ?? string.Empty, + collectionName: "api-logs", + batchPostingLimit: 100, + period: timeSpan + ); }); #endregion From 5b0e9ffb7c51da565562dd280f1a0ce7c1435a66 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 17 Jul 2025 16:38:39 +0530 Subject: [PATCH 091/124] added new function delete all employee entries from cache --- Marco.Pms.CacheHelper/EmployeeCache.cs | 21 +++++++++++-------- .../Helpers/CacheUpdateHelper.cs | 11 ++++++++++ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/Marco.Pms.CacheHelper/EmployeeCache.cs b/Marco.Pms.CacheHelper/EmployeeCache.cs index 0079106..7c7f4b4 100644 --- a/Marco.Pms.CacheHelper/EmployeeCache.cs +++ b/Marco.Pms.CacheHelper/EmployeeCache.cs @@ -122,16 +122,10 @@ namespace Marco.Pms.CacheHelper public async Task ClearAllProjectIdsByPermissionIdFromCache(Guid permissionId) { var filter = Builders.Filter.AnyEq(e => e.PermissionIds, permissionId.ToString()); + var update = Builders.Update.Set(e => e.ProjectIds, new List()); - var update = Builders.Update - .Set(e => e.ProjectIds, new List()); - - var result = await _collection.UpdateOneAsync(filter, update); - - if (result.MatchedCount == 0) - return false; - - return true; + var result = await _collection.UpdateManyAsync(filter, update).ConfigureAwait(false); + return result.IsAcknowledged && result.ModifiedCount > 0; } public async Task RemoveRoleIdFromCache(Guid employeeId, Guid roleId) { @@ -180,6 +174,15 @@ namespace Marco.Pms.CacheHelper return true; } + public async Task ClearAllEmployeesFromCache() + { + var result = await _collection.DeleteManyAsync(FilterDefinition.Empty); + + if (result.DeletedCount == 0) + return false; + + return true; + } // A private method to handle the one-time setup of the collection's indexes. private async Task InitializeCollectionAsync() diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index b0b1e06..9bb159b 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -811,6 +811,17 @@ namespace Marco.Pms.Services.Helpers _logger.LogWarning("Error occured while deleting Application role {RoleId} from Cache for employee {EmployeeId}: {Error}", roleId, employeeId, ex.Message); } } + public async Task ClearAllEmployees() + { + try + { + var response = await _employeeCache.ClearAllEmployeesFromCache(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occured while deleting all employees from Cache"); + } + } // ------------------------------------ Report Cache --------------------------------------- From 079a3804229f6e4e4f18880ddce5d26f4fd9c847 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 17 Jul 2025 17:01:51 +0530 Subject: [PATCH 092/124] Deleted the unused variable --- .../Controllers/ProjectController.cs | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 362c2af..796fd39 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -1,9 +1,6 @@ -using Marco.Pms.DataAccess.Data; -using Marco.Pms.Model.Dtos.Project; +using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Utilities; -using Marco.Pms.Services.Helpers; -using Marco.Pms.Services.Service; using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; @@ -20,30 +17,21 @@ namespace MarcoBMS.Services.Controllers public class ProjectController : ControllerBase { private readonly IProjectServices _projectServices; - private readonly ApplicationDbContext _context; private readonly UserHelper _userHelper; private readonly ILoggingService _logger; private readonly ISignalRService _signalR; - private readonly PermissionServices _permission; - private readonly CacheUpdateHelper _cache; private readonly Guid tenantId; public ProjectController( - ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, ISignalRService signalR, - CacheUpdateHelper cache, - PermissionServices permission, IProjectServices projectServices) { - _context = context; _userHelper = userHelper; _logger = logger; _signalR = signalR; - _cache = cache; - _permission = permission; _projectServices = projectServices; tenantId = userHelper.GetTenantId(); } From 5e84ee9345c8a6db143cd4b456604bc9374e86ee Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 17 Jul 2025 17:16:16 +0530 Subject: [PATCH 093/124] Removed commented code from project Cache --- Marco.Pms.CacheHelper/ProjectCache.cs | 46 ++++++--------------------- Marco.Pms.CacheHelper/ReportCache.cs | 5 +-- 2 files changed, 10 insertions(+), 41 deletions(-) diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index df95419..a9ae3af 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -23,6 +23,8 @@ namespace Marco.Pms.CacheHelper _taskCollection = mongoDB.GetCollection("WorkItemDetails"); } + #region=================================================================== Project Cache Helper =================================================================== + public async Task AddProjectDetailsToCache(ProjectMongoDB projectDetails) { await _projectCollection.InsertOneAsync(projectDetails); @@ -36,7 +38,6 @@ namespace Marco.Pms.CacheHelper await _projectCollection.Indexes.CreateOneAsync(indexModel); } - // The method should focus only on inserting data. public async Task AddProjectDetailsListToCache(List projectDetailsList) { // 1. Add a guard clause to avoid an unnecessary database call for an empty list. @@ -49,7 +50,6 @@ namespace Marco.Pms.CacheHelper await _projectCollection.InsertManyAsync(projectDetailsList); await InitializeCollectionAsync(); } - // A private method to handle the one-time setup of the collection's indexes. private async Task InitializeCollectionAsync() { // 1. Define the TTL (Time-To-Live) index on the 'ExpireAt' field. @@ -135,7 +135,9 @@ namespace Marco.Pms.CacheHelper return result.DeletedCount > 0; } - // ------------------------------------------------------- Project InfraStructure ------------------------------------------------------- + #endregion + + #region=================================================================== Project infrastructure Cache Helper =================================================================== public async Task AddBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId) { @@ -161,11 +163,8 @@ namespace Marco.Pms.CacheHelper if (result.MatchedCount == 0) { - //_logger.LogWarning("Project not found while adding building. ProjectId: {ProjectId}", projectId); return; } - - //_logger.LogInfo("Building {BuildingId} added to project {ProjectId}", building.Id, projectId); return; } @@ -191,11 +190,8 @@ namespace Marco.Pms.CacheHelper if (result.MatchedCount == 0) { - //_logger.LogWarning("Project or building not found while adding floor. ProjectId: {ProjectId}, BuildingId: {BuildingId}", projectId, floor.BuildingId); return; } - - //_logger.LogInfo("Floor {FloorId} added to building {BuildingId} in project {ProjectId}", floor.Id, floor.BuildingId, projectId); return; } @@ -225,16 +221,10 @@ namespace Marco.Pms.CacheHelper if (result.MatchedCount == 0) { - //_logger.LogWarning("Project or nested structure not found while adding work area. ProjectId: {ProjectId}, BuildingId: {BuildingId}, FloorId: {FloorId}", projectId, buildingId, workArea.FloorId); return; } - - //_logger.LogInfo("WorkArea {WorkAreaId} added to floor {FloorId} in building {BuildingId}, ProjectId: {ProjectId}", workArea.Id, workArea.FloorId, buildingId, projectId); return; } - - // Fallback case when no valid data was passed - //_logger.LogWarning("No valid infra data provided to add for ProjectId: {ProjectId}", projectId); } public async Task UpdateBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId) { @@ -257,11 +247,9 @@ namespace Marco.Pms.CacheHelper if (result.MatchedCount == 0) { - //_logger.LogWarning("Update failed: Project or Building not found. ProjectId: {ProjectId}, BuildingId: {BuildingId}", projectId, building.Id); return false; } - //_logger.LogInfo("Building {BuildingId} updated successfully in project {ProjectId}", building.Id, projectId); return true; } @@ -282,11 +270,8 @@ namespace Marco.Pms.CacheHelper if (result.MatchedCount == 0) { - //_logger.LogWarning("Update failed: Project or Floor not found. ProjectId: {ProjectId}, BuildingId: {BuildingId}, FloorId: {FloorId}", projectId, floor.BuildingId, floor.Id); return false; } - - //_logger.LogInfo("Floor {FloorId} updated successfully in Building {BuildingId}, ProjectId: {ProjectId}", floor.Id, floor.BuildingId, projectId); return true; } @@ -308,17 +293,10 @@ namespace Marco.Pms.CacheHelper if (result.MatchedCount == 0) { - //_logger.LogWarning("Update failed: Project or WorkArea not found. ProjectId: {ProjectId}, BuildingId: {BuildingId}, FloorId: {FloorId}, WorkAreaId: {WorkAreaId}", - //projectId, buildingId, workArea.FloorId, workArea.Id); return false; } - - //_logger.LogInfo("WorkArea {WorkAreaId} updated successfully in Floor {FloorId}, Building {BuildingId}, ProjectId: {ProjectId}", - //workArea.Id, workArea.FloorId, buildingId, projectId); return true; } - - //_logger.LogWarning("No update performed. Missing or invalid data for ProjectId: {ProjectId}", projectId); return false; } public async Task?> GetBuildingInfraFromCache(Guid projectId) @@ -333,15 +311,6 @@ namespace Marco.Pms.CacheHelper .Project(p => p.Buildings) .FirstOrDefaultAsync(); - //if (buildings == null) - //{ - // _logger.LogWarning("No building infrastructure found for ProjectId: {ProjectId}", projectId); - //} - //else - //{ - // _logger.LogInfo("Fetched {Count} buildings for ProjectId: {ProjectId}", buildings.Count, projectId); - //} - return buildings; } public async Task UpdatePlannedAndCompleteWorksInBuildingFromCache(Guid workAreaId, double plannedWork, double completedWork) @@ -431,8 +400,9 @@ namespace Marco.Pms.CacheHelper return result; } + #endregion - // ------------------------------------------------------- WorkItem ------------------------------------------------------- + #region=================================================================== WorkItem Cache Helper =================================================================== public async Task> GetWorkItemsByWorkAreaIdsFromCache(List workAreaIds) { @@ -517,5 +487,7 @@ namespace Marco.Pms.CacheHelper var result = await _taskCollection.DeleteOneAsync(filter); return result.DeletedCount > 0; } + + #endregion } } diff --git a/Marco.Pms.CacheHelper/ReportCache.cs b/Marco.Pms.CacheHelper/ReportCache.cs index 76009a4..66611a8 100644 --- a/Marco.Pms.CacheHelper/ReportCache.cs +++ b/Marco.Pms.CacheHelper/ReportCache.cs @@ -1,4 +1,3 @@ -using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.MongoDBModels; using Microsoft.Extensions.Configuration; using MongoDB.Driver; @@ -7,12 +6,10 @@ namespace Marco.Pms.CacheHelper { public class ReportCache { - private readonly ApplicationDbContext _context; private readonly IMongoCollection _projectReportCollection; - public ReportCache(ApplicationDbContext context, IConfiguration configuration) + public ReportCache(IConfiguration configuration) { var connectionString = configuration["MongoDB:ConnectionString"]; - _context = context; var mongoUrl = new MongoUrl(connectionString); var client = new MongoClient(mongoUrl); // Your MongoDB connection string var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name From 3bc51f9cd939bfc21aea6de9ed55d1027dc9fef6 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Sat, 12 Jul 2025 13:13:29 +0530 Subject: [PATCH 094/124] Refactor project report APIs to improve performance and readability --- Marco.Pms.CacheHelper/ReportCache.cs | 45 ++ .../ProjectReportEmailMongoDB.cs | 16 + .../Controllers/ReportController.cs | 530 +++++++++++++++--- Marco.Pms.Services/Service/EmailSender.cs | 26 +- 4 files changed, 523 insertions(+), 94 deletions(-) create mode 100644 Marco.Pms.CacheHelper/ReportCache.cs create mode 100644 Marco.Pms.Model/MongoDBModels/ProjectReportEmailMongoDB.cs diff --git a/Marco.Pms.CacheHelper/ReportCache.cs b/Marco.Pms.CacheHelper/ReportCache.cs new file mode 100644 index 0000000..76009a4 --- /dev/null +++ b/Marco.Pms.CacheHelper/ReportCache.cs @@ -0,0 +1,45 @@ +using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.MongoDBModels; +using Microsoft.Extensions.Configuration; +using MongoDB.Driver; + +namespace Marco.Pms.CacheHelper +{ + public class ReportCache + { + private readonly ApplicationDbContext _context; + private readonly IMongoCollection _projectReportCollection; + public ReportCache(ApplicationDbContext context, IConfiguration configuration) + { + var connectionString = configuration["MongoDB:ConnectionString"]; + _context = context; + var mongoUrl = new MongoUrl(connectionString); + var client = new MongoClient(mongoUrl); // Your MongoDB connection string + var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name + _projectReportCollection = mongoDB.GetCollection("ProjectReportMail"); + } + + /// + /// Retrieves project report emails from the cache based on their sent status. + /// + /// True to get sent reports, false to get unsent reports. + /// A list of ProjectReportEmailMongoDB objects. + public async Task> GetProjectReportMailFromCache(bool isSent) + { + var filter = Builders.Filter.Eq(p => p.IsSent, isSent); + var reports = await _projectReportCollection.Find(filter).ToListAsync(); + return reports; + } + + /// + /// Adds a project report email to the cache. + /// + /// The ProjectReportEmailMongoDB object to add. + /// A Task representing the asynchronous operation. + public async Task AddProjectReportMailToCache(ProjectReportEmailMongoDB report) + { + // Consider adding validation or logging here. + await _projectReportCollection.InsertOneAsync(report); + } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/ProjectReportEmailMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ProjectReportEmailMongoDB.cs new file mode 100644 index 0000000..519ea4f --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/ProjectReportEmailMongoDB.cs @@ -0,0 +1,16 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace Marco.Pms.Model.MongoDBModels +{ + public class ProjectReportEmailMongoDB + { + [BsonId] // Tells MongoDB this is the primary key (_id) + [BsonRepresentation(BsonType.ObjectId)] // Optional: if your _id is ObjectId + public string Id { get; set; } = string.Empty; + public string? Body { get; set; } + public string? Subject { get; set; } + public List? Receivers { get; set; } + public bool IsSent { get; set; } = false; + } +} diff --git a/Marco.Pms.Services/Controllers/ReportController.cs b/Marco.Pms.Services/Controllers/ReportController.cs index 11dec58..717a273 100644 --- a/Marco.Pms.Services/Controllers/ReportController.cs +++ b/Marco.Pms.Services/Controllers/ReportController.cs @@ -1,16 +1,19 @@ -using System.Data; -using Marco.Pms.DataAccess.Data; +using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Dtos.Mail; -using Marco.Pms.Model.Employees; using Marco.Pms.Model.Mail; +using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.Utilities; using Marco.Pms.Services.Helpers; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.CodeAnalysis; using Microsoft.EntityFrameworkCore; using MongoDB.Driver; +using System.Data; +using System.Globalization; +using System.Net.Mail; namespace Marco.Pms.Services.Controllers { @@ -25,7 +28,11 @@ namespace Marco.Pms.Services.Controllers private readonly UserHelper _userHelper; private readonly IWebHostEnvironment _env; private readonly ReportHelper _reportHelper; - public ReportController(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, UserHelper userHelper, IWebHostEnvironment env, ReportHelper reportHelper) + private readonly IConfiguration _configuration; + private readonly CacheUpdateHelper _cache; + private readonly IServiceScopeFactory _serviceScopeFactory; + public ReportController(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, UserHelper userHelper, + IWebHostEnvironment env, ReportHelper reportHelper, IConfiguration configuration, CacheUpdateHelper cache, IServiceScopeFactory serviceScopeFactory) { _context = context; _emailSender = emailSender; @@ -33,27 +40,122 @@ namespace Marco.Pms.Services.Controllers _userHelper = userHelper; _env = env; _reportHelper = reportHelper; + _configuration = configuration; + _cache = cache; + _serviceScopeFactory = serviceScopeFactory; } - [HttpPost("set-mail")] + /// + /// Adds new mail details for a project report. + /// + /// The mail details data. + /// An API response indicating success or failure. + [HttpPost("mail-details")] // More specific route for adding mail details public async Task AddMailDetails([FromBody] MailDetailsDto mailDetailsDto) { + // 1. Get Tenant ID and Basic Authorization Check Guid tenantId = _userHelper.GetTenantId(); - MailDetails mailDetails = new MailDetails + if (tenantId == Guid.Empty) + { + _logger.LogWarning("Authorization Error: Attempt to add mail details with an empty or invalid tenant ID."); + return Unauthorized(ApiResponse.ErrorResponse("Unauthorized", "Tenant ID not found or invalid.", 401)); + } + + // 2. Input Validation (Leverage Model Validation attributes on DTO) + if (mailDetailsDto == null) + { + _logger.LogWarning("Validation Error: MailDetails DTO is null. TenantId: {TenantId}", tenantId); + return BadRequest(ApiResponse.ErrorResponse("Invalid Data", "Request body is empty.", 400)); + } + + // Ensure ProjectId and Recipient are not empty + if (mailDetailsDto.ProjectId == Guid.Empty) + { + _logger.LogWarning("Validation Error: Project ID is empty. TenantId: {TenantId}", tenantId); + return BadRequest(ApiResponse.ErrorResponse("Validation Failed", "Project ID cannot be empty.", 400)); + } + + if (string.IsNullOrWhiteSpace(mailDetailsDto.Recipient)) + { + _logger.LogWarning("Validation Error: Recipient email is empty. ProjectId: {ProjectId}, TenantId: {TenantId}", mailDetailsDto.ProjectId, tenantId); + return BadRequest(ApiResponse.ErrorResponse("Validation Failed", "Recipient email cannot be empty.", 400)); + } + + // Optional: Validate email format using regex or System.Net.Mail.MailAddress + try + { + var mailAddress = new MailAddress(mailDetailsDto.Recipient); + _logger.LogInfo("nothing"); + } + catch (FormatException) + { + _logger.LogWarning("Validation Error: Invalid recipient email format '{Recipient}'. ProjectId: {ProjectId}, TenantId: {TenantId}", mailDetailsDto.Recipient, mailDetailsDto.ProjectId, tenantId); + return BadRequest(ApiResponse.ErrorResponse("Validation Failed", "Invalid recipient email format.", 400)); + } + + // 3. Validate MailListId (Foreign Key Check) + // Ensure the MailListId refers to an existing MailBody (template) within the same tenant. + if (mailDetailsDto.MailListId != Guid.Empty) // Only validate if a MailListId is provided + { + bool mailTemplateExists; + try + { + mailTemplateExists = await _context.MailingList + .AsNoTracking() + .AnyAsync(m => m.Id == mailDetailsDto.MailListId && m.TenantId == tenantId); + } + catch (Exception ex) + { + _logger.LogError("Database Error: Failed to check existence of MailListId '{MailListId}' for TenantId: {TenantId}. : {Error}", mailDetailsDto.MailListId, tenantId, ex.Message); + return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An error occurred while validating mail template.", 500)); + } + + if (!mailTemplateExists) + { + _logger.LogWarning("Validation Error: Provided MailListId '{MailListId}' does not exist or does not belong to TenantId: {TenantId}.", mailDetailsDto.MailListId, tenantId); + return NotFound(ApiResponse.ErrorResponse("Invalid Mail Template", "The specified mail template (MailListId) was not found or accessible.", 404)); + } + } + // If MailListId can be null/empty and implies no specific template, adjust logic accordingly. + // Currently assumes it must exist if provided. + + // 4. Create and Add New Mail Details + var newMailDetails = new MailDetails { ProjectId = mailDetailsDto.ProjectId, Recipient = mailDetailsDto.Recipient, Schedule = mailDetailsDto.Schedule, MailListId = mailDetailsDto.MailListId, - TenantId = tenantId + TenantId = tenantId, }; - _context.MailDetails.Add(mailDetails); - await _context.SaveChangesAsync(); - return Ok("Success"); + + try + { + _context.MailDetails.Add(newMailDetails); + await _context.SaveChangesAsync(); + _logger.LogInfo("Successfully added new mail details with ID {MailDetailsId} for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}.", newMailDetails.Id, newMailDetails.ProjectId, newMailDetails.Recipient, tenantId); + + // 5. Return Success Response (201 Created is ideal for resource creation) + return StatusCode(201, ApiResponse.SuccessResponse( + newMailDetails, // Return the newly created object (or a DTO of it) + "Mail details added successfully.", + 201)); + } + catch (DbUpdateException dbEx) + { + _logger.LogError("Database Error: Failed to save new mail details for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}. : {Error}", newMailDetails.ProjectId, newMailDetails.Recipient, tenantId, dbEx.Message); + // Check for specific constraint violations if applicable (e.g., duplicate recipient for a project) + return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An error occurred while saving the mail details.", 500)); + } + catch (Exception ex) + { + _logger.LogError("Unexpected Error: An unhandled exception occurred while adding mail details for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}. : {Error}", newMailDetails.ProjectId, newMailDetails.Recipient, tenantId, ex.Message); + return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500)); + } } - [HttpPost("mail-template")] - public async Task AddMailTemplate([FromBody] MailTemeplateDto mailTemeplateDto) + [HttpPost("mail-template1")] + public async Task AddMailTemplate1([FromBody] MailTemeplateDto mailTemeplateDto) { Guid tenantId = _userHelper.GetTenantId(); if (string.IsNullOrWhiteSpace(mailTemeplateDto.Body) && string.IsNullOrWhiteSpace(mailTemeplateDto.Title)) @@ -80,116 +182,376 @@ namespace Marco.Pms.Services.Controllers return Ok("Success"); } + /// + /// Adds a new mail template. + /// + /// The mail template data. + /// An API response indicating success or failure. + [HttpPost("mail-template")] // More specific route for adding a template + public async Task AddMailTemplate([FromBody] MailTemeplateDto mailTemplateDto) // Renamed parameter for consistency + { + // 1. Get Tenant ID and Basic Authorization Check + Guid tenantId = _userHelper.GetTenantId(); + if (tenantId == Guid.Empty) + { + _logger.LogWarning("Authorization Error: Attempt to add mail template with an empty or invalid tenant ID."); + return Unauthorized(ApiResponse.ErrorResponse("Unauthorized", "Tenant ID not found or invalid.", 401)); + } + + // 2. Input Validation (Moved to model validation if possible, or keep explicit) + // Use proper model validation attributes ([Required], [StringLength]) on MailTemeplateDto + // and rely on ASP.NET Core's automatic model validation if possible. + // If not, these checks are good. + if (mailTemplateDto == null) + { + _logger.LogWarning("Validation Error: Mail template DTO is null."); + return BadRequest(ApiResponse.ErrorResponse("Invalid Data", "Request body is empty.", 400)); + } + + if (string.IsNullOrWhiteSpace(mailTemplateDto.Title)) + { + _logger.LogWarning("Validation Error: Mail template title is empty or whitespace. TenantId: {TenantId}", tenantId); + return BadRequest(ApiResponse.ErrorResponse("Validation Failed", "Mail template title cannot be empty.", 400)); + } + + // The original logic checked both body and title, but often a template needs at least a title. + // Re-evalute if body can be empty. If so, remove the body check. Assuming title is always mandatory. + // If both body and title are empty, it's definitely invalid. + if (string.IsNullOrWhiteSpace(mailTemplateDto.Body) && string.IsNullOrWhiteSpace(mailTemplateDto.Subject)) + { + _logger.LogWarning("Validation Error: Mail template body and subject are both empty or whitespace for title '{Title}'. TenantId: {TenantId}", mailTemplateDto.Title, tenantId); + return BadRequest(ApiResponse.ErrorResponse("Validation Failed", "Mail template body or subject must be provided.", 400)); + } + + // 3. Check for Existing Template Title (Case-Insensitive) + // Use AsNoTracking() for read-only query + MailingList? existingTemplate; + try + { + existingTemplate = await _context.MailingList + .AsNoTracking() // Important for read-only checks + .FirstOrDefaultAsync(t => t.Title.ToLower() == mailTemplateDto.Title.ToLower() && t.TenantId == tenantId); // IMPORTANT: Filter by TenantId! + } + catch (Exception ex) + { + _logger.LogError("Database Error: Failed to check for existing mail template with title '{Title}' for TenantId: {TenantId}.: {Error}", mailTemplateDto.Title, tenantId, ex.Message); + return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An error occurred while checking for existing templates.", 500)); + } + + + if (existingTemplate != null) + { + _logger.LogWarning("Conflict Error: User tries to add email template with title '{Title}' which already exists for TenantId: {TenantId}.", mailTemplateDto.Title, tenantId); + return Conflict(ApiResponse.ErrorResponse("Conflict", $"Email template with title '{mailTemplateDto.Title}' already exists.", 409)); + } + + // 4. Create and Add New Template + var newMailingList = new MailingList + { + Title = mailTemplateDto.Title, + Body = mailTemplateDto.Body, + Subject = mailTemplateDto.Subject, + Keywords = mailTemplateDto.Keywords, + TenantId = tenantId, + }; + + try + { + _context.MailingList.Add(newMailingList); + await _context.SaveChangesAsync(); + _logger.LogInfo("Successfully added new mail template with ID {TemplateId} and title '{Title}' for TenantId: {TenantId}.", newMailingList.Id, newMailingList.Title, tenantId); + + // 5. Return Success Response (201 Created is ideal for resource creation) + // It's good practice to return the created resource or its ID. + return StatusCode(201, ApiResponse.SuccessResponse( + newMailingList, // Return the newly created object (or a DTO of it) + "Mail template added successfully.", + 201)); + } + catch (DbUpdateException dbEx) + { + _logger.LogError("Database Error: Failed to save new mail template '{Title}' for TenantId: {TenantId}. : {Error}", mailTemplateDto.Title, tenantId, dbEx.Message); + return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An error occurred while saving the mail template.", 500)); + } + catch (Exception ex) + { + _logger.LogError("Unexpected Error: An unhandled exception occurred while adding mail template '{Title}' for TenantId: {TenantId}. : {Error}", mailTemplateDto.Title, tenantId, ex.Message); + return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500)); + } + } + [HttpGet("project-statistics")] public async Task SendProjectReport() { Guid tenantId = _userHelper.GetTenantId(); - // Use AsNoTracking() for read-only queries to improve performance - List mailDetails = await _context.MailDetails + // 1. OPTIMIZATION: Perform grouping and projection on the database server. + // This is far more efficient than loading all entities into memory. + var projectMailGroups = await _context.MailDetails .AsNoTracking() - .Include(m => m.MailBody) .Where(m => m.TenantId == tenantId) - .ToListAsync(); - - int successCount = 0; - int notFoundCount = 0; - int invalidIdCount = 0; - - var groupedMails = mailDetails .GroupBy(m => new { m.ProjectId, m.MailListId }) .Select(g => new { ProjectId = g.Key.ProjectId, - MailListId = g.Key.MailListId, Recipients = g.Select(m => m.Recipient).Distinct().ToList(), - MailBody = g.FirstOrDefault()?.MailBody?.Body ?? "", - Subject = g.FirstOrDefault()?.MailBody?.Subject ?? string.Empty, + // Project the mail body and subject from the first record in the group + MailInfo = g.Select(m => new { Body = m.MailBody != null ? m.MailBody.Body : "", Subject = m.MailBody != null ? m.MailBody.Subject : "" }).FirstOrDefault() }) - .ToList(); + .ToListAsync(); - var semaphore = new SemaphoreSlim(1); - - // Using Task.WhenAll to send reports concurrently for better performance - var sendTasks = groupedMails.Select(async mailDetail => + if (!projectMailGroups.Any()) { - await semaphore.WaitAsync(); - try + return Ok(ApiResponse.SuccessResponse(new { }, "No projects found to send reports for.", 200)); + } + + int successCount = 0; + int notFoundCount = 0; + int invalidIdCount = 0; + int failureCount = 0; + + // 2. OPTIMIZATION: Use true concurrency by removing SemaphoreSlim(1) + // and giving each task its own isolated set of services (including DbContext). + var sendTasks = projectMailGroups.Select(async mailGroup => + { + // SOLUTION: Create a new Dependency Injection scope for each parallel task. + using (var scope = _serviceScopeFactory.CreateScope()) { - var response = await GetProjectStatistics(mailDetail.ProjectId, mailDetail.Recipients, mailDetail.MailBody, mailDetail.Subject, tenantId); - if (response.StatusCode == 200) - Interlocked.Increment(ref successCount); - else if (response.StatusCode == 404) - Interlocked.Increment(ref notFoundCount); - else if (response.StatusCode == 400) - Interlocked.Increment(ref invalidIdCount); - } - finally - { - semaphore.Release(); + // Resolve a new instance of the helper from this isolated scope. + // This ensures each task gets its own thread-safe DbContext. + var reportHelper = scope.ServiceProvider.GetRequiredService(); + + try + { + // Ensure MailInfo and ProjectId are valid before proceeding + if (mailGroup.MailInfo == null || mailGroup.ProjectId == Guid.Empty) + { + Interlocked.Increment(ref invalidIdCount); + return; + } + + var response = await reportHelper.GetProjectStatistics( + mailGroup.ProjectId, + mailGroup.Recipients, + mailGroup.MailInfo.Body, + mailGroup.MailInfo.Subject, + tenantId); + + // Use a switch expression for cleaner counting + switch (response.StatusCode) + { + case 200: Interlocked.Increment(ref successCount); break; + case 404: Interlocked.Increment(ref notFoundCount); break; + case 400: Interlocked.Increment(ref invalidIdCount); break; + default: Interlocked.Increment(ref failureCount); break; + } + } + catch (Exception ex) + { + // 3. OPTIMIZATION: Make the process resilient. + // If one task fails unexpectedly, log it and continue with others. + _logger.LogError("Failed to send report for project {ProjectId} : {Error}", mailGroup.ProjectId, ex.Message); + Interlocked.Increment(ref failureCount); + } } }).ToList(); await Task.WhenAll(sendTasks); - //var response = await GetProjectStatistics(Guid.Parse("2618eb89-2823-11f0-9d9e-bc241163f504"), "ashutosh.nehete@marcoaiot.com", tenantId); + var summaryMessage = $"Processing complete. Success: {successCount}, Not Found: {notFoundCount}, Invalid ID: {invalidIdCount}, Failures: {failureCount}."; _logger.LogInfo( - "Emails of project reports sent for tenant {TenantId}. Successfully sent: {SuccessCount}, Projects not found: {NotFoundCount}, Invalid IDs: {InvalidIdsCount}", - tenantId, successCount, notFoundCount, invalidIdCount); + "Project report sending complete for tenant {TenantId}. Success: {SuccessCount}, Not Found: {NotFoundCount}, Invalid ID: {InvalidIdCount}, Failures: {FailureCount}", + tenantId, successCount, notFoundCount, invalidIdCount, failureCount); return Ok(ApiResponse.SuccessResponse( - new { }, - $"Reports sent successfully: {successCount}. Projects not found: {notFoundCount}. Invalid IDs: {invalidIdCount}.", + new { successCount, notFoundCount, invalidIdCount, failureCount }, + summaryMessage, 200)); } - /// - /// 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. - private async Task> GetProjectStatistics(Guid projectId, List recipientEmails, string body, string subject, Guid tenantId) + + //[HttpPost("add-report-mail1")] + //public async Task StoreProjectStatistics1() + //{ + + // Guid tenantId = _userHelper.GetTenantId(); + + // // Use AsNoTracking() for read-only queries to improve performance + // List mailDetails = await _context.MailDetails + // .AsNoTracking() + // .Include(m => m.MailBody) + // .Where(m => m.TenantId == tenantId) + // .ToListAsync(); + + // var groupedMails = mailDetails + // .GroupBy(m => new { m.ProjectId, m.MailListId }) + // .Select(g => new + // { + // ProjectId = g.Key.ProjectId, + // MailListId = g.Key.MailListId, + // Recipients = g.Select(m => m.Recipient).Distinct().ToList(), + // MailBody = g.FirstOrDefault()?.MailBody?.Body ?? "", + // Subject = g.FirstOrDefault()?.MailBody?.Subject ?? string.Empty, + // }) + // .ToList(); + // foreach (var groupMail in groupedMails) + // { + // var projectId = groupMail.ProjectId; + // var body = groupMail.MailBody; + // var subject = groupMail.Subject; + // var receivers = groupMail.Recipients; + // if (projectId == Guid.Empty) + // { + // _logger.LogError("Provided empty project ID while fetching project report."); + // return NotFound(ApiResponse.ErrorResponse("Provided empty Project ID.", "Provided empty Project ID.", 400)); + // } + + + // var statisticReport = await _reportHelper.GetDailyProjectReport(projectId, tenantId); + + // if (statisticReport == null) + // { + // _logger.LogWarning("User attempted to fetch project progress for project ID {ProjectId} but not found.", projectId); + // return NotFound(ApiResponse.ErrorResponse("Project not found.", "Project not found.", 404)); + // } + // var date = statisticReport.Date.ToString("dd-MMM-yyyy", CultureInfo.InvariantCulture); + + // // Send Email + // var emailBody = await _emailSender.SendProjectStatisticsEmail(new List(), body, subject, statisticReport); + // var subjectReplacements = new Dictionary + // { + // {"DATE", date }, + // {"PROJECT_NAME", statisticReport.ProjectName} + // }; + // foreach (var item in subjectReplacements) + // { + // subject = subject.Replace($"{{{{{item.Key}}}}}", item.Value); + // } + // string env = _configuration["environment:Title"] ?? string.Empty; + // if (string.IsNullOrWhiteSpace(env)) + // { + // subject = $"{subject}"; + // } + // else + // { + // subject = $"({env}) {subject}"; + // } + // var mail = new ProjectReportEmailMongoDB + // { + // IsSent = false, + // Body = emailBody, + // Receivers = receivers, + // Subject = subject, + // }; + // await _cache.AddProjectReportMail(mail); + // } + // return Ok(ApiResponse.SuccessResponse("Project Report Mail is stored in MongoDB", "Project Report Mail is stored in MongoDB", 200)); + //} + + [HttpPost("add-report-mail")] + public async Task StoreProjectStatistics() { + Guid tenantId = _userHelper.GetTenantId(); - if (projectId == Guid.Empty) + // 1. Database-Side Grouping (Still the most efficient way to get initial data) + var projectMailGroups = await _context.MailDetails + .AsNoTracking() + .Where(m => m.TenantId == tenantId && m.ProjectId != Guid.Empty) + .GroupBy(m => new { m.ProjectId, m.MailListId }) + .Select(g => new + { + g.Key.ProjectId, + Recipients = g.Select(m => m.Recipient).Distinct().ToList(), + MailInfo = g.Select(m => new { Body = m.MailBody != null ? m.MailBody.Body : "", Subject = m.MailBody != null ? m.MailBody.Subject : "" }).FirstOrDefault() + }) + .ToListAsync(); + + if (!projectMailGroups.Any()) { - _logger.LogError("Provided empty project ID while fetching project report."); - return ApiResponse.ErrorResponse("Provided empty Project ID.", "Provided empty Project ID.", 400); + _logger.LogInfo("No project mail details found for tenant {TenantId} to process.", tenantId); + return Ok(ApiResponse.SuccessResponse("No project reports to generate.", "No project reports to generate.", 200)); } + string env = _configuration["environment:Title"] ?? string.Empty; - var statisticReport = await _reportHelper.GetDailyProjectReport(projectId, tenantId); - - if (statisticReport == null) + // 2. Process each group concurrently, but with isolated DBContexts. + var processingTasks = projectMailGroups.Select(async group => { - _logger.LogWarning("User attempted to fetch project progress for project ID {ProjectId} but not found.", projectId); - return ApiResponse.ErrorResponse("Project not found.", "Project not found.", 404); - } + // SOLUTION: Create a new DI scope for each parallel task. + using (var scope = _serviceScopeFactory.CreateScope()) + { + // Resolve services from this new, isolated scope. + // These helpers will get their own fresh DbContext instance. + var reportHelper = scope.ServiceProvider.GetRequiredService(); + var emailSender = scope.ServiceProvider.GetRequiredService(); + var cache = scope.ServiceProvider.GetRequiredService(); // e.g., IProjectReportCache - // Send Email - var emailBody = await _emailSender.SendProjectStatisticsEmail(recipientEmails, body, subject, statisticReport); - var employee = await _context.Employees.FirstOrDefaultAsync(e => e.Email != null && recipientEmails.Contains(e.Email)) ?? new Employee(); - - List mailLogs = new List(); - foreach (var recipientEmail in recipientEmails) - { - mailLogs.Add( - new MailLog + // The rest of the logic is the same, but now it's thread-safe. + try { - ProjectId = projectId, - EmailId = recipientEmail, - Body = emailBody, - EmployeeId = employee.Id, - TimeStamp = DateTime.UtcNow, - TenantId = tenantId - }); + var projectId = group.ProjectId; + var statisticReport = await reportHelper.GetDailyProjectReport(projectId, tenantId); + + if (statisticReport == null) + { + _logger.LogWarning("Statistic report for project ID {ProjectId} not found. Skipping.", projectId); + return; + } + + if (group.MailInfo == null) + { + _logger.LogWarning("MailBody info for project ID {ProjectId} not found. Skipping.", projectId); + return; + } + + var date = statisticReport.Date.ToString("dd-MMM-yyyy", CultureInfo.InvariantCulture); + // Assuming the first param to SendProjectStatisticsEmail was just a placeholder + var emailBody = await emailSender.SendProjectStatisticsEmail(new List(), group.MailInfo.Body, string.Empty, statisticReport); + + string subject = group.MailInfo.Subject + .Replace("{{DATE}}", date) + .Replace("{{PROJECT_NAME}}", statisticReport.ProjectName); + + subject = string.IsNullOrWhiteSpace(env) ? subject : $"({env}) {subject}"; + + var mail = new ProjectReportEmailMongoDB + { + IsSent = false, + Body = emailBody, + Receivers = group.Recipients, + Subject = subject, + }; + + await cache.AddProjectReportMail(mail); + } + catch (Exception ex) + { + // It's good practice to log any unexpected errors within a concurrent task. + _logger.LogError("Failed to process project report for ProjectId {ProjectId} : {Error}", group.ProjectId, ex.Message); + } + } + }); + + // Await all the concurrent, now thread-safe, tasks. + await Task.WhenAll(processingTasks); + + return Ok(ApiResponse.SuccessResponse( + $"{projectMailGroups.Count} Project Report Mail(s) are queued for storage.", + "Project Report Mail processing initiated.", + 200)); + } + + + [HttpGet("report-mail")] + public async Task GetProjectStatisticsFromCache() + { + var mailList = await _cache.GetProjectReportMail(false); + if (mailList == null) + { + return NotFound(ApiResponse.ErrorResponse("Not mail found", "Not mail found", 404)); } - _context.MailLogs.AddRange(mailLogs); - - await _context.SaveChangesAsync(); - return ApiResponse.SuccessResponse(statisticReport, "Email sent successfully", 200); + return Ok(ApiResponse.SuccessResponse(mailList, "Fetched list of mail body successfully", 200)); } } } diff --git a/Marco.Pms.Services/Service/EmailSender.cs b/Marco.Pms.Services/Service/EmailSender.cs index 568510a..4d66a4f 100644 --- a/Marco.Pms.Services/Service/EmailSender.cs +++ b/Marco.Pms.Services/Service/EmailSender.cs @@ -150,18 +150,24 @@ namespace MarcoBMS.Services.Service emailBody = emailBody.Replace("{{TEAM_ON_SITE}}", BuildTeamOnSiteHtml(report.TeamOnSite)); emailBody = emailBody.Replace("{{PERFORMED_TASK}}", BuildPerformedTaskHtml(report.PerformedTasks, report.Date)); emailBody = emailBody.Replace("{{PERFORMED_ATTENDANCE}}", BuildPerformedAttendanceHtml(report.PerformedAttendance)); - var subjectReplacements = new Dictionary + if (!string.IsNullOrWhiteSpace(subject)) { - {"DATE", date }, - {"PROJECT_NAME", report.ProjectName} - }; - foreach (var item in subjectReplacements) - { - subject = subject.Replace($"{{{{{item.Key}}}}}", item.Value); + var subjectReplacements = new Dictionary + { + {"DATE", date }, + {"PROJECT_NAME", report.ProjectName} + }; + foreach (var item in subjectReplacements) + { + subject = subject.Replace($"{{{{{item.Key}}}}}", item.Value); + } + string env = _configuration["environment:Title"] ?? string.Empty; + subject = CheckSubject(subject); + } + if (toEmails.Count > 0) + { + await SendEmailAsync(toEmails, subject, emailBody); } - string env = _configuration["environment:Title"] ?? string.Empty; - subject = CheckSubject(subject); - await SendEmailAsync(toEmails, subject, emailBody); return emailBody; } public async Task SendOTP(List toEmails, string emailBody, string name, string otp, string subject) From 7d160a9a52f47d0b26c4c88dbcea60ccc18c9364 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Sat, 12 Jul 2025 13:14:15 +0530 Subject: [PATCH 095/124] Refactored the function to add project in cache and added auto Mapper --- Marco.Pms.CacheHelper/ProjectCache.cs | 135 +----- .../Controllers/AttendanceController.cs | 28 +- .../Controllers/DashboardController.cs | 2 +- .../Controllers/EmployeeController.cs | 9 +- .../Controllers/ImageController.cs | 6 +- .../Controllers/ProjectController.cs | 286 ++++++++---- .../Helpers/CacheUpdateHelper.cs | 432 +++++++++++++++++- Marco.Pms.Services/Helpers/ProjectsHelper.cs | 77 +--- Marco.Pms.Services/Helpers/ReportHelper.cs | 99 +++- .../MappingProfiles/ProjectMappingProfile.cs | 30 ++ Marco.Pms.Services/Marco.Pms.Services.csproj | 1 + Marco.Pms.Services/Program.cs | 269 ++++++----- Marco.Pms.Services/Service/ILoggingService.cs | 5 +- Marco.Pms.Services/Service/LoggingServices.cs | 18 +- .../Service/PermissionServices.cs | 40 +- 15 files changed, 958 insertions(+), 479 deletions(-) create mode 100644 Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs 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); } } } From c359212ee5753b5c0972bc27635625f19cf55094 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 14 Jul 2025 12:02:45 +0530 Subject: [PATCH 096/124] Optimized both get Project list API and get Project list basic API --- Marco.Pms.CacheHelper/ProjectCache.cs | 43 +- .../Controllers/ProjectController.cs | 727 +++++++++--------- .../Controllers/UserController.cs | 2 +- .../Helpers/CacheUpdateHelper.cs | 43 +- Marco.Pms.Services/Helpers/ProjectsHelper.cs | 2 +- Marco.Pms.Services/Helpers/RolesHelper.cs | 121 ++- .../Service/PermissionServices.cs | 2 +- 7 files changed, 513 insertions(+), 427 deletions(-) diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index 1fd36f4..183bbc4 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -32,20 +32,9 @@ namespace Marco.Pms.CacheHelper 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) + public async Task UpdateProjectDetailsOnlyToCache(Project project, StatusMaster projectStatus) { - //_logger.LogInfo("Starting update for project: {ProjectId}", project.Id); - - var projectStatus = await _context.StatusMasters - .FirstOrDefaultAsync(s => s.Id == project.ProjectStatusId); - - if (projectStatus == null) - { - //_logger.LogWarning("StatusMaster not found for ProjectStatusId: {StatusId}", project.ProjectStatusId); - } - // Build the update definition var updates = Builders.Update.Combine( Builders.Update.Set(r => r.Name, project.Name), @@ -69,11 +58,9 @@ namespace Marco.Pms.CacheHelper if (result.MatchedCount == 0) { - //_logger.LogWarning("No project matched in MongoDB for update. ProjectId: {ProjectId}", project.Id); return false; } - //_logger.LogInfo("Project {ProjectId} successfully updated in MongoDB", project.Id); return true; } public async Task GetProjectDetailsFromCache(Guid projectId) @@ -83,21 +70,12 @@ namespace Marco.Pms.CacheHelper var filter = Builders.Filter.Eq(p => p.Id, projectId.ToString()); var projection = Builders.Projection.Exclude(p => p.Buildings); - //_logger.LogInfo("Fetching project details for ProjectId: {ProjectId} from MongoDB", projectId); - // Perform query var project = await _projetCollection .Find(filter) .Project(projection) .FirstOrDefaultAsync(); - if (project == null) - { - //_logger.LogWarning("No project found in MongoDB for ProjectId: {ProjectId}", projectId); - return null; - } - - //_logger.LogInfo("Successfully fetched project details (excluding Buildings) for ProjectId: {ProjectId}", projectId); return project; } public async Task> GetProjectDetailsListFromCache(List projectIds) @@ -111,6 +89,12 @@ namespace Marco.Pms.CacheHelper .ToListAsync(); return projects; } + public async Task DeleteProjectByIdFromCacheAsync(Guid projectId) + { + var filter = Builders.Filter.Eq(e => e.Id, projectId.ToString()); + var result = await _projetCollection.DeleteOneAsync(filter); + return result.DeletedCount > 0; + } // ------------------------------------------------------- Project InfraStructure ------------------------------------------------------- @@ -407,6 +391,10 @@ namespace Marco.Pms.CacheHelper return null; return result; } + + + // ------------------------------------------------------- WorkItem ------------------------------------------------------- + public async Task> GetWorkItemsByWorkAreaIdsFromCache(List workAreaIds) { var stringWorkAreaIds = workAreaIds.Select(wa => wa.ToString()).ToList(); @@ -418,9 +406,6 @@ namespace Marco.Pms.CacheHelper return workItems; } - - // ------------------------------------------------------- WorkItem ------------------------------------------------------- - public async Task ManageWorkItemDetailsToCache(List workItems) { var activityIds = workItems.Select(wi => wi.ActivityId).ToList(); @@ -510,5 +495,11 @@ namespace Marco.Pms.CacheHelper } return false; } + public async Task DeleteWorkItemByIdFromCacheAsync(Guid workItemId) + { + var filter = Builders.Filter.Eq(e => e.Id, workItemId.ToString()); + var result = await _taskCollection.DeleteOneAsync(filter); + return result.DeletedCount > 0; + } } } diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 29f9d04..adb5887 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -28,10 +28,10 @@ namespace MarcoBMS.Services.Controllers [Authorize] public class ProjectController : ControllerBase { + private readonly IDbContextFactory _dbContextFactory; private readonly ApplicationDbContext _context; private readonly UserHelper _userHelper; private readonly ILoggingService _logger; - //private readonly RolesHelper _rolesHelper; private readonly ProjectsHelper _projectsHelper; private readonly IHubContext _signalR; private readonly PermissionServices _permission; @@ -40,13 +40,13 @@ namespace MarcoBMS.Services.Controllers private readonly Guid tenantId; - public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, ProjectsHelper projectHelper, - IHubContext signalR, PermissionServices permission, CacheUpdateHelper cache, IMapper mapper) + public ProjectController(IDbContextFactory dbContextFactory, ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, + ProjectsHelper projectHelper, IHubContext signalR, PermissionServices permission, CacheUpdateHelper cache, IMapper mapper) { + _dbContextFactory = dbContextFactory; _context = context; _userHelper = userHelper; _logger = logger; - //_rolesHelper = rolesHelper; _projectsHelper = projectHelper; _signalR = signalR; _cache = cache; @@ -55,55 +55,10 @@ namespace MarcoBMS.Services.Controllers tenantId = _userHelper.GetTenantId(); } - [HttpGet("list/basic1")] - public async Task GetAllProjects1() - { - if (!ModelState.IsValid) - { - var errors = ModelState.Values - .SelectMany(v => v.Errors) - .Select(e => e.ErrorMessage) - .ToList(); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); - - } - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - // Defensive check for null employee (important for robust APIs) - if (LoggedInEmployee == null) - { - return Unauthorized(ApiResponse.ErrorResponse("Employee not found.", null, 401)); - } - - List response = new List(); - List projectIds = 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(); - - //} - 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)); - } + #region =================================================================== Project Get APIs =================================================================== [HttpGet("list/basic")] - public async Task GetAllProjects() // Renamed for clarity + public async Task GetAllProjectsBasic() { // Step 1: Get the current user var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); @@ -133,146 +88,82 @@ namespace MarcoBMS.Services.Controllers } /// - /// 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. + /// Retrieves a list of projects accessible to the current user, including aggregated details. + /// This method is optimized to use a cache-first approach. If data is not in the cache, + /// it fetches and aggregates data efficiently from the database in parallel. /// - /// 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; - } + /// An ApiResponse containing a list of projects or an error. [HttpGet("list")] - public async Task GetAll() + public async Task GetAllProjects() { + // --- Step 1: Input Validation and Initial Setup --- if (!ModelState.IsValid) { var errors = ModelState.Values .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); - + _logger.LogWarning("GetAllProjects called with invalid model state. Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - //List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(LoggedInEmployee.Id); - //string[] projectsId = []; - //List projects = new List(); - ///* User with permission manage project can see all projects */ - //if (featurePermission != null && featurePermission.Exists(c => c.Id.ToString() == "172fc9b6-755b-4f62-ab26-55c34a330614")) - //{ - // projects = await _projectsHelper.GetAllProjectByTanentID(LoggedInEmployee.TenantId); - //} - //else - //{ - // List allocation = await _projectsHelper.GetProjectByEmployeeID(LoggedInEmployee.Id); - // projectsId = allocation.Select(c => c.ProjectId.ToString()).ToArray(); - // projects = await _context.Projects.Where(c => projectsId.Contains(c.Id.ToString()) && c.TenantId == tenantId).ToListAsync(); - //} - - //List projects = await _projectsHelper.GetMyProjects(tenantId, LoggedInEmployee); - ////List projects = new List(); - /// - List response = new List(); - List projectIds = await _projectsHelper.GetMyProjects(tenantId, LoggedInEmployee); - - var projectsDetails = await _cache.GetProjectDetailsList(projectIds); - if (projectsDetails == null) + try { - List projects = await _context.Projects.Where(p => projectIds.Contains(p.Id)).ToListAsync(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + _logger.LogInfo("Starting GetAllProjects for TenantId: {TenantId}, User: {UserId}", tenantId, loggedInEmployee.Id); - var teams = await _context.ProjectAllocations.Where(p => p.TenantId == tenantId && projectIds.Contains(p.ProjectId) && p.IsActive == true).ToListAsync(); - - - List allBuildings = await _context.Buildings.Where(b => projectIds.Contains(b.ProjectId) && b.TenantId == tenantId).ToListAsync(); - List idList = allBuildings.Select(b => b.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 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) + // --- Step 2: Get a list of project IDs the user can access --- + List projectIds = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); + if (!projectIds.Any()) { - 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); + _logger.LogInfo("User has no assigned projects. Returning empty list."); + return Ok(ApiResponse>.SuccessResponse(new List(), "No projects found for the current user.", 200)); } - } - else - { - response = projectsDetails.Select(p => _mapper.Map(p)).ToList(); - } - return Ok(ApiResponse.SuccessResponse(response, "Success.", 200)); + // --- Step 3: Efficiently handle partial cache hits --- + _logger.LogInfo("Attempting to fetch details for {ProjectCount} projects from cache.", projectIds.Count); + + // Fetch what we can from the cache. + var cachedDetails = await _cache.GetProjectDetailsList(projectIds) ?? new List(); + var cachedDictionary = cachedDetails.ToDictionary(p => Guid.Parse(p.Id)); + + // Identify which projects are missing from the cache. + var missingIds = projectIds.Where(id => !cachedDictionary.ContainsKey(id)).ToList(); + + // Start building the response with the items we found in the cache. + var responseVms = _mapper.Map>(cachedDictionary.Values); + + if (missingIds.Any()) + { + // --- Step 4: Fetch ONLY the missing items from the database --- + _logger.LogInfo("Cache partial MISS. Found {CachedCount}, fetching {MissingCount} projects from DB.", + cachedDictionary.Count, missingIds.Count); + + // Call our dedicated data-fetching method for the missing IDs. + var newMongoDetails = await FetchAndBuildProjectDetails(missingIds, tenantId); + + if (newMongoDetails.Any()) + { + // Map the newly fetched items and add them to our response list. + responseVms.AddRange(newMongoDetails); + } + } + else + { + _logger.LogInfo("Cache HIT. All {ProjectCount} projects found in cache.", projectIds.Count); + } + + // --- Step 5: Return the combined result --- + _logger.LogInfo("Successfully retrieved a total of {ProjectCount} projects.", responseVms.Count); + return Ok(ApiResponse>.SuccessResponse(responseVms, "Projects retrieved successfully.", 200)); + } + catch (Exception ex) + { + // --- Step 6: Graceful Error Handling --- + _logger.LogError("An unexpected error occurred in GetAllProjects for tenant {TenantId}. : {Error}", tenantId, ex.Message); + return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred. Please try again later.", null, 500)); + } } [HttpGet("get/{id}")] @@ -351,23 +242,6 @@ namespace MarcoBMS.Services.Controllers { 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) @@ -486,40 +360,9 @@ namespace MarcoBMS.Services.Controllers } - private async Task GetProjectViewModel(Guid? id, Project project) - { - ProjectDetailsVM vm = new ProjectDetailsVM(); + #endregion - // List buildings = _unitOfWork.Building.GetAll(c => c.ProjectId == id).ToList(); - List buildings = await _context.Buildings.Where(c => c.ProjectId == id).ToListAsync(); - List idList = buildings.Select(o => o.Id).ToList(); - // List floors = _unitOfWork.Floor.GetAll(c => idList.Contains(c.Id)).ToList(); - List floors = await _context.Floor.Where(c => idList.Contains(c.BuildingId)).ToListAsync(); - idList = floors.Select(o => o.Id).ToList(); - //List workAreas = _unitOfWork.WorkArea.GetAll(c => idList.Contains(c.Id), includeProperties: "WorkItems,WorkItems.ActivityMaster").ToList(); - - List workAreas = await _context.WorkAreas.Where(c => idList.Contains(c.FloorId)).ToListAsync(); - - idList = workAreas.Select(o => o.Id).ToList(); - List workItems = await _context.WorkItems.Include(c => c.WorkCategoryMaster).Where(c => idList.Contains(c.WorkAreaId)).Include(c => c.ActivityMaster).ToListAsync(); - // List workItems = _unitOfWork.WorkItem.GetAll(c => idList.Contains(c.WorkAreaId), includeProperties: "ActivityMaster").ToList(); - idList = workItems.Select(t => t.Id).ToList(); - List tasks = await _context.TaskAllocations.Where(t => idList.Contains(t.WorkItemId) && t.AssignmentDate.Date == DateTime.UtcNow.Date).ToListAsync(); - vm.project = project; - vm.buildings = buildings; - vm.floors = floors; - vm.workAreas = workAreas; - vm.workItems = workItems; - vm.Tasks = tasks; - return vm; - } - - private Guid GetTenantId() - { - return _userHelper.GetTenantId(); - //var tenant = User.FindFirst("TenantId")?.Value; - //return (tenant != null ? Convert.ToInt32(tenant) : 1); - } + #region =================================================================== Project Manage APIs =================================================================== [HttpPost] public async Task Create([FromBody] CreateProjectDto projectDto) @@ -619,50 +462,9 @@ namespace MarcoBMS.Services.Controllers } } - //[HttpPost("assign-employee")] - //public async Task AssignEmployee(int? allocationid, int employeeId, int projectId) - //{ - // var employee = await _context.Employees.FindAsync(employeeId); - // var project = _projectrepo.Get(c => c.Id == projectId); - // if (employee == null || project == null) - // { - // return NotFound(); - // } + #endregion - // // Logic to add the product to a new table (e.g., selected products) - - // if (allocationid == null) - // { - // // Add allocation - // ProjectAllocation allocation = new ProjectAllocation() - // { - // EmployeeId = employeeId, - // ProjectId = project.Id, - // AllocationDate = DateTime.UtcNow, - // //EmployeeRole = employee.Rol - // TenantId = project.TenantId - // }; - - // _unitOfWork.ProjectAllocation.CreateAsync(allocation); - // } - // else - // { - // //remove allocation - // var allocation = await _context.ProjectAllocations.FindAsync(allocationid); - // if (allocation != null) - // { - // allocation.ReAllocationDate = DateTime.UtcNow; - - // _unitOfWork.ProjectAllocation.UpdateAsync(allocation.Id, allocation); - // } - // else - // { - // return NotFound(); - // } - // } - - // return Ok(); - //} + #region =================================================================== Project Allocation APIs =================================================================== [HttpGet] [Route("employees/get/{projectid?}/{includeInactive?}")] @@ -838,6 +640,134 @@ namespace MarcoBMS.Services.Controllers } + [HttpGet("assigned-projects/{employeeId}")] + public async Task GetProjectsByEmployee([FromRoute] Guid employeeId) + { + + Guid tenantId = _userHelper.GetTenantId(); + if (employeeId == Guid.Empty) + { + return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Employee id not valid.", 400)); + } + + List projectList = await _context.ProjectAllocations + .Where(c => c.TenantId == tenantId && c.EmployeeId == employeeId && c.IsActive) + .Select(c => c.ProjectId).Distinct() + .ToListAsync(); + + if (!projectList.Any()) + { + return NotFound(ApiResponse.SuccessResponse(new List(), "No projects found.", 200)); + } + + + List projectlist = await _context.Projects + .Where(p => projectList.Contains(p.Id)) + .ToListAsync(); + + List projects = new List(); + + + foreach (var project in projectlist) + { + + projects.Add(project.ToProjectListVMFromProject()); + } + + + + return Ok(ApiResponse.SuccessResponse(projects, "Success.", 200)); + } + + [HttpPost("assign-projects/{employeeId}")] + public async Task AssigneProjectsToEmployee([FromBody] List projectAllocationDtos, [FromRoute] Guid employeeId) + { + if (projectAllocationDtos != null && employeeId != Guid.Empty) + { + Guid TenentID = GetTenantId(); + var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + List? result = new List(); + List projectIds = new List(); + + foreach (var projectAllocationDto in projectAllocationDtos) + { + try + { + ProjectAllocation projectAllocation = projectAllocationDto.ToProjectAllocationFromProjectsAllocationDto(TenentID, employeeId); + ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.ProjectId == projectAllocationDto.ProjectId && c.ReAllocationDate == null && c.TenantId == TenentID).SingleOrDefaultAsync(); + + if (projectAllocationFromDb != null) + { + + + _context.ProjectAllocations.Attach(projectAllocationFromDb); + + if (projectAllocationDto.Status) + { + projectAllocationFromDb.JobRoleId = projectAllocation.JobRoleId; ; + projectAllocationFromDb.IsActive = true; + _context.Entry(projectAllocationFromDb).Property(e => e.JobRoleId).IsModified = true; + _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; + } + else + { + projectAllocationFromDb.ReAllocationDate = DateTime.UtcNow; + projectAllocationFromDb.IsActive = false; + _context.Entry(projectAllocationFromDb).Property(e => e.ReAllocationDate).IsModified = true; + _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; + + projectIds.Add(projectAllocation.ProjectId); + } + await _context.SaveChangesAsync(); + var result1 = new + { + Id = projectAllocationFromDb.Id, + EmployeeId = projectAllocation.EmployeeId, + JobRoleId = projectAllocation.JobRoleId, + IsActive = projectAllocation.IsActive, + ProjectId = projectAllocation.ProjectId, + AllocationDate = projectAllocation.AllocationDate, + ReAllocationDate = projectAllocation.ReAllocationDate, + TenantId = projectAllocation.TenantId + }; + result.Add(result1); + } + else + { + projectAllocation.AllocationDate = DateTime.Now; + projectAllocation.IsActive = true; + _context.ProjectAllocations.Add(projectAllocation); + await _context.SaveChangesAsync(); + + projectIds.Add(projectAllocation.ProjectId); + + } + + + } + catch (Exception ex) + { + + return Ok(ApiResponse.ErrorResponse(ex.Message, ex, 400)); + } + } + await _cache.ClearAllProjectIds(employeeId); + var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeId = employeeId }; + + await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + + return Ok(ApiResponse.SuccessResponse(result, "Data saved successfully", 200)); + } + else + { + return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "All Field is required", 400)); + } + + } + + #endregion + + #region =================================================================== Project InfraStructure Get APIs =================================================================== [HttpGet("infra-details/{projectId}")] public async Task GetInfraDetails(Guid projectId) @@ -1026,6 +956,10 @@ namespace MarcoBMS.Services.Controllers return Ok(ApiResponse.SuccessResponse(workItemVMs, $"{workItemVMs.Count} records of tasks fetched successfully", 200)); } + #endregion + + #region =================================================================== Project Infrastructre Manage APIs =================================================================== + [HttpPost("task")] public async Task CreateProjectTask(List workItemDtos) { @@ -1309,131 +1243,172 @@ namespace MarcoBMS.Services.Controllers } - [HttpGet("assigned-projects/{employeeId}")] - public async Task GetProjectsByEmployee([FromRoute] Guid employeeId) + #endregion + + #region =================================================================== Helper Functions =================================================================== + + /// + /// 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); - Guid tenantId = _userHelper.GetTenantId(); - if (employeeId == Guid.Empty) + _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 BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Employee id not valid.", 400)); + return finalViewModels; } - List projectList = await _context.ProjectAllocations - .Where(c => c.TenantId == tenantId && c.EmployeeId == employeeId && c.IsActive) - .Select(c => c.ProjectId).Distinct() - .ToListAsync(); + var cachedIds = cachedMongoDocs.Select(p => p.Id).ToHashSet(); // Assuming ProjectMongoDB has an Id + var missingIds = projectIds.Where(id => !cachedIds.Contains(id.ToString())).ToList(); - if (!projectList.Any()) + // --- Step 3: Fetch Missing from Database --- + if (missingIds.Any()) { - return NotFound(ApiResponse.SuccessResponse(new List(), "No projects found.", 200)); - } + _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(); - List projectlist = await _context.Projects - .Where(p => projectList.Contains(p.Id)) - .ToListAsync(); - - List projects = new List(); - - - foreach (var project in projectlist) - { - - projects.Add(project.ToProjectListVMFromProject()); - } - - - - return Ok(ApiResponse.SuccessResponse(projects, "Success.", 200)); - } - - [HttpPost("assign-projects/{employeeId}")] - public async Task AssigneProjectsToEmployee([FromBody] List projectAllocationDtos, [FromRoute] Guid employeeId) - { - if (projectAllocationDtos != null && employeeId != Guid.Empty) - { - Guid TenentID = GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - List? result = new List(); - List projectIds = new List(); - - foreach (var projectAllocationDto in projectAllocationDtos) + if (projectsFromDb.Any()) { - try - { - ProjectAllocation projectAllocation = projectAllocationDto.ToProjectAllocationFromProjectsAllocationDto(TenentID, employeeId); - ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.ProjectId == projectAllocationDto.ProjectId && c.ReAllocationDate == null && c.TenantId == TenentID).SingleOrDefaultAsync(); + // Map the newly fetched projects (from SQL) to their ViewModel + var vmsFromDb = _mapper.Map>(projectsFromDb); + finalViewModels.AddRange(vmsFromDb); - if (projectAllocationFromDb != null) - { - - - _context.ProjectAllocations.Attach(projectAllocationFromDb); - - if (projectAllocationDto.Status) - { - projectAllocationFromDb.JobRoleId = projectAllocation.JobRoleId; ; - projectAllocationFromDb.IsActive = true; - _context.Entry(projectAllocationFromDb).Property(e => e.JobRoleId).IsModified = true; - _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; - } - else - { - projectAllocationFromDb.ReAllocationDate = DateTime.UtcNow; - projectAllocationFromDb.IsActive = false; - _context.Entry(projectAllocationFromDb).Property(e => e.ReAllocationDate).IsModified = true; - _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; - - projectIds.Add(projectAllocation.ProjectId); - } - await _context.SaveChangesAsync(); - var result1 = new - { - Id = projectAllocationFromDb.Id, - EmployeeId = projectAllocation.EmployeeId, - JobRoleId = projectAllocation.JobRoleId, - IsActive = projectAllocation.IsActive, - ProjectId = projectAllocation.ProjectId, - AllocationDate = projectAllocation.AllocationDate, - ReAllocationDate = projectAllocation.ReAllocationDate, - TenantId = projectAllocation.TenantId - }; - result.Add(result1); - } - else - { - projectAllocation.AllocationDate = DateTime.Now; - projectAllocation.IsActive = true; - _context.ProjectAllocations.Add(projectAllocation); - await _context.SaveChangesAsync(); - - projectIds.Add(projectAllocation.ProjectId); - - } - - - } - catch (Exception ex) - { - - return Ok(ApiResponse.ErrorResponse(ex.Message, ex, 400)); - } + // --- 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); } - await _cache.ClearAllProjectIds(employeeId); - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeId = employeeId }; - - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); - - return Ok(ApiResponse.SuccessResponse(result, "Data saved successfully", 200)); - } - else - { - return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "All Field is required", 400)); } + return finalViewModels; + } + + private Guid GetTenantId() + { + return _userHelper.GetTenantId(); + } + + private async Task GetProjectViewModel(Guid? id, Project project) + { + ProjectDetailsVM vm = new ProjectDetailsVM(); + + // List buildings = _unitOfWork.Building.GetAll(c => c.ProjectId == id).ToList(); + List buildings = await _context.Buildings.Where(c => c.ProjectId == id).ToListAsync(); + List idList = buildings.Select(o => o.Id).ToList(); + // List floors = _unitOfWork.Floor.GetAll(c => idList.Contains(c.Id)).ToList(); + List floors = await _context.Floor.Where(c => idList.Contains(c.BuildingId)).ToListAsync(); + idList = floors.Select(o => o.Id).ToList(); + //List workAreas = _unitOfWork.WorkArea.GetAll(c => idList.Contains(c.Id), includeProperties: "WorkItems,WorkItems.ActivityMaster").ToList(); + + List workAreas = await _context.WorkAreas.Where(c => idList.Contains(c.FloorId)).ToListAsync(); + + idList = workAreas.Select(o => o.Id).ToList(); + List workItems = await _context.WorkItems.Include(c => c.WorkCategoryMaster).Where(c => idList.Contains(c.WorkAreaId)).Include(c => c.ActivityMaster).ToListAsync(); + // List workItems = _unitOfWork.WorkItem.GetAll(c => idList.Contains(c.WorkAreaId), includeProperties: "ActivityMaster").ToList(); + idList = workItems.Select(t => t.Id).ToList(); + List tasks = await _context.TaskAllocations.Where(t => idList.Contains(t.WorkItemId) && t.AssignmentDate.Date == DateTime.UtcNow.Date).ToListAsync(); + vm.project = project; + vm.buildings = buildings; + vm.floors = floors; + vm.workAreas = workAreas; + vm.workItems = workItems; + vm.Tasks = tasks; + return vm; } + /// + /// Fetches project details from the database for a given list of project IDs and assembles them into MongoDB models. + /// This method encapsulates the optimized, parallel database queries. + /// + /// The list of project IDs to fetch. + /// The current tenant ID for filtering. + /// A list of fully populated ProjectMongoDB objects. + private async Task> FetchAndBuildProjectDetails(List projectIdsToFetch, Guid tenantId) + { + // Task to get base project details for the MISSING projects + var projectsTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + return await context.Projects.AsNoTracking() + .Where(p => projectIdsToFetch.Contains(p.Id) && p.TenantId == tenantId) + .ToListAsync(); + }); + + // Task to get team sizes for the MISSING projects + var teamSizesTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + return await context.ProjectAllocations.AsNoTracking() + .Where(pa => pa.TenantId == tenantId && projectIdsToFetch.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); + }); + + // Task to get work summaries for the MISSING projects + var workSummariesTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + return await context.WorkItems.AsNoTracking() + .Where(wi => wi.TenantId == tenantId && + wi.WorkArea != null && + wi.WorkArea.Floor != null && + wi.WorkArea.Floor.Building != null && + projectIdsToFetch.Contains(wi.WorkArea.Floor.Building.ProjectId)) + .GroupBy(wi => wi.WorkArea!.Floor!.Building!.ProjectId) + .Select(g => new { ProjectId = g.Key, PlannedWork = g.Sum(i => i.PlannedWork), CompletedWork = g.Sum(i => i.CompletedWork) }) + .ToDictionaryAsync(x => x.ProjectId); + }); + + // Await all parallel tasks to complete + await Task.WhenAll(projectsTask, teamSizesTask, workSummariesTask); + + var projects = await projectsTask; + var teamSizes = await teamSizesTask; + var workSummaries = await workSummariesTask; + + // Proactively update the cache with the items we just fetched. + _logger.LogInfo("Updating cache with {NewItemCount} newly fetched projects.", projects.Count); + await _cache.AddProjectDetailsList(projects); + + // This section would build the full ProjectMongoDB objects, similar to your AddProjectDetailsList method. + // For brevity, assuming you have a mapper or a builder for this. Here's a simplified representation: + var mongoDetailsList = new List(); + foreach (var project in projects) + { + // This is a placeholder for the full build logic from your other methods. + // In a real scenario, you would fetch all hierarchy levels (buildings, floors, etc.) + // for the `projectIdsToFetch` and build the complete MongoDB object. + var mongoDetail = _mapper.Map(project); + mongoDetail.Id = project.Id; + mongoDetail.TeamSize = teamSizes.GetValueOrDefault(project.Id, 0); + if (workSummaries.TryGetValue(project.Id, out var summary)) + { + mongoDetail.PlannedWork = summary.PlannedWork; + mongoDetail.CompletedWork = summary.CompletedWork; + } + mongoDetailsList.Add(mongoDetail); + } + + return mongoDetailsList; + } + + #endregion } } \ No newline at end of file diff --git a/Marco.Pms.Services/Controllers/UserController.cs b/Marco.Pms.Services/Controllers/UserController.cs index 2aeb208..4bb4432 100644 --- a/Marco.Pms.Services/Controllers/UserController.cs +++ b/Marco.Pms.Services/Controllers/UserController.cs @@ -50,7 +50,7 @@ namespace MarcoBMS.Services.Controllers emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id); } - List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(emp.Id); + List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeId(emp.Id); string[] projectsId = []; /* User with permission manage project can see all projects */ diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 589ab52..4369b5b 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -1,5 +1,6 @@ using Marco.Pms.CacheHelper; using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.Master; using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.Projects; using MarcoBMS.Services.Service; @@ -15,20 +16,20 @@ namespace Marco.Pms.Services.Helpers private readonly ReportCache _reportCache; private readonly ILoggingService _logger; private readonly IDbContextFactory _dbContextFactory; + private readonly ApplicationDbContext _context; public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache, ReportCache reportCache, ILoggingService logger, - IDbContextFactory dbContextFactory) + IDbContextFactory dbContextFactory, ApplicationDbContext context) { _projectCache = projectCache; _employeeCache = employeeCache; _reportCache = reportCache; _logger = logger; _dbContextFactory = dbContextFactory; + _context = context; } // ------------------------------------ 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) { @@ -417,9 +418,11 @@ namespace Marco.Pms.Services.Helpers } public async Task UpdateProjectDetailsOnly(Project project) { + StatusMaster projectStatus = await _context.StatusMasters + .FirstOrDefaultAsync(s => s.Id == project.ProjectStatusId) ?? new StatusMaster(); try { - bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project); + bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project, projectStatus); return response; } catch (Exception ex) @@ -457,10 +460,22 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while getting list od project details from to Cache: {Error}", ex.Message); + _logger.LogWarning("Error occured while getting list of project details from to Cache: {Error}", ex.Message); return null; } } + public async Task DeleteProjectByIdAsync(Guid projectId) + { + try + { + var response = await _projectCache.DeleteProjectByIdFromCacheAsync(projectId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting project from to Cache: {Error}", ex.Message); + + } + } // ------------------------------------ Project Infrastructure Cache --------------------------------------- @@ -527,6 +542,9 @@ namespace Marco.Pms.Services.Helpers return null; } } + + // ------------------------------------------------------- WorkItem ------------------------------------------------------- + public async Task?> GetWorkItemsByWorkAreaIds(List workAreaIds) { try @@ -544,9 +562,6 @@ namespace Marco.Pms.Services.Helpers return null; } } - - // ------------------------------------------------------- WorkItem ------------------------------------------------------- - public async Task ManageWorkItemDetails(List workItems) { try @@ -609,6 +624,18 @@ namespace Marco.Pms.Services.Helpers _logger.LogWarning("Error occured while updating planned work, completed work, and today's assigned work in workItems in Cache: {Error}", ex.Message); } } + public async Task DeleteWorkItemByIdAsync(Guid workItemId) + { + try + { + var response = await _projectCache.DeleteWorkItemByIdFromCacheAsync(workItemId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting work item from to Cache: {Error}", ex.Message); + + } + } // ------------------------------------ Employee Profile Cache --------------------------------------- diff --git a/Marco.Pms.Services/Helpers/ProjectsHelper.cs b/Marco.Pms.Services/Helpers/ProjectsHelper.cs index fb5b6f2..6c1cab1 100644 --- a/Marco.Pms.Services/Helpers/ProjectsHelper.cs +++ b/Marco.Pms.Services/Helpers/ProjectsHelper.cs @@ -58,7 +58,7 @@ namespace MarcoBMS.Services.Helpers if (projectIds == null) { - var hasPermission = await _permission.HasPermission(LoggedInEmployee.Id, PermissionsMaster.ManageProject); + var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageProject, LoggedInEmployee.Id); if (hasPermission) { var projects = await _context.Projects.Where(c => c.TenantId == tenantId).ToListAsync(); diff --git a/Marco.Pms.Services/Helpers/RolesHelper.cs b/Marco.Pms.Services/Helpers/RolesHelper.cs index 15bf0b1..1688dce 100644 --- a/Marco.Pms.Services/Helpers/RolesHelper.cs +++ b/Marco.Pms.Services/Helpers/RolesHelper.cs @@ -3,6 +3,7 @@ using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Entitlements; using Marco.Pms.Services.Helpers; +using MarcoBMS.Services.Service; using Microsoft.EntityFrameworkCore; namespace MarcoBMS.Services.Helpers @@ -11,33 +12,81 @@ namespace MarcoBMS.Services.Helpers { private readonly ApplicationDbContext _context; private readonly CacheUpdateHelper _cache; - public RolesHelper(ApplicationDbContext context, CacheUpdateHelper cache) + private readonly ILoggingService _logger; + public RolesHelper(ApplicationDbContext context, CacheUpdateHelper cache, ILoggingService logger) { _context = context; _cache = cache; + _logger = logger; } - public async Task> GetFeaturePermissionByEmployeeID(Guid EmployeeID) + /// + /// Retrieves a unique list of enabled feature permissions for a given employee. + /// This method is optimized to use a single, composed database query. + /// + /// The ID of the employee. + /// A distinct list of FeaturePermission objects the employee is granted. + public async Task> GetFeaturePermissionByEmployeeId(Guid EmployeeId) { - List roleMappings = await _context.EmployeeRoleMappings.Where(c => c.EmployeeId == EmployeeID && c.IsEnabled == true).Select(c => c.RoleId).ToListAsync(); + _logger.LogInfo("Fetching feature permissions for EmployeeId: {EmployeeId}", EmployeeId); - await _cache.AddApplicationRole(EmployeeID, roleMappings); + try + { + // --- Step 1: Define the subquery for the employee's roles --- + // This is an IQueryable, not a list. It will be composed directly into the main query + // by Entity Framework, avoiding a separate database call. + var employeeRoleIdsQuery = _context.EmployeeRoleMappings + .Where(erm => erm.EmployeeId == EmployeeId && erm.IsEnabled == true) + .Select(erm => erm.RoleId); - // _context.RolePermissionMappings + // --- Step 2: Asynchronously update the cache in the background (Fire and Forget) --- + // This task is started but not awaited. The main function continues immediately, + // reducing latency. The cache will be updated eventually without blocking the user. + _ = Task.Run(async () => + { + try + { + var roleIds = await employeeRoleIdsQuery.ToListAsync(); // Execute the query for the cache + if (roleIds.Any()) + { + await _cache.AddApplicationRole(EmployeeId, roleIds); + _logger.LogInfo("Successfully queued cache update for EmployeeId: {EmployeeId}", EmployeeId); + } + } + catch (Exception ex) + { + // Log errors from the background task so they are not lost. + _logger.LogWarning("Background cache update failed for EmployeeId {EmployeeId} : {Error}", EmployeeId, ex.Message); + } + }); - var result = await (from rpm in _context.RolePermissionMappings - join fp in _context.FeaturePermissions.Where(c => c.IsEnabled == true).Include(fp => fp.Feature) // Include Feature - on rpm.FeaturePermissionId equals fp.Id - where roleMappings.Contains(rpm.ApplicationRoleId) - select fp) - .ToListAsync(); + // --- Step 3: Execute the main query to get permissions in a single database call --- + // This single, efficient query gets all the required data at once. + var permissions = await ( + from rpm in _context.RolePermissionMappings + join fp in _context.FeaturePermissions.Include(f => f.Feature) // Include related Feature data + on rpm.FeaturePermissionId equals fp.Id + // The 'employeeRoleIdsQuery' subquery is seamlessly integrated here by EF Core, + // resulting in a SQL "IN (SELECT ...)" clause. + where employeeRoleIdsQuery.Contains(rpm.ApplicationRoleId) && fp.IsEnabled == true + select fp) + .Distinct() // Ensures each permission is returned only once + .ToListAsync(); - return result; + _logger.LogInfo("Successfully retrieved {PermissionCount} unique permissions for EmployeeId: {EmployeeId}", permissions.Count, EmployeeId); - // return null; + return permissions; + } + catch (Exception ex) + { + _logger.LogError("An error occurred while fetching permissions for EmployeeId {EmployeeId} :{Error}", EmployeeId, ex.Message); + // Depending on your application's error handling strategy, you might re-throw, + // or return an empty list to prevent downstream failures. + return new List(); + } } - public async Task> GetFeaturePermissionByRoleID(Guid roleId) + public async Task> GetFeaturePermissionByRoleID1(Guid roleId) { List roleMappings = await _context.RolePermissionMappings.Where(c => c.ApplicationRoleId == roleId).Select(c => c.ApplicationRoleId).ToListAsync(); @@ -54,5 +103,49 @@ namespace MarcoBMS.Services.Helpers // return null; } + /// + /// Retrieves a unique list of enabled feature permissions for a given role. + /// This method is optimized to fetch all data in a single, efficient database query. + /// + /// The ID of the role. + /// A distinct list of FeaturePermission objects granted to the role. + public async Task> GetFeaturePermissionByRoleID(Guid roleId) + { + _logger.LogInfo("Fetching feature permissions for RoleID: {RoleId}", roleId); + + try + { + // This single, efficient query gets all the required data at once. + // It joins the mapping table to the permissions table and filters by the given roleId. + var permissions = await ( + // 1. Start with the linking table. + from rpm in _context.RolePermissionMappings + + // 2. Join to the FeaturePermissions table on the foreign key. + join fp in _context.FeaturePermissions on rpm.FeaturePermissionId equals fp.Id + + // 3. Apply all filters in one 'where' clause for clarity and efficiency. + where + rpm.ApplicationRoleId == roleId // Filter by the specific role + && fp.IsEnabled == true // And only get enabled permissions + + // 4. Select the final FeaturePermission object. + select fp) + .Include(fp => fp.Feature) + .Distinct() + .ToListAsync(); + + _logger.LogInfo("Successfully retrieved {PermissionCount} unique permissions for RoleID: {RoleId}", permissions.Count, roleId); + + return permissions; + } + catch (Exception ex) + { + _logger.LogError("An error occurred while fetching permissions for RoleId {RoleId}: {Error}", roleId, ex.Message); + // Return an empty list as a safe default to prevent downstream failures. + return new List(); + } + } + } } diff --git a/Marco.Pms.Services/Service/PermissionServices.cs b/Marco.Pms.Services/Service/PermissionServices.cs index 7162dc5..f20a768 100644 --- a/Marco.Pms.Services/Service/PermissionServices.cs +++ b/Marco.Pms.Services/Service/PermissionServices.cs @@ -24,7 +24,7 @@ namespace Marco.Pms.Services.Service var featurePermissionIds = await _cache.GetPermissions(employeeId); if (featurePermissionIds == null) { - List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(employeeId); + List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeId(employeeId); featurePermissionIds = featurePermission.Select(fp => fp.Id).ToList(); } var hasPermission = featurePermissionIds.Contains(featurePermissionId); From b78f58c304b27aa33eafa5b1fb939e14e8b03e4f Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 14 Jul 2025 15:08:31 +0530 Subject: [PATCH 097/124] Solved Concurrency Issue --- Marco.Pms.CacheHelper/EmployeeCache.cs | 19 +------- .../Helpers/CacheUpdateHelper.cs | 23 +++++++++- Marco.Pms.Services/Helpers/RolesHelper.cs | 43 ++++++++++--------- 3 files changed, 46 insertions(+), 39 deletions(-) diff --git a/Marco.Pms.CacheHelper/EmployeeCache.cs b/Marco.Pms.CacheHelper/EmployeeCache.cs index c2a1f7b..4a668f0 100644 --- a/Marco.Pms.CacheHelper/EmployeeCache.cs +++ b/Marco.Pms.CacheHelper/EmployeeCache.cs @@ -20,29 +20,12 @@ namespace Marco.Pms.CacheHelper var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name _collection = mongoDB.GetCollection("EmployeeProfile"); } - public async Task AddApplicationRoleToCache(Guid employeeId, List roleIds) + public async Task AddApplicationRoleToCache(Guid employeeId, List newRoleIds, List newPermissionIds) { - // 1. Guard Clause: Avoid unnecessary database work if there are no roles to add. - if (roleIds == null || !roleIds.Any()) - { - return false; // Nothing to add, so the operation did not result in a change. - } // 2. Perform database queries concurrently for better performance. var employeeIdString = employeeId.ToString(); - Task> getPermissionIdsTask = _context.RolePermissionMappings - .Where(rp => roleIds.Contains(rp.ApplicationRoleId)) - .Select(p => p.FeaturePermissionId.ToString()) - .Distinct() - .ToListAsync(); - - // 3. Prepare role IDs in parallel with the database query. - var newRoleIds = roleIds.Select(r => r.ToString()).ToList(); - - // 4. Await the database query result. - var newPermissionIds = await getPermissionIdsTask; - // 5. Build a single, efficient update operation. var filter = Builders.Filter.Eq(e => e.Id, employeeIdString); diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 4369b5b..5bae90f 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -641,9 +641,30 @@ namespace Marco.Pms.Services.Helpers // ------------------------------------ Employee Profile Cache --------------------------------------- public async Task AddApplicationRole(Guid employeeId, List roleIds) { + // 1. Guard Clause: Avoid unnecessary database work if there are no roles to add. + if (roleIds == null || !roleIds.Any()) + { + return; // Nothing to add, so the operation did not result in a change. + } + Task> getPermissionIdsTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + + return await context.RolePermissionMappings + .Where(rp => roleIds.Contains(rp.ApplicationRoleId)) + .Select(p => p.FeaturePermissionId.ToString()) + .Distinct() + .ToListAsync(); + }); + + // 3. Prepare role IDs in parallel with the database query. + var newRoleIds = roleIds.Select(r => r.ToString()).ToList(); + + // 4. Await the database query result. + var newPermissionIds = await getPermissionIdsTask; try { - var response = await _employeeCache.AddApplicationRoleToCache(employeeId, roleIds); + var response = await _employeeCache.AddApplicationRoleToCache(employeeId, newRoleIds, newPermissionIds); } catch (Exception ex) { diff --git a/Marco.Pms.Services/Helpers/RolesHelper.cs b/Marco.Pms.Services/Helpers/RolesHelper.cs index 1688dce..cd73c0f 100644 --- a/Marco.Pms.Services/Helpers/RolesHelper.cs +++ b/Marco.Pms.Services/Helpers/RolesHelper.cs @@ -10,14 +10,16 @@ namespace MarcoBMS.Services.Helpers { public class RolesHelper { + private readonly IDbContextFactory _dbContextFactory; private readonly ApplicationDbContext _context; private readonly CacheUpdateHelper _cache; private readonly ILoggingService _logger; - public RolesHelper(ApplicationDbContext context, CacheUpdateHelper cache, ILoggingService logger) + public RolesHelper(ApplicationDbContext context, CacheUpdateHelper cache, ILoggingService logger, IDbContextFactory dbContextFactory) { _context = context; _cache = cache; _logger = logger; + _dbContextFactory = dbContextFactory; } /// @@ -32,56 +34,57 @@ namespace MarcoBMS.Services.Helpers try { - // --- Step 1: Define the subquery for the employee's roles --- - // This is an IQueryable, not a list. It will be composed directly into the main query - // by Entity Framework, avoiding a separate database call. + // --- Step 1: Define the subquery using the main thread's context --- + // This is safe because the query is not executed yet. var employeeRoleIdsQuery = _context.EmployeeRoleMappings - .Where(erm => erm.EmployeeId == EmployeeId && erm.IsEnabled == true) + .Where(erm => erm.EmployeeId == EmployeeId && erm.IsEnabled) .Select(erm => erm.RoleId); - // --- Step 2: Asynchronously update the cache in the background (Fire and Forget) --- - // This task is started but not awaited. The main function continues immediately, - // reducing latency. The cache will be updated eventually without blocking the user. + // --- Step 2: Asynchronously update the cache using the DbContextFactory --- _ = Task.Run(async () => { try { - var roleIds = await employeeRoleIdsQuery.ToListAsync(); // Execute the query for the cache + // Create a NEW, short-lived DbContext instance for this background task. + await using var contextForCache = await _dbContextFactory.CreateDbContextAsync(); + + // Now, re-create and execute the query using this new, isolated context. + var roleIds = await contextForCache.EmployeeRoleMappings + .Where(erm => erm.EmployeeId == EmployeeId && erm.IsEnabled) + .Select(erm => erm.RoleId) + .ToListAsync(); + if (roleIds.Any()) { + // The cache service might also need its own context, or you can pass the data directly. + // Assuming AddApplicationRole takes the data, not a context. await _cache.AddApplicationRole(EmployeeId, roleIds); _logger.LogInfo("Successfully queued cache update for EmployeeId: {EmployeeId}", EmployeeId); } } catch (Exception ex) { - // Log errors from the background task so they are not lost. _logger.LogWarning("Background cache update failed for EmployeeId {EmployeeId} : {Error}", EmployeeId, ex.Message); } }); - // --- Step 3: Execute the main query to get permissions in a single database call --- - // This single, efficient query gets all the required data at once. + // --- Step 3: Execute the main query on the main thread using its original context --- + // This is now safe because the background task is using a different DbContext instance. var permissions = await ( from rpm in _context.RolePermissionMappings - join fp in _context.FeaturePermissions.Include(f => f.Feature) // Include related Feature data + join fp in _context.FeaturePermissions.Include(f => f.Feature) on rpm.FeaturePermissionId equals fp.Id - // The 'employeeRoleIdsQuery' subquery is seamlessly integrated here by EF Core, - // resulting in a SQL "IN (SELECT ...)" clause. where employeeRoleIdsQuery.Contains(rpm.ApplicationRoleId) && fp.IsEnabled == true select fp) - .Distinct() // Ensures each permission is returned only once + .Distinct() .ToListAsync(); _logger.LogInfo("Successfully retrieved {PermissionCount} unique permissions for EmployeeId: {EmployeeId}", permissions.Count, EmployeeId); - return permissions; } catch (Exception ex) { - _logger.LogError("An error occurred while fetching permissions for EmployeeId {EmployeeId} :{Error}", EmployeeId, ex.Message); - // Depending on your application's error handling strategy, you might re-throw, - // or return an empty list to prevent downstream failures. + _logger.LogError("An error occurred while fetching permissions for EmployeeId {EmployeeId} : {Error}", EmployeeId, ex.Message); return new List(); } } From ca34b01ab0e3dc1a0be114c29c2fdc96288a184c Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 14 Jul 2025 15:57:52 +0530 Subject: [PATCH 098/124] Optimized the Get project By ID API --- .../MongoDBModels/StatusMasterMongoDB.cs | 2 +- .../Controllers/ProjectController.cs | 117 +++++++++++++++--- .../MappingProfiles/ProjectMappingProfile.cs | 13 +- 3 files changed, 116 insertions(+), 16 deletions(-) diff --git a/Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs index 01a0552..77e8eb5 100644 --- a/Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs @@ -2,7 +2,7 @@ { public class StatusMasterMongoDB { - public string? Id { get; set; } + public string Id { get; set; } = string.Empty; public string? Status { get; set; } } } diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index adb5887..acc97d2 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -40,8 +40,8 @@ namespace MarcoBMS.Services.Controllers private readonly Guid tenantId; - public ProjectController(IDbContextFactory dbContextFactory, ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, - ProjectsHelper projectHelper, IHubContext signalR, PermissionServices permission, CacheUpdateHelper cache, IMapper mapper) + public ProjectController(IDbContextFactory dbContextFactory, ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, + ProjectsHelper projectHelper, IHubContext signalR, CacheUpdateHelper cache, PermissionServices permission, IMapper mapper) { _dbContextFactory = dbContextFactory; _context = context; @@ -52,7 +52,7 @@ namespace MarcoBMS.Services.Controllers _cache = cache; _permission = permission; _mapper = mapper; - tenantId = _userHelper.GetTenantId(); + tenantId = userHelper.GetTenantId(); } #region =================================================================== Project Get APIs =================================================================== @@ -161,29 +161,74 @@ namespace MarcoBMS.Services.Controllers catch (Exception ex) { // --- Step 6: Graceful Error Handling --- - _logger.LogError("An unexpected error occurred in GetAllProjects for tenant {TenantId}. : {Error}", tenantId, ex.Message); + _logger.LogError("An unexpected error occurred in GetAllProjects for tenant {TenantId}. \n {Error}", tenantId, ex.Message); return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred. Please try again later.", null, 500)); } } + /// + /// Retrieves details for a specific project by its ID. + /// This endpoint is optimized with a cache-first strategy and parallel permission checks. + /// + /// The unique identifier of the project. + /// An ApiResponse containing the project details or an appropriate error. + [HttpGet("get/{id}")] public async Task Get([FromRoute] Guid id) { + // --- Step 1: Input Validation --- if (!ModelState.IsValid) { - var errors = ModelState.Values - .SelectMany(v => v.Errors) - .Select(e => e.ErrorMessage) - .ToList(); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); - + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("Get project called with invalid model state for ID {ProjectId}. Errors: {Errors}", id, string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - var project = await _context.Projects.Where(c => c.TenantId == _userHelper.GetTenantId() && c.Id == id).SingleOrDefaultAsync(); - if (project == null) return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); - return Ok(ApiResponse.SuccessResponse(project, "Success.", 200)); + try + { + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // --- Step 2: Run independent operations in PARALLEL --- + // We can check permissions and fetch data at the same time to reduce latency. + var permissionTask = _permission.HasProjectPermission(loggedInEmployee, id); + + // This helper method encapsulates the "cache-first, then database" logic. + var projectDataTask = GetProjectDataAsync(id); + + // Await both tasks to complete. + await Task.WhenAll(permissionTask, projectDataTask); + + var hasPermission = await permissionTask; + var projectVm = await projectDataTask; + + // --- Step 3: Process results sequentially --- + + // 3a. Check for permission first. Forbid() is the idiomatic way to return 403. + if (!hasPermission) + { + _logger.LogWarning("Access denied for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, id); + return StatusCode(403, (ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to access this project.", 403))); + } + + // 3b. Check if the project was found (either in cache or DB). + if (projectVm == null) + { + _logger.LogInfo("Project with ID {ProjectId} not found.", id); + return NotFound(ApiResponse.ErrorResponse("Project not found.", $"No project found with ID {id}.", 404)); + } + + // 3c. Success. Return the consistent ViewModel. + _logger.LogInfo("Successfully retrieved project {ProjectId}.", id); + return Ok(ApiResponse.SuccessResponse(projectVm, "Project retrieved successfully.", 200)); + } + catch (Exception ex) + { + _logger.LogError("An unexpected error occurred while getting project {ProjectId} : \n {Error}", id, ex.Message); + return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred.", null, 500)); + } } + [HttpGet("details/{id}")] public async Task Details([FromRoute] Guid id) { @@ -1331,7 +1376,6 @@ namespace MarcoBMS.Services.Controllers return vm; } - /// /// Fetches project details from the database for a given list of project IDs and assembles them into MongoDB models. /// This method encapsulates the optimized, parallel database queries. @@ -1409,6 +1453,51 @@ namespace MarcoBMS.Services.Controllers return mongoDetailsList; } + /// + /// Private helper to encapsulate the cache-first data retrieval logic. + /// + /// A ProjectDetailVM if found, otherwise null. + private async Task GetProjectDataAsync(Guid projectId) + { + // --- Cache First --- + _logger.LogDebug("Attempting to fetch project {ProjectId} from cache.", projectId); + var cachedProject = await _cache.GetProjectDetails(projectId); + if (cachedProject != null) + { + _logger.LogInfo("Cache HIT for project {ProjectId}.", projectId); + // Map from the cache model (e.g., ProjectMongoDB) to the response ViewModel. + return _mapper.Map(cachedProject); + } + + // --- Database Second (on Cache Miss) --- + _logger.LogInfo("Cache MISS for project {ProjectId}. Fetching from database.", projectId); + var dbProject = await _context.Projects + .AsNoTracking() // Use AsNoTracking for read-only queries. + .Where(p => p.Id == projectId && p.TenantId == tenantId) + .SingleOrDefaultAsync(); + + if (dbProject == null) + { + return null; // The project doesn't exist. + } + + // --- Proactively Update Cache --- + // The next request for this project will now be a cache hit. + try + { + // Map the DB entity to the cache model (e.g., ProjectMongoDB) before caching. + await _cache.AddProjectDetails(dbProject); + _logger.LogInfo("Updated cache with project {ProjectId}.", projectId); + } + catch (Exception ex) + { + _logger.LogWarning("Failed to update cache for project {ProjectId} : \n {Error}", projectId, ex.Message); + } + + // Map from the database entity to the response ViewModel. + return dbProject; + } + #endregion } } \ No newline at end of file diff --git a/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs b/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs index c7ec4af..f527f67 100644 --- a/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs @@ -20,7 +20,18 @@ namespace Marco.Pms.Services.MappingProfiles .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)) + opt => opt.MapFrom(src => new Guid(src.Id)) + ); + + CreateMap() + .ForMember( + dest => dest.Id, + // Explicitly and safely convert string Id to Guid Id + opt => opt.MapFrom(src => new Guid(src.Id)) + ).ForMember( + dest => dest.ProjectStatusId, + // Explicitly and safely convert string ProjectStatusId to Guid ProjectStatusId + opt => opt.MapFrom(src => src.ProjectStatus == null ? Guid.Empty : new Guid(src.ProjectStatus.Id)) ); CreateMap(); From 36eb7aef7fcc6dd6f209f383c330bef1415366b8 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 14 Jul 2025 17:00:28 +0530 Subject: [PATCH 099/124] Optimized the Update project API --- .../Controllers/ProjectController.cs | 168 ++++++++++++++---- Marco.Pms.Services/Helpers/ProjectsHelper.cs | 6 +- .../MappingProfiles/ProjectMappingProfile.cs | 3 + 3 files changed, 142 insertions(+), 35 deletions(-) diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index acc97d2..3d5558f 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -70,7 +70,6 @@ namespace MarcoBMS.Services.Controllers _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()) @@ -316,7 +315,7 @@ namespace MarcoBMS.Services.Controllers } - var project = await _context.Projects.Where(c => c.TenantId == _userHelper.GetTenantId() && c.Id == id).Include(c => c.ProjectStatus).SingleOrDefaultAsync(); // includeProperties: "ProjectStatus,Tenant"); //_context.Stock.FindAsync(id); + var project = await _context.Projects.Where(c => c.TenantId == tenantId && c.Id == id).Include(c => c.ProjectStatus).SingleOrDefaultAsync(); // includeProperties: "ProjectStatus,Tenant"); //_context.Stock.FindAsync(id); if (project == null) { @@ -420,7 +419,6 @@ namespace MarcoBMS.Services.Controllers } // 2. Prepare data without I/O - Guid tenantId = _userHelper.GetTenantId(); // Assuming this is fast and from claims Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInUserId = loggedInEmployee.Id; var project = projectDto.ToProjectFromCreateProjectDto(tenantId); @@ -465,7 +463,7 @@ namespace MarcoBMS.Services.Controllers } [HttpPut] - [Route("update/{id}")] + [Route("update1/{id}")] public async Task Update([FromRoute] Guid id, [FromBody] UpdateProjectDto updateProjectDto) { var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); @@ -480,9 +478,7 @@ namespace MarcoBMS.Services.Controllers } try { - Guid TenantId = GetTenantId(); - - Project project = updateProjectDto.ToProjectFromUpdateProjectDto(TenantId, id); + Project project = updateProjectDto.ToProjectFromUpdateProjectDto(tenantId, id); _context.Projects.Update(project); await _context.SaveChangesAsync(); @@ -507,6 +503,97 @@ namespace MarcoBMS.Services.Controllers } } + /// + /// Updates an existing project's details. + /// This endpoint is secure, handles concurrency, and performs non-essential tasks in the background. + /// + /// The ID of the project to update. + /// The data to update the project with. + /// An ApiResponse confirming the update or an appropriate error. + + [HttpPut("update/{id}")] + public async Task UpdateProject([FromRoute] Guid id, [FromBody] UpdateProjectDto updateProjectDto) + { + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) + { + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("Update project called with invalid model state for ID {ProjectId}. Errors: {Errors}", id, string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); + } + + try + { + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + // --- Step 2: Fetch the Existing Entity from the Database --- + // This is crucial to avoid the data loss bug. We only want to modify an existing record. + var existingProject = await _context.Projects + .Where(p => p.Id == id && p.TenantId == tenantId) + .SingleOrDefaultAsync(); + + // 2a. Existence Check + if (existingProject == null) + { + _logger.LogWarning("Attempt to update non-existent project with ID {ProjectId} by user {UserId}.", id, loggedInEmployee.Id); + return NotFound(ApiResponse.ErrorResponse("Project not found.", $"No project found with ID {id}.", 404)); + } + + // 2b. Security Check + var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, id); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} attempting to update project {ProjectId}.", loggedInEmployee.Id, id); + return StatusCode(403, (ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to modify this project.", 403))); + } + + // --- Step 3: Apply Changes and Save --- + // Map the changes from the DTO onto the entity we just fetched from the database. + // This only modifies the properties defined in the mapping, preventing data loss. + _mapper.Map(updateProjectDto, existingProject); + + // Mark the entity as modified (if your mapping doesn't do it automatically). + _context.Entry(existingProject).State = EntityState.Modified; + + try + { + await _context.SaveChangesAsync(); + _logger.LogInfo("Successfully updated project {ProjectId} by user {UserId}.", id, loggedInEmployee.Id); + } + catch (DbUpdateConcurrencyException ex) + { + // --- Step 4: Handle Concurrency Conflicts --- + // This happens if another user modified the project after we fetched it. + _logger.LogWarning("Concurrency conflict while updating project {ProjectId} \n {Error}", id, ex.Message); + return Conflict(ApiResponse.ErrorResponse("Conflict occurred.", "This project has been modified by someone else. Please refresh and try again.", 409)); + } + + // --- Step 5: Perform Side-Effects in the Background (Fire and Forget) --- + // The core database operation is done. Now, we perform non-blocking cache and notification updates. + _ = Task.Run(async () => + { + // Create a DTO of the updated project to pass to background tasks. + var projectDto = _mapper.Map(existingProject); + + // 5a. Update Cache + await UpdateCacheInBackground(existingProject); + + // 5b. Send Targeted SignalR Notification + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Update_Project", Response = projectDto }; + await SendNotificationInBackground(notification, projectDto.Id); + }); + + // --- Step 6: Return Success Response Immediately --- + // The client gets a fast response without waiting for caching or SignalR. + return Ok(ApiResponse.SuccessResponse(_mapper.Map(existingProject), "Project updated successfully.", 200)); + } + catch (Exception ex) + { + // --- Step 7: Graceful Error Handling for Unexpected Errors --- + _logger.LogError("An unexpected error occurred while updating project {ProjectId} \n {Error}", id, ex.Message); + return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred.", null, 500)); + } + } + #endregion #region =================================================================== Project Allocation APIs =================================================================== @@ -524,7 +611,6 @@ namespace MarcoBMS.Services.Controllers return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } - Guid TenantId = GetTenantId(); if (projectid != null) { @@ -535,14 +621,14 @@ namespace MarcoBMS.Services.Controllers { result = await (from rpm in _context.Employees.Include(c => c.JobRole) - join fp in _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == projectid) + join fp in _context.ProjectAllocations.Where(c => c.TenantId == tenantId && c.ProjectId == projectid) on rpm.Id equals fp.EmployeeId select rpm).ToListAsync(); } else { result = await (from rpm in _context.Employees.Include(c => c.JobRole) - join fp in _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == projectid && c.IsActive == true) + join fp in _context.ProjectAllocations.Where(c => c.TenantId == tenantId && c.ProjectId == projectid && c.IsActive) on rpm.Id equals fp.EmployeeId select rpm).ToListAsync(); } @@ -577,11 +663,9 @@ namespace MarcoBMS.Services.Controllers return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } - Guid TenantId = GetTenantId(); - var employees = await _context.ProjectAllocations - .Where(c => c.TenantId == TenantId && c.ProjectId == projectId && c.Employee != null) + .Where(c => c.TenantId == tenantId && c.ProjectId == projectId && c.Employee != null) .Include(e => e.Employee) .Select(e => new { @@ -605,7 +689,6 @@ namespace MarcoBMS.Services.Controllers { if (projectAllocationDot != null) { - Guid TenentID = GetTenantId(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); List? result = new List(); @@ -616,11 +699,11 @@ namespace MarcoBMS.Services.Controllers { try { - ProjectAllocation projectAllocation = item.ToProjectAllocationFromProjectAllocationDto(TenentID); + ProjectAllocation projectAllocation = item.ToProjectAllocationFromProjectAllocationDto(tenantId); ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == projectAllocation.EmployeeId && c.ProjectId == projectAllocation.ProjectId && c.ReAllocationDate == null - && c.TenantId == TenentID).SingleOrDefaultAsync(); + && c.TenantId == tenantId).SingleOrDefaultAsync(); if (projectAllocationFromDb != null) { @@ -688,8 +771,6 @@ namespace MarcoBMS.Services.Controllers [HttpGet("assigned-projects/{employeeId}")] public async Task GetProjectsByEmployee([FromRoute] Guid employeeId) { - - Guid tenantId = _userHelper.GetTenantId(); if (employeeId == Guid.Empty) { return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Employee id not valid.", 400)); @@ -729,7 +810,6 @@ namespace MarcoBMS.Services.Controllers { if (projectAllocationDtos != null && employeeId != Guid.Empty) { - Guid TenentID = GetTenantId(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); List? result = new List(); List projectIds = new List(); @@ -738,8 +818,8 @@ namespace MarcoBMS.Services.Controllers { try { - ProjectAllocation projectAllocation = projectAllocationDto.ToProjectAllocationFromProjectsAllocationDto(TenentID, employeeId); - ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.ProjectId == projectAllocationDto.ProjectId && c.ReAllocationDate == null && c.TenantId == TenentID).SingleOrDefaultAsync(); + ProjectAllocation projectAllocation = projectAllocationDto.ToProjectAllocationFromProjectsAllocationDto(tenantId, employeeId); + ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.ProjectId == projectAllocationDto.ProjectId && c.ReAllocationDate == null && c.TenantId == tenantId).SingleOrDefaultAsync(); if (projectAllocationFromDb != null) { @@ -1017,7 +1097,6 @@ namespace MarcoBMS.Services.Controllers return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Work Item details are not valid.", 400)); } - Guid tenantId = GetTenantId(); var workItemsToCreate = new List(); var workItemsToUpdate = new List(); var responseList = new List(); @@ -1113,7 +1192,6 @@ namespace MarcoBMS.Services.Controllers [HttpDelete("task/{id}")] public async Task DeleteProjectTask(Guid id) { - Guid tenantId = _userHelper.GetTenantId(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); List workAreaIds = new List(); WorkItem? task = await _context.WorkItems.AsNoTracking().Include(t => t.WorkArea).FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId); @@ -1162,7 +1240,6 @@ namespace MarcoBMS.Services.Controllers [HttpPost("manage-infra")] public async Task ManageProjectInfra(List infraDots) { - Guid tenantId = GetTenantId(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var responseData = new InfraVM { }; @@ -1177,7 +1254,7 @@ namespace MarcoBMS.Services.Controllers { Building building = item.Building.ToBuildingFromBuildingDto(tenantId); - building.TenantId = GetTenantId(); + building.TenantId = tenantId; if (item.Building.Id == null) { @@ -1204,7 +1281,7 @@ namespace MarcoBMS.Services.Controllers if (item.Floor != null) { Floor floor = item.Floor.ToFloorFromFloorDto(tenantId); - floor.TenantId = GetTenantId(); + floor.TenantId = tenantId; bool isCreated = false; if (item.Floor.Id == null) @@ -1242,7 +1319,7 @@ namespace MarcoBMS.Services.Controllers if (item.WorkArea != null) { WorkArea workArea = item.WorkArea.ToWorkAreaFromWorkAreaDto(tenantId); - workArea.TenantId = GetTenantId(); + workArea.TenantId = tenantId; bool isCreated = false; if (item.WorkArea.Id == null) @@ -1343,11 +1420,6 @@ namespace MarcoBMS.Services.Controllers return finalViewModels; } - private Guid GetTenantId() - { - return _userHelper.GetTenantId(); - } - private async Task GetProjectViewModel(Guid? id, Project project) { ProjectDetailsVM vm = new ProjectDetailsVM(); @@ -1498,6 +1570,38 @@ namespace MarcoBMS.Services.Controllers return dbProject; } + // Helper method for background cache update + private async Task UpdateCacheInBackground(Project project) + { + try + { + // This logic can be more complex, but the idea is to update or add. + if (!await _cache.UpdateProjectDetailsOnly(project)) + { + await _cache.AddProjectDetails(project); + } + _logger.LogInfo("Background cache update succeeded for project {ProjectId}.", project.Id); + } + catch (Exception ex) + { + _logger.LogError("Background cache update failed for project {ProjectId} \n {Error}", project.Id, ex.Message); + } + } + + // Helper method for background notification + private async Task SendNotificationInBackground(object notification, Guid projectId) + { + try + { + await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + _logger.LogInfo("Background SignalR notification sent for project {ProjectId}.", projectId); + } + catch (Exception ex) + { + _logger.LogError("Background SignalR notification failed for project {ProjectId} \n {Error}", projectId, ex.Message); + } + } + #endregion } } \ No newline at end of file diff --git a/Marco.Pms.Services/Helpers/ProjectsHelper.cs b/Marco.Pms.Services/Helpers/ProjectsHelper.cs index 6c1cab1..fe70a0a 100644 --- a/Marco.Pms.Services/Helpers/ProjectsHelper.cs +++ b/Marco.Pms.Services/Helpers/ProjectsHelper.cs @@ -67,11 +67,11 @@ namespace MarcoBMS.Services.Helpers else { var allocation = await GetProjectByEmployeeID(LoggedInEmployee.Id); - if (allocation.Any()) + if (!allocation.Any()) { - projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); + return new List(); } - return new List(); + projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); } await _cache.AddProjects(LoggedInEmployee.Id, projectIds); } diff --git a/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs b/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs index f527f67..18db7ff 100644 --- a/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs @@ -1,4 +1,5 @@ using AutoMapper; +using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Master; using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.Projects; @@ -14,7 +15,9 @@ namespace Marco.Pms.Services.MappingProfiles CreateMap(); CreateMap(); CreateMap(); + CreateMap(); CreateMap(); + CreateMap(); CreateMap(); CreateMap() .ForMember( From f4ca7670e3b12a0309f106e3fc7c56b2af4eec3b Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 14 Jul 2025 18:45:23 +0530 Subject: [PATCH 100/124] Refactored: Moved business logic from ProjectController to ProjectService --- .../Controllers/ProjectController.cs | 693 +----------------- .../MappingProfiles/ProjectMappingProfile.cs | 1 + Marco.Pms.Services/Program.cs | 10 + .../Service/PermissionServices.cs | 10 +- Marco.Pms.Services/Service/ProjectServices.cs | 691 +++++++++++++++++ .../ServiceInterfaces/IProjectServices.cs | 17 + 6 files changed, 760 insertions(+), 662 deletions(-) create mode 100644 Marco.Pms.Services/Service/ProjectServices.cs create mode 100644 Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 3d5558f..e7d257f 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -1,6 +1,4 @@ -using AutoMapper; -using Marco.Pms.DataAccess.Data; -using Marco.Pms.Model.Activities; +using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Entitlements; @@ -13,6 +11,7 @@ using Marco.Pms.Model.ViewModels.Projects; using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Service; +using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; @@ -28,30 +27,26 @@ namespace MarcoBMS.Services.Controllers [Authorize] public class ProjectController : ControllerBase { - private readonly IDbContextFactory _dbContextFactory; + private readonly IProjectServices _projectServices; private readonly ApplicationDbContext _context; private readonly UserHelper _userHelper; private readonly ILoggingService _logger; - private readonly ProjectsHelper _projectsHelper; private readonly IHubContext _signalR; private readonly PermissionServices _permission; private readonly CacheUpdateHelper _cache; - private readonly IMapper _mapper; private readonly Guid tenantId; - public ProjectController(IDbContextFactory dbContextFactory, ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, - ProjectsHelper projectHelper, IHubContext signalR, CacheUpdateHelper cache, PermissionServices permission, IMapper mapper) + public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, + IHubContext signalR, CacheUpdateHelper cache, PermissionServices permission, IProjectServices projectServices) { - _dbContextFactory = dbContextFactory; _context = context; _userHelper = userHelper; _logger = logger; - _projectsHelper = projectHelper; _signalR = signalR; _cache = cache; _permission = permission; - _mapper = mapper; + _projectServices = projectServices; tenantId = userHelper.GetTenantId(); } @@ -60,30 +55,10 @@ namespace MarcoBMS.Services.Controllers [HttpGet("list/basic")] public async Task GetAllProjectsBasic() { - // Step 1: Get the current user + // 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 - 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)); + var response = await _projectServices.GetAllProjectsBasicAsync(tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } /// @@ -96,7 +71,7 @@ namespace MarcoBMS.Services.Controllers [HttpGet("list")] public async Task GetAllProjects() { - // --- Step 1: Input Validation and Initial Setup --- + // --- Input Validation and Initial Setup --- if (!ModelState.IsValid) { var errors = ModelState.Values @@ -106,63 +81,9 @@ namespace MarcoBMS.Services.Controllers _logger.LogWarning("GetAllProjects called with invalid model state. Errors: {Errors}", string.Join(", ", errors)); return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - - try - { - var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - _logger.LogInfo("Starting GetAllProjects for TenantId: {TenantId}, User: {UserId}", tenantId, loggedInEmployee.Id); - - // --- Step 2: Get a list of project IDs the user can access --- - List projectIds = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); - if (!projectIds.Any()) - { - _logger.LogInfo("User has no assigned projects. Returning empty list."); - return Ok(ApiResponse>.SuccessResponse(new List(), "No projects found for the current user.", 200)); - } - - // --- Step 3: Efficiently handle partial cache hits --- - _logger.LogInfo("Attempting to fetch details for {ProjectCount} projects from cache.", projectIds.Count); - - // Fetch what we can from the cache. - var cachedDetails = await _cache.GetProjectDetailsList(projectIds) ?? new List(); - var cachedDictionary = cachedDetails.ToDictionary(p => Guid.Parse(p.Id)); - - // Identify which projects are missing from the cache. - var missingIds = projectIds.Where(id => !cachedDictionary.ContainsKey(id)).ToList(); - - // Start building the response with the items we found in the cache. - var responseVms = _mapper.Map>(cachedDictionary.Values); - - if (missingIds.Any()) - { - // --- Step 4: Fetch ONLY the missing items from the database --- - _logger.LogInfo("Cache partial MISS. Found {CachedCount}, fetching {MissingCount} projects from DB.", - cachedDictionary.Count, missingIds.Count); - - // Call our dedicated data-fetching method for the missing IDs. - var newMongoDetails = await FetchAndBuildProjectDetails(missingIds, tenantId); - - if (newMongoDetails.Any()) - { - // Map the newly fetched items and add them to our response list. - responseVms.AddRange(newMongoDetails); - } - } - else - { - _logger.LogInfo("Cache HIT. All {ProjectCount} projects found in cache.", projectIds.Count); - } - - // --- Step 5: Return the combined result --- - _logger.LogInfo("Successfully retrieved a total of {ProjectCount} projects.", responseVms.Count); - return Ok(ApiResponse>.SuccessResponse(responseVms, "Projects retrieved successfully.", 200)); - } - catch (Exception ex) - { - // --- Step 6: Graceful Error Handling --- - _logger.LogError("An unexpected error occurred in GetAllProjects for tenant {TenantId}. \n {Error}", tenantId, ex.Message); - return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred. Please try again later.", null, 500)); - } + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetAllProjectsAsync(tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } /// @@ -173,7 +94,7 @@ namespace MarcoBMS.Services.Controllers /// An ApiResponse containing the project details or an appropriate error. [HttpGet("get/{id}")] - public async Task Get([FromRoute] Guid id) + public async Task GetProject([FromRoute] Guid id) { // --- Step 1: Input Validation --- if (!ModelState.IsValid) @@ -183,53 +104,14 @@ namespace MarcoBMS.Services.Controllers return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - try - { - var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - // --- Step 2: Run independent operations in PARALLEL --- - // We can check permissions and fetch data at the same time to reduce latency. - var permissionTask = _permission.HasProjectPermission(loggedInEmployee, id); - - // This helper method encapsulates the "cache-first, then database" logic. - var projectDataTask = GetProjectDataAsync(id); - - // Await both tasks to complete. - await Task.WhenAll(permissionTask, projectDataTask); - - var hasPermission = await permissionTask; - var projectVm = await projectDataTask; - - // --- Step 3: Process results sequentially --- - - // 3a. Check for permission first. Forbid() is the idiomatic way to return 403. - if (!hasPermission) - { - _logger.LogWarning("Access denied for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, id); - return StatusCode(403, (ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to access this project.", 403))); - } - - // 3b. Check if the project was found (either in cache or DB). - if (projectVm == null) - { - _logger.LogInfo("Project with ID {ProjectId} not found.", id); - return NotFound(ApiResponse.ErrorResponse("Project not found.", $"No project found with ID {id}.", 404)); - } - - // 3c. Success. Return the consistent ViewModel. - _logger.LogInfo("Successfully retrieved project {ProjectId}.", id); - return Ok(ApiResponse.SuccessResponse(projectVm, "Project retrieved successfully.", 200)); - } - catch (Exception ex) - { - _logger.LogError("An unexpected error occurred while getting project {ProjectId} : \n {Error}", id, ex.Message); - return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred.", null, 500)); - } + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetProjectAsync(id, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } [HttpGet("details/{id}")] - public async Task Details([FromRoute] Guid id) + public async Task GetProjectDetails([FromRoute] Guid id) { // Step 1: Validate model state if (!ModelState.IsValid) @@ -245,63 +127,13 @@ namespace MarcoBMS.Services.Controllers // Step 2: Get logged-in employee var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - _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(PermissionsMaster.ViewProject, loggedInEmployee.Id); - if (!hasViewProjectPermission) - { - _logger.LogWarning("ViewProjects permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); - return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have permission to view projects", 403)); - } - - // Step 4: Check permission for this specific project - var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, id); - if (!hasProjectPermission) - { - _logger.LogWarning("Project-specific access denied. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployee.Id, id); - return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have access to this project", 403)); - } - - // Step 5: Fetch project with status - var projectDetails = await _cache.GetProjectDetails(id); - ProjectVM? projectVM = null; - if (projectDetails == null) - { - var project = await _context.Projects - .Include(c => c.ProjectStatus) - .FirstOrDefaultAsync(c => c.TenantId == tenantId && c.Id == id); - - projectVM = _mapper.Map(project); - - if (project != null) - { - await _cache.AddProjectDetails(project); - } - } - else - { - projectVM = _mapper.Map(projectDetails); - if (projectVM.ProjectStatus != null) - { - projectVM.ProjectStatus.TenantId = tenantId; - } - } - - if (projectVM == null) - { - _logger.LogWarning("Project not found. ProjectId: {ProjectId}", id); - return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); - } - - // Step 6: Return result - - _logger.LogInfo("Project details fetched successfully. ProjectId: {ProjectId}", id); - return Ok(ApiResponse.SuccessResponse(projectVM, "Project details fetched successfully", 200)); + var response = await _projectServices.GetProjectDetailsAsync(id, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } [HttpGet("details-old/{id}")] - public async Task DetailsOld([FromRoute] Guid id) + public async Task GetProjectDetailsOld([FromRoute] Guid id) { // ProjectDetailsVM vm = new ProjectDetailsVM(); @@ -315,92 +147,10 @@ namespace MarcoBMS.Services.Controllers } - var project = await _context.Projects.Where(c => c.TenantId == tenantId && c.Id == id).Include(c => c.ProjectStatus).SingleOrDefaultAsync(); // includeProperties: "ProjectStatus,Tenant"); //_context.Stock.FindAsync(id); - - if (project == null) - { - return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); - - } - else - { - //var project = projects.Where(c => c.Id == id).SingleOrDefault(); - ProjectDetailsVM vm = await GetProjectViewModel(id, project); - - OldProjectVM projectVM = new OldProjectVM(); - if (vm.project != null) - { - projectVM.Id = vm.project.Id; - projectVM.Name = vm.project.Name; - projectVM.ShortName = vm.project.ShortName; - projectVM.ProjectAddress = vm.project.ProjectAddress; - projectVM.ContactPerson = vm.project.ContactPerson; - projectVM.StartDate = vm.project.StartDate; - projectVM.EndDate = vm.project.EndDate; - projectVM.ProjectStatusId = vm.project.ProjectStatusId; - } - projectVM.Buildings = new List(); - if (vm.buildings != null) - { - foreach (Building build in vm.buildings) - { - BuildingVM buildVM = new BuildingVM() { Id = build.Id, Description = build.Description, Name = build.Name }; - buildVM.Floors = new List(); - if (vm.floors != null) - { - foreach (Floor floorDto in vm.floors.Where(c => c.BuildingId == build.Id).ToList()) - { - FloorsVM floorVM = new FloorsVM() { FloorName = floorDto.FloorName, Id = floorDto.Id }; - floorVM.WorkAreas = new List(); - - if (vm.workAreas != null) - { - foreach (WorkArea workAreaDto in vm.workAreas.Where(c => c.FloorId == floorVM.Id).ToList()) - { - WorkAreaVM workAreaVM = new WorkAreaVM() { Id = workAreaDto.Id, AreaName = workAreaDto.AreaName, WorkItems = new List() }; - - if (vm.workItems != null) - { - foreach (WorkItem workItemDto in vm.workItems.Where(c => c.WorkAreaId == workAreaDto.Id).ToList()) - { - WorkItemVM workItemVM = new WorkItemVM() { WorkItemId = workItemDto.Id, WorkItem = workItemDto }; - - workItemVM.WorkItem.WorkArea = new WorkArea(); - - if (workItemVM.WorkItem.ActivityMaster != null) - { - workItemVM.WorkItem.ActivityMaster.Tenant = new Tenant(); - } - workItemVM.WorkItem.Tenant = new Tenant(); - - double todaysAssigned = 0; - if (vm.Tasks != null) - { - var tasks = vm.Tasks.Where(t => t.WorkItemId == workItemDto.Id).ToList(); - foreach (TaskAllocation task in tasks) - { - todaysAssigned += task.PlannedTask; - } - } - workItemVM.TodaysAssigned = todaysAssigned; - - workAreaVM.WorkItems.Add(workItemVM); - } - } - - floorVM.WorkAreas.Add(workAreaVM); - } - } - - buildVM.Floors.Add(floorVM); - } - } - projectVM.Buildings.Add(buildVM); - } - } - return Ok(ApiResponse.SuccessResponse(projectVM, "Success.", 200)); - } + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetProjectDetailsAsync(id, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } @@ -409,7 +159,7 @@ namespace MarcoBMS.Services.Controllers #region =================================================================== Project Manage APIs =================================================================== [HttpPost] - public async Task Create([FromBody] CreateProjectDto projectDto) + public async Task CreateProject([FromBody] CreateProjectDto projectDto) { // 1. Validate input first (early exit) if (!ModelState.IsValid) @@ -420,87 +170,13 @@ namespace MarcoBMS.Services.Controllers // 2. Prepare data without I/O Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var loggedInUserId = loggedInEmployee.Id; - var project = projectDto.ToProjectFromCreateProjectDto(tenantId); - - // 3. Store it to database - try + var response = await _projectServices.CreateProjectAsync(projectDto, tenantId, loggedInEmployee); + if (response.Success) { - _context.Projects.Add(project); - await _context.SaveChangesAsync(); - } - catch (Exception ex) - { - // Log the detailed exception - _logger.LogError("Failed to create project in database. Rolling back transaction. : {Error}", ex.Message); - // Return a server error as the primary operation failed - return StatusCode(500, ApiResponse.ErrorResponse("An error occurred while saving the project.", ex.Message, 500)); - } - - // 4. Perform non-critical side-effects (caching, notifications) concurrently - try - { - // These operations do not depend on each other, so they can run in parallel. - Task cacheAddDetailsTask = _cache.AddProjectDetails(project); - 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) - Task notificationTask = _signalR.Clients.Group(tenantId.ToString()).SendAsync("NotificationEventHandler", notification); - - // Await all side-effect tasks to complete in parallel - await Task.WhenAll(cacheAddDetailsTask, cacheClearListTask, notificationTask); - } - catch (Exception ex) - { - // The project was created successfully, but a side-effect failed. - // Log this as a warning, as the primary operation succeeded. Don't return an error to the user. - _logger.LogWarning("Project {ProjectId} was created, but a post-creation side-effect (caching/notification) failed. : {Error}", project.Id, ex.Message); - } - - // 5. Return a success response to the user as soon as the critical data is saved. - return Ok(ApiResponse.SuccessResponse(project.ToProjectDto(), "Project created successfully.", 200)); - } - - [HttpPut] - [Route("update1/{id}")] - public async Task Update([FromRoute] Guid id, [FromBody] UpdateProjectDto updateProjectDto) - { - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - if (!ModelState.IsValid) - { - var errors = ModelState.Values - .SelectMany(v => v.Errors) - .Select(e => e.ErrorMessage) - .ToList(); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); - - } - try - { - Project project = updateProjectDto.ToProjectFromUpdateProjectDto(tenantId, id); - _context.Projects.Update(project); - - await _context.SaveChangesAsync(); - - // Cache functions - bool isUpdated = await _cache.UpdateProjectDetailsOnly(project); - if (!isUpdated) - { - await _cache.AddProjectDetails(project); - } - - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Update_Project", Response = project.ToProjectDto() }; - + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Create_Project", Response = response.Data }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); - - return Ok(ApiResponse.SuccessResponse(project.ToProjectDto(), "Success.", 200)); - - } - catch (Exception ex) - { - return BadRequest(ApiResponse.ErrorResponse(ex.Message, ex, 400)); } + return StatusCode(response.StatusCode, response); } /// @@ -522,76 +198,15 @@ namespace MarcoBMS.Services.Controllers return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - try + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.UpdateProjectAsync(id, updateProjectDto, tenantId, loggedInEmployee); + if (response.Success) { - var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - // --- Step 2: Fetch the Existing Entity from the Database --- - // This is crucial to avoid the data loss bug. We only want to modify an existing record. - var existingProject = await _context.Projects - .Where(p => p.Id == id && p.TenantId == tenantId) - .SingleOrDefaultAsync(); - - // 2a. Existence Check - if (existingProject == null) - { - _logger.LogWarning("Attempt to update non-existent project with ID {ProjectId} by user {UserId}.", id, loggedInEmployee.Id); - return NotFound(ApiResponse.ErrorResponse("Project not found.", $"No project found with ID {id}.", 404)); - } - - // 2b. Security Check - var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, id); - if (!hasPermission) - { - _logger.LogWarning("Access DENIED for user {UserId} attempting to update project {ProjectId}.", loggedInEmployee.Id, id); - return StatusCode(403, (ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to modify this project.", 403))); - } - - // --- Step 3: Apply Changes and Save --- - // Map the changes from the DTO onto the entity we just fetched from the database. - // This only modifies the properties defined in the mapping, preventing data loss. - _mapper.Map(updateProjectDto, existingProject); - - // Mark the entity as modified (if your mapping doesn't do it automatically). - _context.Entry(existingProject).State = EntityState.Modified; - - try - { - await _context.SaveChangesAsync(); - _logger.LogInfo("Successfully updated project {ProjectId} by user {UserId}.", id, loggedInEmployee.Id); - } - catch (DbUpdateConcurrencyException ex) - { - // --- Step 4: Handle Concurrency Conflicts --- - // This happens if another user modified the project after we fetched it. - _logger.LogWarning("Concurrency conflict while updating project {ProjectId} \n {Error}", id, ex.Message); - return Conflict(ApiResponse.ErrorResponse("Conflict occurred.", "This project has been modified by someone else. Please refresh and try again.", 409)); - } - - // --- Step 5: Perform Side-Effects in the Background (Fire and Forget) --- - // The core database operation is done. Now, we perform non-blocking cache and notification updates. - _ = Task.Run(async () => - { - // Create a DTO of the updated project to pass to background tasks. - var projectDto = _mapper.Map(existingProject); - - // 5a. Update Cache - await UpdateCacheInBackground(existingProject); - - // 5b. Send Targeted SignalR Notification - var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Update_Project", Response = projectDto }; - await SendNotificationInBackground(notification, projectDto.Id); - }); - - // --- Step 6: Return Success Response Immediately --- - // The client gets a fast response without waiting for caching or SignalR. - return Ok(ApiResponse.SuccessResponse(_mapper.Map(existingProject), "Project updated successfully.", 200)); - } - catch (Exception ex) - { - // --- Step 7: Graceful Error Handling for Unexpected Errors --- - _logger.LogError("An unexpected error occurred while updating project {ProjectId} \n {Error}", id, ex.Message); - return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred.", null, 500)); + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Update_Project", Response = response.Data }; + await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); } + return StatusCode(response.StatusCode, response); } #endregion @@ -1367,241 +982,5 @@ namespace MarcoBMS.Services.Controllers #endregion - #region =================================================================== Helper Functions =================================================================== - - /// - /// 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; - } - - private async Task GetProjectViewModel(Guid? id, Project project) - { - ProjectDetailsVM vm = new ProjectDetailsVM(); - - // List buildings = _unitOfWork.Building.GetAll(c => c.ProjectId == id).ToList(); - List buildings = await _context.Buildings.Where(c => c.ProjectId == id).ToListAsync(); - List idList = buildings.Select(o => o.Id).ToList(); - // List floors = _unitOfWork.Floor.GetAll(c => idList.Contains(c.Id)).ToList(); - List floors = await _context.Floor.Where(c => idList.Contains(c.BuildingId)).ToListAsync(); - idList = floors.Select(o => o.Id).ToList(); - //List workAreas = _unitOfWork.WorkArea.GetAll(c => idList.Contains(c.Id), includeProperties: "WorkItems,WorkItems.ActivityMaster").ToList(); - - List workAreas = await _context.WorkAreas.Where(c => idList.Contains(c.FloorId)).ToListAsync(); - - idList = workAreas.Select(o => o.Id).ToList(); - List workItems = await _context.WorkItems.Include(c => c.WorkCategoryMaster).Where(c => idList.Contains(c.WorkAreaId)).Include(c => c.ActivityMaster).ToListAsync(); - // List workItems = _unitOfWork.WorkItem.GetAll(c => idList.Contains(c.WorkAreaId), includeProperties: "ActivityMaster").ToList(); - idList = workItems.Select(t => t.Id).ToList(); - List tasks = await _context.TaskAllocations.Where(t => idList.Contains(t.WorkItemId) && t.AssignmentDate.Date == DateTime.UtcNow.Date).ToListAsync(); - vm.project = project; - vm.buildings = buildings; - vm.floors = floors; - vm.workAreas = workAreas; - vm.workItems = workItems; - vm.Tasks = tasks; - return vm; - } - - /// - /// Fetches project details from the database for a given list of project IDs and assembles them into MongoDB models. - /// This method encapsulates the optimized, parallel database queries. - /// - /// The list of project IDs to fetch. - /// The current tenant ID for filtering. - /// A list of fully populated ProjectMongoDB objects. - private async Task> FetchAndBuildProjectDetails(List projectIdsToFetch, Guid tenantId) - { - // Task to get base project details for the MISSING projects - var projectsTask = Task.Run(async () => - { - using var context = _dbContextFactory.CreateDbContext(); - return await context.Projects.AsNoTracking() - .Where(p => projectIdsToFetch.Contains(p.Id) && p.TenantId == tenantId) - .ToListAsync(); - }); - - // Task to get team sizes for the MISSING projects - var teamSizesTask = Task.Run(async () => - { - using var context = _dbContextFactory.CreateDbContext(); - return await context.ProjectAllocations.AsNoTracking() - .Where(pa => pa.TenantId == tenantId && projectIdsToFetch.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); - }); - - // Task to get work summaries for the MISSING projects - var workSummariesTask = Task.Run(async () => - { - using var context = _dbContextFactory.CreateDbContext(); - return await context.WorkItems.AsNoTracking() - .Where(wi => wi.TenantId == tenantId && - wi.WorkArea != null && - wi.WorkArea.Floor != null && - wi.WorkArea.Floor.Building != null && - projectIdsToFetch.Contains(wi.WorkArea.Floor.Building.ProjectId)) - .GroupBy(wi => wi.WorkArea!.Floor!.Building!.ProjectId) - .Select(g => new { ProjectId = g.Key, PlannedWork = g.Sum(i => i.PlannedWork), CompletedWork = g.Sum(i => i.CompletedWork) }) - .ToDictionaryAsync(x => x.ProjectId); - }); - - // Await all parallel tasks to complete - await Task.WhenAll(projectsTask, teamSizesTask, workSummariesTask); - - var projects = await projectsTask; - var teamSizes = await teamSizesTask; - var workSummaries = await workSummariesTask; - - // Proactively update the cache with the items we just fetched. - _logger.LogInfo("Updating cache with {NewItemCount} newly fetched projects.", projects.Count); - await _cache.AddProjectDetailsList(projects); - - // This section would build the full ProjectMongoDB objects, similar to your AddProjectDetailsList method. - // For brevity, assuming you have a mapper or a builder for this. Here's a simplified representation: - var mongoDetailsList = new List(); - foreach (var project in projects) - { - // This is a placeholder for the full build logic from your other methods. - // In a real scenario, you would fetch all hierarchy levels (buildings, floors, etc.) - // for the `projectIdsToFetch` and build the complete MongoDB object. - var mongoDetail = _mapper.Map(project); - mongoDetail.Id = project.Id; - mongoDetail.TeamSize = teamSizes.GetValueOrDefault(project.Id, 0); - if (workSummaries.TryGetValue(project.Id, out var summary)) - { - mongoDetail.PlannedWork = summary.PlannedWork; - mongoDetail.CompletedWork = summary.CompletedWork; - } - mongoDetailsList.Add(mongoDetail); - } - - return mongoDetailsList; - } - - /// - /// Private helper to encapsulate the cache-first data retrieval logic. - /// - /// A ProjectDetailVM if found, otherwise null. - private async Task GetProjectDataAsync(Guid projectId) - { - // --- Cache First --- - _logger.LogDebug("Attempting to fetch project {ProjectId} from cache.", projectId); - var cachedProject = await _cache.GetProjectDetails(projectId); - if (cachedProject != null) - { - _logger.LogInfo("Cache HIT for project {ProjectId}.", projectId); - // Map from the cache model (e.g., ProjectMongoDB) to the response ViewModel. - return _mapper.Map(cachedProject); - } - - // --- Database Second (on Cache Miss) --- - _logger.LogInfo("Cache MISS for project {ProjectId}. Fetching from database.", projectId); - var dbProject = await _context.Projects - .AsNoTracking() // Use AsNoTracking for read-only queries. - .Where(p => p.Id == projectId && p.TenantId == tenantId) - .SingleOrDefaultAsync(); - - if (dbProject == null) - { - return null; // The project doesn't exist. - } - - // --- Proactively Update Cache --- - // The next request for this project will now be a cache hit. - try - { - // Map the DB entity to the cache model (e.g., ProjectMongoDB) before caching. - await _cache.AddProjectDetails(dbProject); - _logger.LogInfo("Updated cache with project {ProjectId}.", projectId); - } - catch (Exception ex) - { - _logger.LogWarning("Failed to update cache for project {ProjectId} : \n {Error}", projectId, ex.Message); - } - - // Map from the database entity to the response ViewModel. - return dbProject; - } - - // Helper method for background cache update - private async Task UpdateCacheInBackground(Project project) - { - try - { - // This logic can be more complex, but the idea is to update or add. - if (!await _cache.UpdateProjectDetailsOnly(project)) - { - await _cache.AddProjectDetails(project); - } - _logger.LogInfo("Background cache update succeeded for project {ProjectId}.", project.Id); - } - catch (Exception ex) - { - _logger.LogError("Background cache update failed for project {ProjectId} \n {Error}", project.Id, ex.Message); - } - } - - // Helper method for background notification - private async Task SendNotificationInBackground(object notification, Guid projectId) - { - try - { - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); - _logger.LogInfo("Background SignalR notification sent for project {ProjectId}.", projectId); - } - catch (Exception ex) - { - _logger.LogError("Background SignalR notification failed for project {ProjectId} \n {Error}", projectId, ex.Message); - } - } - - #endregion } } \ No newline at end of file diff --git a/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs b/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs index 18db7ff..b811056 100644 --- a/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs @@ -39,6 +39,7 @@ namespace Marco.Pms.Services.MappingProfiles CreateMap(); CreateMap(); + CreateMap(); } } } diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index 7fa2647..6553745 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -6,6 +6,7 @@ using Marco.Pms.Model.Utilities; using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Service; +using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Middleware; using MarcoBMS.Services.Service; @@ -154,8 +155,13 @@ builder.Services.AddTransient(); builder.Services.AddTransient(); // Scoped services (one instance per HTTP request) +#region Customs Services builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +#endregion + +#region Helpers builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -164,9 +170,13 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +#endregion + +#region Cache Services builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +#endregion // Singleton services (one instance for the app's lifetime) builder.Services.AddSingleton(); diff --git a/Marco.Pms.Services/Service/PermissionServices.cs b/Marco.Pms.Services/Service/PermissionServices.cs index f20a768..9758a5f 100644 --- a/Marco.Pms.Services/Service/PermissionServices.cs +++ b/Marco.Pms.Services/Service/PermissionServices.cs @@ -37,7 +37,7 @@ namespace Marco.Pms.Services.Service if (projectIds == null) { - var hasPermission = await HasPermission(employeeId, PermissionsMaster.ManageProject); + var hasPermission = await HasPermission(PermissionsMaster.ManageProject, employeeId); if (hasPermission) { var projects = await _context.Projects.Where(c => c.TenantId == LoggedInEmployee.TenantId).ToListAsync(); @@ -45,12 +45,12 @@ namespace Marco.Pms.Services.Service } else { - var allocation = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.IsActive == true).ToListAsync(); - if (allocation.Any()) + var allocation = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.IsActive).ToListAsync(); + if (!allocation.Any()) { - projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); + return false; } - return false; + projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); } await _cache.AddProjects(LoggedInEmployee.Id, projectIds); } diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs new file mode 100644 index 0000000..3280558 --- /dev/null +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -0,0 +1,691 @@ +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.MongoDBModels; +using Marco.Pms.Model.Projects; +using Marco.Pms.Model.Utilities; +using Marco.Pms.Model.ViewModels.Projects; +using Marco.Pms.Services.Helpers; +using Marco.Pms.Services.Service.ServiceInterfaces; +using MarcoBMS.Services.Helpers; +using MarcoBMS.Services.Service; +using Microsoft.EntityFrameworkCore; + +namespace Marco.Pms.Services.Service +{ + public class ProjectServices : IProjectServices + { + private readonly IDbContextFactory _dbContextFactory; + private readonly ApplicationDbContext _context; // Keeping this for direct scoped context use where appropriate + private readonly ILoggingService _logger; + private readonly ProjectsHelper _projectsHelper; + private readonly PermissionServices _permission; + private readonly CacheUpdateHelper _cache; + private readonly IMapper _mapper; + public ProjectServices( + IDbContextFactory dbContextFactory, + ApplicationDbContext context, + ILoggingService logger, + ProjectsHelper projectsHelper, + PermissionServices permission, + CacheUpdateHelper cache, + IMapper mapper) + { + _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); + _context = context ?? throw new ArgumentNullException(nameof(context)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _projectsHelper = projectsHelper ?? throw new ArgumentNullException(nameof(projectsHelper)); + _permission = permission ?? throw new ArgumentNullException(nameof(permission)); + _cache = cache ?? throw new ArgumentNullException(nameof(cache)); + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + } + #region =================================================================== Project Get APIs =================================================================== + + public async Task> GetAllProjectsBasicAsync(Guid tenantId, Employee loggedInEmployee) + { + try + { + // Step 1: Verify the current user + if (loggedInEmployee == null) + { + return 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 + List accessibleProjectIds = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); + + if (accessibleProjectIds == null || !accessibleProjectIds.Any()) + { + _logger.LogInfo("No accessible projects found for EmployeeId {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.SuccessResponse(new List(), "0 records of project fetchd successfully", 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 ApiResponse.SuccessResponse(projectVMs, $"{projectVMs.Count} records of project fetchd successfully", 200); + } + catch (Exception ex) + { + // --- Step 5: Graceful Error Handling --- + _logger.LogError("An unexpected error occurred in GetAllProjectsBasic for tenant {TenantId}. \n {Error}", tenantId, ex.Message); + return ApiResponse.ErrorResponse("An internal server error occurred. Please try again later.", null, 500); + } + } + + public async Task> GetAllProjectsAsync(Guid tenantId, Employee loggedInEmployee) + { + try + { + _logger.LogInfo("Starting GetAllProjects for TenantId: {TenantId}, User: {UserId}", tenantId, loggedInEmployee.Id); + + // --- Step 1: Get a list of project IDs the user can access --- + List projectIds = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); + if (!projectIds.Any()) + { + _logger.LogInfo("User has no assigned projects. Returning empty list."); + return ApiResponse.SuccessResponse(new List(), "No projects found for the current user.", 200); + } + + // --- Step 2: Efficiently handle partial cache hits --- + _logger.LogInfo("Attempting to fetch details for {ProjectCount} projects from cache.", projectIds.Count); + + // Fetch what we can from the cache. + var cachedDetails = await _cache.GetProjectDetailsList(projectIds) ?? new List(); + var cachedDictionary = cachedDetails.ToDictionary(p => Guid.Parse(p.Id)); + + // Identify which projects are missing from the cache. + var missingIds = projectIds.Where(id => !cachedDictionary.ContainsKey(id)).ToList(); + + // Start building the response with the items we found in the cache. + var responseVms = _mapper.Map>(cachedDictionary.Values); + + if (missingIds.Any()) + { + // --- Step 3: Fetch ONLY the missing items from the database --- + _logger.LogInfo("Cache partial MISS. Found {CachedCount}, fetching {MissingCount} projects from DB.", + cachedDictionary.Count, missingIds.Count); + + // Call our dedicated data-fetching method for the missing IDs. + var newMongoDetails = await FetchAndBuildProjectDetails(missingIds, tenantId); + + if (newMongoDetails.Any()) + { + // Map the newly fetched items and add them to our response list. + responseVms.AddRange(newMongoDetails); + } + } + else + { + _logger.LogInfo("Cache HIT. All {ProjectCount} projects found in cache.", projectIds.Count); + } + + // --- Step 4: Return the combined result --- + _logger.LogInfo("Successfully retrieved a total of {ProjectCount} projects.", responseVms.Count); + return ApiResponse.SuccessResponse(responseVms, "Projects retrieved successfully.", 200); + } + catch (Exception ex) + { + // --- Step 5: Graceful Error Handling --- + _logger.LogError("An unexpected error occurred in GetAllProjects for tenant {TenantId}. \n {Error}", tenantId, ex.Message); + return ApiResponse.ErrorResponse("An internal server error occurred. Please try again later.", null, 500); + } + } + + public async Task> GetProjectAsync(Guid id, Guid tenantId, Employee loggedInEmployee) + { + try + { + // --- Step 1: Run independent operations in PARALLEL --- + // We can check permissions and fetch data at the same time to reduce latency. + var permissionTask = _permission.HasProjectPermission(loggedInEmployee, id); + + // This helper method encapsulates the "cache-first, then database" logic. + var projectDataTask = GetProjectDataAsync(id, tenantId); + + // Await both tasks to complete. + await Task.WhenAll(permissionTask, projectDataTask); + + var hasPermission = await permissionTask; + var projectVm = await projectDataTask; + + // --- Step 2: Process results sequentially --- + + // 2a. Check for permission first. Forbid() is the idiomatic way to return 403. + if (!hasPermission) + { + _logger.LogWarning("Access denied for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, id); + return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to access this project.", 403); + } + + // 2b. Check if the project was found (either in cache or DB). + if (projectVm == null) + { + _logger.LogInfo("Project with ID {ProjectId} not found.", id); + return ApiResponse.ErrorResponse("Project not found.", $"No project found with ID {id}.", 404); + } + + // 2c. Success. Return the consistent ViewModel. + _logger.LogInfo("Successfully retrieved project {ProjectId}.", id); + return ApiResponse.SuccessResponse(projectVm, "Project retrieved successfully.", 200); + } + catch (Exception ex) + { + _logger.LogError("An unexpected error occurred while getting project {ProjectId} : \n {Error}", id, ex.Message); + return ApiResponse.ErrorResponse("An internal server error occurred.", null, 500); + } + } + + public async Task> GetProjectDetailsAsync(Guid id, Guid tenantId, Employee loggedInEmployee) + { + try + { + _logger.LogInfo("Details requested by EmployeeId: {EmployeeId} for ProjectId: {ProjectId}", loggedInEmployee.Id, id); + + // Step 1: Check global view project permission + var hasViewProjectPermission = await _permission.HasPermission(PermissionsMaster.ViewProject, loggedInEmployee.Id); + if (!hasViewProjectPermission) + { + _logger.LogWarning("ViewProjects permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Access denied", "You don't have permission to view projects", 403); + } + + // Step 2: Check permission for this specific project + var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, id); + if (!hasProjectPermission) + { + _logger.LogWarning("Project-specific access denied. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployee.Id, id); + return ApiResponse.ErrorResponse("Access denied", "You don't have access to this project", 403); + } + + // Step 3: Fetch project with status + var projectDetails = await _cache.GetProjectDetails(id); + ProjectVM? projectVM = null; + if (projectDetails == null) + { + var project = await _context.Projects + .Include(c => c.ProjectStatus) + .FirstOrDefaultAsync(c => c.TenantId == tenantId && c.Id == id); + + projectVM = _mapper.Map(project); + + if (project != null) + { + await _cache.AddProjectDetails(project); + } + } + else + { + projectVM = _mapper.Map(projectDetails); + if (projectVM.ProjectStatus != null) + { + projectVM.ProjectStatus.TenantId = tenantId; + } + } + + if (projectVM == null) + { + _logger.LogWarning("Project not found. ProjectId: {ProjectId}", id); + return ApiResponse.ErrorResponse("Project not found", "Project not found", 404); + } + + // Step 4: Return result + + _logger.LogInfo("Project details fetched successfully. ProjectId: {ProjectId}", id); + return ApiResponse.SuccessResponse(projectVM, "Project details fetched successfully", 200); + } + catch (Exception ex) + { + // --- Step 5: Graceful Error Handling --- + _logger.LogError("An unexpected error occurred in Get Project Details for project {ProjectId} for tenant {TenantId}. \n {Error}", id, tenantId, ex.Message); + return ApiResponse.ErrorResponse("An internal server error occurred. Please try again later.", null, 500); + } + } + + public async Task> GetProjectDetailsOldAsync(Guid id, Guid tenantId, Employee loggedInEmployee) + { + var project = await _context.Projects + .Where(c => c.TenantId == tenantId && c.Id == id) + .Include(c => c.ProjectStatus) + .SingleOrDefaultAsync(); + + if (project == null) + { + return ApiResponse.ErrorResponse("Project not found", "Project not found", 404); + + } + else + { + ProjectDetailsVM vm = await GetProjectViewModel(id, project); + + OldProjectVM projectVM = new OldProjectVM(); + if (vm.project != null) + { + projectVM.Id = vm.project.Id; + projectVM.Name = vm.project.Name; + projectVM.ShortName = vm.project.ShortName; + projectVM.ProjectAddress = vm.project.ProjectAddress; + projectVM.ContactPerson = vm.project.ContactPerson; + projectVM.StartDate = vm.project.StartDate; + projectVM.EndDate = vm.project.EndDate; + projectVM.ProjectStatusId = vm.project.ProjectStatusId; + } + projectVM.Buildings = new List(); + if (vm.buildings != null) + { + foreach (Building build in vm.buildings) + { + BuildingVM buildVM = new BuildingVM() { Id = build.Id, Description = build.Description, Name = build.Name }; + buildVM.Floors = new List(); + if (vm.floors != null) + { + foreach (Floor floorDto in vm.floors.Where(c => c.BuildingId == build.Id).ToList()) + { + FloorsVM floorVM = new FloorsVM() { FloorName = floorDto.FloorName, Id = floorDto.Id }; + floorVM.WorkAreas = new List(); + + if (vm.workAreas != null) + { + foreach (WorkArea workAreaDto in vm.workAreas.Where(c => c.FloorId == floorVM.Id).ToList()) + { + WorkAreaVM workAreaVM = new WorkAreaVM() { Id = workAreaDto.Id, AreaName = workAreaDto.AreaName, WorkItems = new List() }; + + if (vm.workItems != null) + { + foreach (WorkItem workItemDto in vm.workItems.Where(c => c.WorkAreaId == workAreaDto.Id).ToList()) + { + WorkItemVM workItemVM = new WorkItemVM() { WorkItemId = workItemDto.Id, WorkItem = workItemDto }; + + workItemVM.WorkItem.WorkArea = new WorkArea(); + + if (workItemVM.WorkItem.ActivityMaster != null) + { + workItemVM.WorkItem.ActivityMaster.Tenant = new Tenant(); + } + workItemVM.WorkItem.Tenant = new Tenant(); + + double todaysAssigned = 0; + if (vm.Tasks != null) + { + var tasks = vm.Tasks.Where(t => t.WorkItemId == workItemDto.Id).ToList(); + foreach (TaskAllocation task in tasks) + { + todaysAssigned += task.PlannedTask; + } + } + workItemVM.TodaysAssigned = todaysAssigned; + + workAreaVM.WorkItems.Add(workItemVM); + } + } + + floorVM.WorkAreas.Add(workAreaVM); + } + } + + buildVM.Floors.Add(floorVM); + } + } + projectVM.Buildings.Add(buildVM); + } + } + return ApiResponse.SuccessResponse(projectVM, "Success.", 200); + } + } + + #endregion + + #region =================================================================== Project Manage APIs =================================================================== + + public async Task> CreateProjectAsync(CreateProjectDto projectDto, Guid tenantId, Employee loggedInEmployee) + { + // 1. Prepare data without I/O + var loggedInUserId = loggedInEmployee.Id; + var project = _mapper.Map(projectDto); + project.TenantId = tenantId; + + // 2. Store it to database + try + { + _context.Projects.Add(project); + await _context.SaveChangesAsync(); + } + catch (Exception ex) + { + // Log the detailed exception + _logger.LogError("Failed to create project in database. Rolling back transaction. \n {Error}", ex.Message); + // Return a server error as the primary operation failed + return ApiResponse.ErrorResponse("An error occurred while saving the project.", ex.Message, 500); + } + + // 3. Perform non-critical side-effects (caching, notifications) concurrently + try + { + // These operations do not depend on each other, so they can run in parallel. + Task cacheAddDetailsTask = _cache.AddProjectDetails(project); + Task cacheClearListTask = _cache.ClearAllProjectIdsByPermissionId(PermissionsMaster.ManageProject); + + // Await all side-effect tasks to complete in parallel + await Task.WhenAll(cacheAddDetailsTask, cacheClearListTask); + } + catch (Exception ex) + { + // The project was created successfully, but a side-effect failed. + // Log this as a warning, as the primary operation succeeded. Don't return an error to the user. + _logger.LogWarning("Project {ProjectId} was created, but a post-creation side-effect (caching/notification) failed. \n {Error}", project.Id, ex.Message); + } + + // 4. Return a success response to the user as soon as the critical data is saved. + return ApiResponse.SuccessResponse(_mapper.Map(project), "Project created successfully.", 200); + } + + /// + /// Updates an existing project's details. + /// This endpoint is secure, handles concurrency, and performs non-essential tasks in the background. + /// + /// The ID of the project to update. + /// The data to update the project with. + /// An ApiResponse confirming the update or an appropriate error. + public async Task> UpdateProjectAsync(Guid id, UpdateProjectDto updateProjectDto, Guid tenantId, Employee loggedInEmployee) + { + try + { + // --- Step 1: Fetch the Existing Entity from the Database --- + // This is crucial to avoid the data loss bug. We only want to modify an existing record. + var existingProject = await _context.Projects + .Where(p => p.Id == id && p.TenantId == tenantId) + .SingleOrDefaultAsync(); + + // 1a. Existence Check + if (existingProject == null) + { + _logger.LogWarning("Attempt to update non-existent project with ID {ProjectId} by user {UserId}.", id, loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Project not found.", $"No project found with ID {id}.", 404); + } + + // 1b. Security Check + var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, id); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} attempting to update project {ProjectId}.", loggedInEmployee.Id, id); + return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to modify this project.", 403); + } + + // --- Step 2: Apply Changes and Save --- + // Map the changes from the DTO onto the entity we just fetched from the database. + // This only modifies the properties defined in the mapping, preventing data loss. + _mapper.Map(updateProjectDto, existingProject); + + // Mark the entity as modified (if your mapping doesn't do it automatically). + _context.Entry(existingProject).State = EntityState.Modified; + + try + { + await _context.SaveChangesAsync(); + _logger.LogInfo("Successfully updated project {ProjectId} by user {UserId}.", id, loggedInEmployee.Id); + } + catch (DbUpdateConcurrencyException ex) + { + // --- Step 3: Handle Concurrency Conflicts --- + // This happens if another user modified the project after we fetched it. + _logger.LogWarning("Concurrency conflict while updating project {ProjectId} \n {Error}", id, ex.Message); + return ApiResponse.ErrorResponse("Conflict occurred.", "This project has been modified by someone else. Please refresh and try again.", 409); + } + + // --- Step 4: Perform Side-Effects in the Background (Fire and Forget) --- + // The core database operation is done. Now, we perform non-blocking cache and notification updates. + _ = Task.Run(async () => + { + // Create a DTO of the updated project to pass to background tasks. + var projectDto = _mapper.Map(existingProject); + + // 4a. Update Cache + await UpdateCacheInBackground(existingProject); + + }); + + // --- Step 5: Return Success Response Immediately --- + // The client gets a fast response without waiting for caching or SignalR. + return ApiResponse.SuccessResponse(_mapper.Map(existingProject), "Project updated successfully.", 200); + } + catch (Exception ex) + { + // --- Step 6: Graceful Error Handling for Unexpected Errors --- + _logger.LogError("An unexpected error occurred while updating project {ProjectId} \n {Error}", id, ex.Message); + return ApiResponse.ErrorResponse("An internal server error occurred.", null, 500); + } + } + + #endregion + + #region =================================================================== Helper Functions =================================================================== + + /// + /// 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; + } + + private async Task GetProjectViewModel(Guid? id, Project project) + { + ProjectDetailsVM vm = new ProjectDetailsVM(); + + // List buildings = _unitOfWork.Building.GetAll(c => c.ProjectId == id).ToList(); + List buildings = await _context.Buildings.Where(c => c.ProjectId == id).ToListAsync(); + List idList = buildings.Select(o => o.Id).ToList(); + // List floors = _unitOfWork.Floor.GetAll(c => idList.Contains(c.Id)).ToList(); + List floors = await _context.Floor.Where(c => idList.Contains(c.BuildingId)).ToListAsync(); + idList = floors.Select(o => o.Id).ToList(); + //List workAreas = _unitOfWork.WorkArea.GetAll(c => idList.Contains(c.Id), includeProperties: "WorkItems,WorkItems.ActivityMaster").ToList(); + + List workAreas = await _context.WorkAreas.Where(c => idList.Contains(c.FloorId)).ToListAsync(); + + idList = workAreas.Select(o => o.Id).ToList(); + List workItems = await _context.WorkItems.Include(c => c.WorkCategoryMaster).Where(c => idList.Contains(c.WorkAreaId)).Include(c => c.ActivityMaster).ToListAsync(); + // List workItems = _unitOfWork.WorkItem.GetAll(c => idList.Contains(c.WorkAreaId), includeProperties: "ActivityMaster").ToList(); + idList = workItems.Select(t => t.Id).ToList(); + List tasks = await _context.TaskAllocations.Where(t => idList.Contains(t.WorkItemId) && t.AssignmentDate.Date == DateTime.UtcNow.Date).ToListAsync(); + vm.project = project; + vm.buildings = buildings; + vm.floors = floors; + vm.workAreas = workAreas; + vm.workItems = workItems; + vm.Tasks = tasks; + return vm; + } + + /// + /// Fetches project details from the database for a given list of project IDs and assembles them into MongoDB models. + /// This method encapsulates the optimized, parallel database queries. + /// + /// The list of project IDs to fetch. + /// The current tenant ID for filtering. + /// A list of fully populated ProjectMongoDB objects. + private async Task> FetchAndBuildProjectDetails(List projectIdsToFetch, Guid tenantId) + { + // Task to get base project details for the MISSING projects + var projectsTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + return await context.Projects.AsNoTracking() + .Where(p => projectIdsToFetch.Contains(p.Id) && p.TenantId == tenantId) + .ToListAsync(); + }); + + // Task to get team sizes for the MISSING projects + var teamSizesTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + return await context.ProjectAllocations.AsNoTracking() + .Where(pa => pa.TenantId == tenantId && projectIdsToFetch.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); + }); + + // Task to get work summaries for the MISSING projects + var workSummariesTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + return await context.WorkItems.AsNoTracking() + .Where(wi => wi.TenantId == tenantId && + wi.WorkArea != null && + wi.WorkArea.Floor != null && + wi.WorkArea.Floor.Building != null && + projectIdsToFetch.Contains(wi.WorkArea.Floor.Building.ProjectId)) + .GroupBy(wi => wi.WorkArea!.Floor!.Building!.ProjectId) + .Select(g => new { ProjectId = g.Key, PlannedWork = g.Sum(i => i.PlannedWork), CompletedWork = g.Sum(i => i.CompletedWork) }) + .ToDictionaryAsync(x => x.ProjectId); + }); + + // Await all parallel tasks to complete + await Task.WhenAll(projectsTask, teamSizesTask, workSummariesTask); + + var projects = await projectsTask; + var teamSizes = await teamSizesTask; + var workSummaries = await workSummariesTask; + + // Proactively update the cache with the items we just fetched. + _logger.LogInfo("Updating cache with {NewItemCount} newly fetched projects.", projects.Count); + await _cache.AddProjectDetailsList(projects); + + // This section would build the full ProjectMongoDB objects, similar to your AddProjectDetailsList method. + // For brevity, assuming you have a mapper or a builder for this. Here's a simplified representation: + var mongoDetailsList = new List(); + foreach (var project in projects) + { + // This is a placeholder for the full build logic from your other methods. + // In a real scenario, you would fetch all hierarchy levels (buildings, floors, etc.) + // for the `projectIdsToFetch` and build the complete MongoDB object. + var mongoDetail = _mapper.Map(project); + mongoDetail.Id = project.Id; + mongoDetail.TeamSize = teamSizes.GetValueOrDefault(project.Id, 0); + if (workSummaries.TryGetValue(project.Id, out var summary)) + { + mongoDetail.PlannedWork = summary.PlannedWork; + mongoDetail.CompletedWork = summary.CompletedWork; + } + mongoDetailsList.Add(mongoDetail); + } + + return mongoDetailsList; + } + + /// + /// Private helper to encapsulate the cache-first data retrieval logic. + /// + /// A ProjectDetailVM if found, otherwise null. + private async Task GetProjectDataAsync(Guid projectId, Guid tenantId) + { + // --- Cache First --- + _logger.LogDebug("Attempting to fetch project {ProjectId} from cache.", projectId); + var cachedProject = await _cache.GetProjectDetails(projectId); + if (cachedProject != null) + { + _logger.LogInfo("Cache HIT for project {ProjectId}.", projectId); + // Map from the cache model (e.g., ProjectMongoDB) to the response ViewModel. + return _mapper.Map(cachedProject); + } + + // --- Database Second (on Cache Miss) --- + _logger.LogInfo("Cache MISS for project {ProjectId}. Fetching from database.", projectId); + var dbProject = await _context.Projects + .AsNoTracking() // Use AsNoTracking for read-only queries. + .Where(p => p.Id == projectId && p.TenantId == tenantId) + .SingleOrDefaultAsync(); + + if (dbProject == null) + { + return null; // The project doesn't exist. + } + + // --- Proactively Update Cache --- + // The next request for this project will now be a cache hit. + try + { + // Map the DB entity to the cache model (e.g., ProjectMongoDB) before caching. + await _cache.AddProjectDetails(dbProject); + _logger.LogInfo("Updated cache with project {ProjectId}.", projectId); + } + catch (Exception ex) + { + _logger.LogWarning("Failed to update cache for project {ProjectId} : \n {Error}", projectId, ex.Message); + } + + // Map from the database entity to the response ViewModel. + return dbProject; + } + + // Helper method for background cache update + private async Task UpdateCacheInBackground(Project project) + { + try + { + // This logic can be more complex, but the idea is to update or add. + if (!await _cache.UpdateProjectDetailsOnly(project)) + { + await _cache.AddProjectDetails(project); + } + _logger.LogInfo("Background cache update succeeded for project {ProjectId}.", project.Id); + } + catch (Exception ex) + { + _logger.LogError("Background cache update failed for project {ProjectId} \n {Error}", project.Id, ex.Message); + } + } + + #endregion + } +} diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs new file mode 100644 index 0000000..a23eba0 --- /dev/null +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs @@ -0,0 +1,17 @@ +using Marco.Pms.Model.Dtos.Project; +using Marco.Pms.Model.Employees; +using Marco.Pms.Model.Utilities; + +namespace Marco.Pms.Services.Service.ServiceInterfaces +{ + public interface IProjectServices + { + Task> GetAllProjectsBasicAsync(Guid tenantId, Employee loggedInEmployee); + Task> GetAllProjectsAsync(Guid tenantId, Employee loggedInEmployee); + Task> GetProjectAsync(Guid id, Guid tenantId, Employee loggedInEmployee); + Task> GetProjectDetailsAsync(Guid id, Guid tenantId, Employee loggedInEmployee); + Task> GetProjectDetailsOldAsync(Guid id, Guid tenantId, Employee loggedInEmployee); + Task> CreateProjectAsync(CreateProjectDto projectDto, Guid tenantId, Employee loggedInEmployee); + Task> UpdateProjectAsync(Guid id, UpdateProjectDto updateProjectDto, Guid tenantId, Employee loggedInEmployee); + } +} From 560d2f2d4dc2c7b8ee3a93ac4974a93132aa9f21 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 15 Jul 2025 12:44:38 +0530 Subject: [PATCH 101/124] adde functionality to delete workItems from cache --- .../Controllers/AttendanceController.cs | 30 +-- .../Controllers/AuthController.cs | 34 +-- .../Controllers/DashboardController.cs | 10 +- .../Controllers/DirectoryController.cs | 4 +- .../Controllers/EmployeeController.cs | 4 +- .../Controllers/ForumController.cs | 30 +-- .../Controllers/MasterController.cs | 48 ++-- .../Controllers/ProjectController.cs | 129 ++++------ .../Controllers/ReportController.cs | 16 +- .../Helpers/CacheUpdateHelper.cs | 8 +- Marco.Pms.Services/Helpers/DirectoryHelper.cs | 18 +- Marco.Pms.Services/Helpers/EmployeeHelper.cs | 6 +- Marco.Pms.Services/Helpers/MasterHelper.cs | 10 +- Marco.Pms.Services/Helpers/ReportHelper.cs | 10 +- Marco.Pms.Services/Helpers/RolesHelper.cs | 4 +- ...ectMappingProfile.cs => MappingProfile.cs} | 12 +- Marco.Pms.Services/Program.cs | 1 + Marco.Pms.Services/Service/ILoggingService.cs | 2 +- Marco.Pms.Services/Service/LoggingServices.cs | 6 +- Marco.Pms.Services/Service/ProjectServices.cs | 227 +++++++++++++++++- .../Service/RefreshTokenService.cs | 14 +- Marco.Pms.Services/Service/S3UploadService.cs | 14 +- .../ServiceInterfaces/IProjectServices.cs | 2 + .../ServiceInterfaces/ISignalRService.cs | 7 + Marco.Pms.Services/Service/SignalRService.cs | 29 +++ 25 files changed, 444 insertions(+), 231 deletions(-) rename Marco.Pms.Services/MappingProfiles/{ProjectMappingProfile.cs => MappingProfile.cs} (75%) create mode 100644 Marco.Pms.Services/Service/ServiceInterfaces/ISignalRService.cs create mode 100644 Marco.Pms.Services/Service/SignalRService.cs diff --git a/Marco.Pms.Services/Controllers/AttendanceController.cs b/Marco.Pms.Services/Controllers/AttendanceController.cs index 4c2f2c1..1a5e4e7 100644 --- a/Marco.Pms.Services/Controllers/AttendanceController.cs +++ b/Marco.Pms.Services/Controllers/AttendanceController.cs @@ -90,18 +90,18 @@ namespace MarcoBMS.Services.Controllers if (dateFrom != null && DateTime.TryParse(dateFrom, out fromDate) == false) { - _logger.LogError("User sent Invalid from Date while featching attendance logs"); + _logger.LogWarning("User sent Invalid from Date while featching attendance logs"); return BadRequest(ApiResponse.ErrorResponse("Invalid Date", "Invalid Date", 400)); } if (dateTo != null && DateTime.TryParse(dateTo, out toDate) == false) { - _logger.LogError("User sent Invalid to Date while featching attendance logs"); + _logger.LogWarning("User sent Invalid to Date while featching attendance logs"); return BadRequest(ApiResponse.ErrorResponse("Invalid Date", "Invalid Date", 400)); } if (employeeId == Guid.Empty) { - _logger.LogError("The employee Id sent by user is empty"); + _logger.LogWarning("The employee Id sent by user is empty"); return BadRequest(ApiResponse.ErrorResponse("Employee ID is required and must not be Empty.", "Employee ID is required and must not be empty.", 400)); } List attendances = await _context.Attendes.Where(c => c.EmployeeID == employeeId && c.TenantId == TenantId && c.AttendanceDate.Date >= fromDate && c.AttendanceDate.Date <= toDate).ToListAsync(); @@ -161,18 +161,18 @@ namespace MarcoBMS.Services.Controllers if (dateFrom != null && DateTime.TryParse(dateFrom, out fromDate) == false) { - _logger.LogError("User sent Invalid fromDate while featching attendance logs"); + _logger.LogWarning("User sent Invalid fromDate while featching attendance logs"); return BadRequest(ApiResponse.ErrorResponse("Invalid Date", "Invalid Date", 400)); } if (dateTo != null && DateTime.TryParse(dateTo, out toDate) == false) { - _logger.LogError("User sent Invalid toDate while featching attendance logs"); + _logger.LogWarning("User sent Invalid toDate while featching attendance logs"); return BadRequest(ApiResponse.ErrorResponse("Invalid Date", "Invalid Date", 400)); } if (projectId == Guid.Empty) { - _logger.LogError("The project Id sent by user is less than or equal to zero"); + _logger.LogWarning("The project Id sent by user is less than or equal to zero"); return BadRequest(ApiResponse.ErrorResponse("Project ID is required and must be greater than zero.", "Project ID is required and must be greater than zero.", 400)); } @@ -276,13 +276,13 @@ namespace MarcoBMS.Services.Controllers if (date != null && DateTime.TryParse(date, out forDate) == false) { - _logger.LogError("User sent Invalid Date while featching attendance logs"); + _logger.LogWarning("User sent Invalid Date while featching attendance logs"); return BadRequest(ApiResponse.ErrorResponse("Invalid Date", "Invalid Date", 400)); } if (projectId == Guid.Empty) { - _logger.LogError("The project Id sent by user is less than or equal to zero"); + _logger.LogWarning("The project Id sent by user is less than or equal to zero"); return BadRequest(ApiResponse.ErrorResponse("Project ID is required and must be greater than zero.", "Project ID is required and must be greater than zero.", 400)); } @@ -425,7 +425,7 @@ namespace MarcoBMS.Services.Controllers .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - _logger.LogError("User sent Invalid Date while marking attendance"); + _logger.LogWarning("User sent Invalid Date while marking attendance \n {Error}", string.Join(",", errors)); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } @@ -439,14 +439,14 @@ namespace MarcoBMS.Services.Controllers if (recordAttendanceDot.MarkTime == null) { - _logger.LogError("User sent Invalid Mark Time while marking attendance"); + _logger.LogWarning("User sent Invalid Mark Time while marking attendance"); return BadRequest(ApiResponse.ErrorResponse("Invalid Mark Time", "Invalid Mark Time", 400)); } DateTime finalDateTime = GetDateFromTimeStamp(recordAttendanceDot.Date, recordAttendanceDot.MarkTime); if (recordAttendanceDot.Comment == null) { - _logger.LogError("User sent Invalid comment while marking attendance"); + _logger.LogWarning("User sent Invalid comment while marking attendance"); return BadRequest(ApiResponse.ErrorResponse("Invalid Comment", "Invalid Comment", 400)); } @@ -480,7 +480,7 @@ namespace MarcoBMS.Services.Controllers } else { - _logger.LogError("Employee {EmployeeId} sent regularization request but it check-out time is earlier than check-out"); + _logger.LogWarning("Employee {EmployeeId} sent regularization request but it check-out time is earlier than check-out"); return BadRequest(ApiResponse.ErrorResponse("Check-out time must be later than check-in time", "Check-out time must be later than check-in time", 400)); } // do nothing @@ -585,7 +585,7 @@ namespace MarcoBMS.Services.Controllers catch (Exception ex) { await transaction.RollbackAsync(); // Rollback on failure - _logger.LogError("{Error} while marking attendance", ex.Message); + _logger.LogError(ex, "An Error occured while marking attendance"); var response = new { message = ex.Message, @@ -604,7 +604,7 @@ namespace MarcoBMS.Services.Controllers if (!ModelState.IsValid) { var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); - _logger.LogError("Invalid attendance model received."); + _logger.LogWarning("Invalid attendance model received. \n {Error}", string.Join(",", errors)); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } @@ -780,7 +780,7 @@ namespace MarcoBMS.Services.Controllers catch (Exception ex) { await transaction.RollbackAsync(); - _logger.LogError("Error while recording attendance : {Error}", ex.Message); + _logger.LogError(ex, "Error while recording attendance"); return BadRequest(ApiResponse.ErrorResponse("Something went wrong", ex.Message, 500)); } } diff --git a/Marco.Pms.Services/Controllers/AuthController.cs b/Marco.Pms.Services/Controllers/AuthController.cs index 1b45eb7..429a38b 100644 --- a/Marco.Pms.Services/Controllers/AuthController.cs +++ b/Marco.Pms.Services/Controllers/AuthController.cs @@ -1,8 +1,4 @@ -using System.Net; -using System.Security.Claims; -using System.Security.Cryptography; -using System.Text; -using Marco.Pms.DataAccess.Data; +using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Authentication; using Marco.Pms.Model.Dtos.Authentication; using Marco.Pms.Model.Dtos.Util; @@ -15,6 +11,10 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using System.Net; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; namespace MarcoBMS.Services.Controllers { @@ -110,7 +110,7 @@ namespace MarcoBMS.Services.Controllers } catch (Exception ex) { - _logger.LogError("Unexpected error during login : {Error}", ex.Message); + _logger.LogError(ex, "Unexpected error during login"); return StatusCode(500, ApiResponse.ErrorResponse("Unexpected error", ex.Message, 500)); } } @@ -270,7 +270,7 @@ namespace MarcoBMS.Services.Controllers } catch (Exception ex) { - _logger.LogError("Unexpected error occurred while verifying MPIN : {Error}", ex.Message); + _logger.LogError(ex, "Unexpected error occurred while verifying MPIN"); return StatusCode(500, ApiResponse.ErrorResponse("Unexpected error", ex.Message, 500)); } } @@ -307,7 +307,7 @@ namespace MarcoBMS.Services.Controllers } catch (Exception ex) { - _logger.LogError("Unexpected error during logout : {Error}", ex.Message); + _logger.LogError(ex, "Unexpected error during logout"); return StatusCode(500, ApiResponse.ErrorResponse("Unexpected error occurred", ex.Message, 500)); } } @@ -351,7 +351,7 @@ namespace MarcoBMS.Services.Controllers if (string.IsNullOrWhiteSpace(user.UserName)) { - _logger.LogError("Username missing for user ID: {UserId}", user.Id); + _logger.LogWarning("Username missing for user ID: {UserId}", user.Id); return NotFound(ApiResponse.ErrorResponse("Username not found.", "Username not found.", 404)); } @@ -370,7 +370,7 @@ namespace MarcoBMS.Services.Controllers } catch (Exception ex) { - _logger.LogError("An unexpected error occurred during token refresh. : {Error}", ex.Message); + _logger.LogError(ex, "An unexpected error occurred during token refresh."); return StatusCode(500, ApiResponse.ErrorResponse("Unexpected error occurred.", ex.Message, 500)); } } @@ -406,7 +406,7 @@ namespace MarcoBMS.Services.Controllers } catch (Exception ex) { - _logger.LogError("Error while sending password reset email to: {Error}", ex.Message); + _logger.LogError(ex, "Error while sending password reset email to"); return StatusCode(500, ApiResponse.ErrorResponse("Error sending password reset email.", ex.Message, 500)); } } @@ -480,7 +480,7 @@ namespace MarcoBMS.Services.Controllers } catch (Exception ex) { - _logger.LogError("Error while sending reset password success email to user: {Error}", ex.Message); + _logger.LogError(ex, "Error while sending reset password success email to user"); // Continue, do not fail because of email issue } @@ -547,7 +547,7 @@ namespace MarcoBMS.Services.Controllers } catch (Exception ex) { - _logger.LogError("An unexpected error occurred while sending OTP to {Email} : {Error}", generateOTP.Email ?? "", ex.Message); + _logger.LogError(ex, "An unexpected error occurred while sending OTP to {Email}", generateOTP.Email ?? ""); return StatusCode(500, ApiResponse.ErrorResponse("An unexpected error occurred.", ex.Message, 500)); } } @@ -638,7 +638,7 @@ namespace MarcoBMS.Services.Controllers } catch (Exception ex) { - _logger.LogError("An unexpected error occurred during OTP login for email {Email} : {Error}", verifyOTP.Email ?? string.Empty, ex.Message); + _logger.LogError(ex, "An unexpected error occurred during OTP login for email {Email}", verifyOTP.Email ?? string.Empty); return StatusCode(500, ApiResponse.ErrorResponse("Unexpected error", ex.Message, 500)); } } @@ -719,7 +719,7 @@ namespace MarcoBMS.Services.Controllers if (!result.Succeeded) { var errors = result.Errors.Select(e => e.Description).ToList(); - _logger.LogError("Password reset failed for user {Email}. Errors: {Errors}", changePassword.Email, string.Join("; ", errors)); + _logger.LogWarning("Password reset failed for user {Email}. Errors: {Errors}", changePassword.Email, string.Join("; ", errors)); return BadRequest(ApiResponse.ErrorResponse("Failed to change password", errors, 400)); } @@ -732,7 +732,7 @@ namespace MarcoBMS.Services.Controllers } catch (Exception exp) { - _logger.LogError("An unexpected error occurred while changing password : {Error}", exp.Message); + _logger.LogError(exp, "An unexpected error occurred while changing password"); return StatusCode(500, ApiResponse.ErrorResponse("An unexpected error occurred.", exp.Message, 500)); } } @@ -752,7 +752,7 @@ namespace MarcoBMS.Services.Controllers // Validate employee and MPIN input if (requestEmployee == null || string.IsNullOrWhiteSpace(generateMPINDto.MPIN) || generateMPINDto.MPIN.Length != 6 || !generateMPINDto.MPIN.All(char.IsDigit)) { - _logger.LogError("Employee {EmployeeId} provided invalid information to generate MPIN", loggedInEmployee.Id); + _logger.LogWarning("Employee {EmployeeId} provided invalid information to generate MPIN", loggedInEmployee.Id); return BadRequest(ApiResponse.ErrorResponse("Provided invalid information", "Provided invalid information", 400)); } diff --git a/Marco.Pms.Services/Controllers/DashboardController.cs b/Marco.Pms.Services/Controllers/DashboardController.cs index f2332df..0e01717 100644 --- a/Marco.Pms.Services/Controllers/DashboardController.cs +++ b/Marco.Pms.Services/Controllers/DashboardController.cs @@ -364,7 +364,7 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Number of pending regularization and pending check-out are fetched successfully for employee {EmployeeId}", LoggedInEmployee.Id); return Ok(ApiResponse.SuccessResponse(response, "Pending regularization and pending check-out are fetched successfully", 200)); } - _logger.LogError("No attendance entry was found for employee {EmployeeId}", LoggedInEmployee.Id); + _logger.LogWarning("No attendance entry was found for employee {EmployeeId}", LoggedInEmployee.Id); return NotFound(ApiResponse.ErrorResponse("No attendance entry was found for this employee", "No attendance entry was found for this employee", 404)); } @@ -378,14 +378,14 @@ namespace Marco.Pms.Services.Controllers List? projectProgressionVMs = new List(); if (date != null && DateTime.TryParse(date, out currentDate) == false) { - _logger.LogError($"user send invalid date"); + _logger.LogWarning($"user send invalid date"); return BadRequest(ApiResponse.ErrorResponse("Invalid date.", "Invalid date.", 400)); } Project? project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == projectId); if (project == null) { - _logger.LogError("Employee {EmployeeId} was attempted to get project attendance for date {Date}, but project not found in database", LoggedInEmployee.Id, currentDate); + _logger.LogWarning("Employee {EmployeeId} was attempted to get project attendance for date {Date}, but project not found in database", LoggedInEmployee.Id, currentDate); return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); } List? projectAllocation = await _context.ProjectAllocations.Where(p => p.ProjectId == projectId && p.IsActive && p.TenantId == tenantId).ToListAsync(); @@ -431,14 +431,14 @@ namespace Marco.Pms.Services.Controllers DateTime currentDate = DateTime.UtcNow; if (date != null && DateTime.TryParse(date, out currentDate) == false) { - _logger.LogError($"user send invalid date"); + _logger.LogWarning($"user send invalid date"); return BadRequest(ApiResponse.ErrorResponse("Invalid date.", "Invalid date.", 400)); } Project? project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == projectId); if (project == null) { - _logger.LogError("Employee {EmployeeId} was attempted to get activities performed for date {Date}, but project not found in database", LoggedInEmployee.Id, currentDate); + _logger.LogWarning("Employee {EmployeeId} was attempted to get activities performed for date {Date}, but project not found in database", LoggedInEmployee.Id, currentDate); return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); } diff --git a/Marco.Pms.Services/Controllers/DirectoryController.cs b/Marco.Pms.Services/Controllers/DirectoryController.cs index 4a0e41e..9eb06e0 100644 --- a/Marco.Pms.Services/Controllers/DirectoryController.cs +++ b/Marco.Pms.Services/Controllers/DirectoryController.cs @@ -77,7 +77,7 @@ namespace Marco.Pms.Services.Controllers .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - _logger.LogError("User sent Invalid Date while marking attendance"); + _logger.LogWarning("User sent Invalid Date while marking attendance"); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } var response = await _directoryHelper.CreateContact(createContact); @@ -256,7 +256,7 @@ namespace Marco.Pms.Services.Controllers .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - _logger.LogError("User sent Invalid Date while marking attendance"); + _logger.LogWarning("User sent Invalid Date while marking attendance"); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } var response = await _directoryHelper.CreateBucket(bucketDto); diff --git a/Marco.Pms.Services/Controllers/EmployeeController.cs b/Marco.Pms.Services/Controllers/EmployeeController.cs index 2f0ca5e..c9e19fa 100644 --- a/Marco.Pms.Services/Controllers/EmployeeController.cs +++ b/Marco.Pms.Services/Controllers/EmployeeController.cs @@ -382,7 +382,7 @@ namespace MarcoBMS.Services.Controllers Employee? existingEmployee = await _context.Employees.FirstOrDefaultAsync(e => e.Id == model.Id.Value); if (existingEmployee == null) { - _logger.LogError("User tries to update employee {EmployeeId} but not found in database", model.Id); + _logger.LogWarning("User tries to update employee {EmployeeId} but not found in database", model.Id); return NotFound(ApiResponse.ErrorResponse("Employee not found", "Employee not found", 404)); } byte[]? imageBytes = null; @@ -495,7 +495,7 @@ namespace MarcoBMS.Services.Controllers } else { - _logger.LogError("Employee with ID {EmploueeId} not found in database", id); + _logger.LogWarning("Employee with ID {EmploueeId} not found in database", id); } return Ok(ApiResponse.SuccessResponse(new { }, "Employee Suspended successfully", 200)); } diff --git a/Marco.Pms.Services/Controllers/ForumController.cs b/Marco.Pms.Services/Controllers/ForumController.cs index 769c08a..fb6d0e7 100644 --- a/Marco.Pms.Services/Controllers/ForumController.cs +++ b/Marco.Pms.Services/Controllers/ForumController.cs @@ -44,7 +44,7 @@ namespace Marco.Pms.Services.Controllers .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - _logger.LogError("{error}", errors); + _logger.LogWarning("{error}", errors); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } Guid tenantId = _userHelper.GetTenantId(); @@ -66,7 +66,7 @@ namespace Marco.Pms.Services.Controllers var Image = attachmentDto; if (string.IsNullOrEmpty(Image.Base64Data)) { - _logger.LogError("Base64 data is missing"); + _logger.LogWarning("Base64 data is missing"); return BadRequest(ApiResponse.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); } @@ -160,7 +160,7 @@ namespace Marco.Pms.Services.Controllers .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - _logger.LogError("{error}", errors); + _logger.LogWarning("{error}", errors); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } Guid tenantId = _userHelper.GetTenantId(); @@ -197,7 +197,7 @@ namespace Marco.Pms.Services.Controllers var Image = attachmentDto; if (string.IsNullOrEmpty(Image.Base64Data)) { - _logger.LogError("Base64 data is missing"); + _logger.LogWarning("Base64 data is missing"); return BadRequest(ApiResponse.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); } @@ -336,7 +336,7 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Ticket {TicketId} updated", updateTicketDto.Id); return Ok(ApiResponse.SuccessResponse(ticketVM, "Ticket Updated Successfully", 200)); } - _logger.LogError("Ticket {TicketId} not Found in database", updateTicketDto.Id); + _logger.LogWarning("Ticket {TicketId} not Found in database", updateTicketDto.Id); return NotFound(ApiResponse.ErrorResponse("Ticket not Found", "Ticket not Found", 404)); } @@ -349,7 +349,7 @@ namespace Marco.Pms.Services.Controllers .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - _logger.LogError("{error}", errors); + _logger.LogWarning("{error}", errors); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } @@ -364,7 +364,7 @@ namespace Marco.Pms.Services.Controllers if (ticket == null) { - _logger.LogError("Ticket {TicketId} not Found in database", addCommentDto.TicketId); + _logger.LogWarning("Ticket {TicketId} not Found in database", addCommentDto.TicketId); return NotFound(ApiResponse.ErrorResponse("Ticket not Found", "Ticket not Found", 404)); } @@ -379,7 +379,7 @@ namespace Marco.Pms.Services.Controllers var Image = attachmentDto; if (string.IsNullOrEmpty(Image.Base64Data)) { - _logger.LogError("Base64 data is missing"); + _logger.LogWarning("Base64 data is missing"); return BadRequest(ApiResponse.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); } @@ -437,7 +437,7 @@ namespace Marco.Pms.Services.Controllers .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - _logger.LogError("{error}", errors); + _logger.LogWarning("{error}", errors); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } @@ -451,7 +451,7 @@ namespace Marco.Pms.Services.Controllers if (ticket == null) { - _logger.LogError("Ticket {TicketId} not Found in database", updateCommentDto.TicketId); + _logger.LogWarning("Ticket {TicketId} not Found in database", updateCommentDto.TicketId); return NotFound(ApiResponse.ErrorResponse("Ticket not Found", "Ticket not Found", 404)); } @@ -474,7 +474,7 @@ namespace Marco.Pms.Services.Controllers var Image = attachmentDto; if (string.IsNullOrEmpty(Image.Base64Data)) { - _logger.LogError("Base64 data is missing"); + _logger.LogWarning("Base64 data is missing"); return BadRequest(ApiResponse.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); } @@ -552,7 +552,7 @@ namespace Marco.Pms.Services.Controllers .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - _logger.LogError("{error}", errors); + _logger.LogWarning("{error}", errors); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } @@ -568,7 +568,7 @@ namespace Marco.Pms.Services.Controllers if (tickets == null || tickets.Count > 0) { - _logger.LogError("Tickets not Found in database"); + _logger.LogWarning("Tickets not Found in database"); return NotFound(ApiResponse.ErrorResponse("Ticket not Found", "Ticket not Found", 404)); } @@ -578,12 +578,12 @@ namespace Marco.Pms.Services.Controllers { if (string.IsNullOrEmpty(forumAttachmentDto.Base64Data)) { - _logger.LogError("Base64 data is missing"); + _logger.LogWarning("Base64 data is missing"); return BadRequest(ApiResponse.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); } if (forumAttachmentDto.TicketId == null) { - _logger.LogError("ticket ID is missing"); + _logger.LogWarning("ticket ID is missing"); return BadRequest(ApiResponse.ErrorResponse("ticket ID is missing", "ticket ID is missing", 400)); } var ticket = tickets.FirstOrDefault(t => t.Id == forumAttachmentDto.TicketId); diff --git a/Marco.Pms.Services/Controllers/MasterController.cs b/Marco.Pms.Services/Controllers/MasterController.cs index ebd8998..9000cdf 100644 --- a/Marco.Pms.Services/Controllers/MasterController.cs +++ b/Marco.Pms.Services/Controllers/MasterController.cs @@ -168,7 +168,7 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("activity updated successfully from tenant {tenantId}", tenantId); return Ok(ApiResponse.SuccessResponse(activityVM, "activity updated successfully", 200)); } - _logger.LogError("Activity {ActivityId} not found", id); + _logger.LogWarning("Activity {ActivityId} not found", id); return NotFound(ApiResponse.ErrorResponse("Activity not found", "Activity not found", 404)); } @@ -230,7 +230,7 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Ticket Status master {TicketStatusId} added successfully from tenant {tenantId}", statusMaster.Id, tenantId); return Ok(ApiResponse.SuccessResponse(statusVM, "Ticket Status master added successfully", 200)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("Sent Empty payload", "Sent Empty payload", 400)); } @@ -251,10 +251,10 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Ticket Status master {TicketStatusId} updated successfully from tenant {tenantId}", statusMaster.Id, tenantId); return Ok(ApiResponse.SuccessResponse(statusVM, "Ticket Status master updated successfully", 200)); } - _logger.LogError("Ticket Status master {TicketStatusId} not found in database", statusMasterDto.Id != null ? statusMasterDto.Id.Value : Guid.Empty); + _logger.LogWarning("Ticket Status master {TicketStatusId} not found in database", statusMasterDto.Id != null ? statusMasterDto.Id.Value : Guid.Empty); return NotFound(ApiResponse.ErrorResponse("Ticket Status master not found", "Ticket Status master not found", 404)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("Sent Empty payload", "Sent Empty payload", 400)); } @@ -281,7 +281,7 @@ namespace Marco.Pms.Services.Controllers } else { - _logger.LogError("Ticket Status {TickeStatusId} not found in database", id); + _logger.LogWarning("Ticket Status {TickeStatusId} not found in database", id); return NotFound(ApiResponse.ErrorResponse("Ticket Status not found", "Ticket Status not found", 404)); } } @@ -318,7 +318,7 @@ namespace Marco.Pms.Services.Controllers return Ok(ApiResponse.SuccessResponse(typeVM, "Ticket type master added successfully", 200)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); } @@ -339,10 +339,10 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Ticket Type master {TicketTypeId} updated successfully from tenant {tenantId}", typeMaster.Id, tenantId); return Ok(ApiResponse.SuccessResponse(typeVM, "Ticket type master updated successfully", 200)); } - _logger.LogError("Ticket type master {TicketTypeId} not found in database", typeMasterDto.Id != null ? typeMasterDto.Id.Value : Guid.Empty); + _logger.LogWarning("Ticket type master {TicketTypeId} not found in database", typeMasterDto.Id != null ? typeMasterDto.Id.Value : Guid.Empty); return NotFound(ApiResponse.ErrorResponse("Ticket type master not found", "Ticket type master not found", 404)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); } @@ -369,7 +369,7 @@ namespace Marco.Pms.Services.Controllers } else { - _logger.LogError("Ticket Type {TickeTypeId} not found in database", id); + _logger.LogWarning("Ticket Type {TickeTypeId} not found in database", id); return NotFound(ApiResponse.ErrorResponse("Ticket Type not found", "Ticket Type not found", 404)); } } @@ -407,7 +407,7 @@ namespace Marco.Pms.Services.Controllers return Ok(ApiResponse.SuccessResponse(typeVM, "Ticket Priority master added successfully", 200)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); } [HttpPost("ticket-priorities/edit/{id}")] @@ -427,10 +427,10 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Ticket Priority master {TicketPriorityId} updated successfully from tenant {tenantId}", typeMaster.Id, tenantId); return Ok(ApiResponse.SuccessResponse(typeVM, "Ticket Priority master updated successfully", 200)); } - _logger.LogError("Ticket Priority master {TicketPriorityId} not found in database", priorityMasterDto.Id != null ? priorityMasterDto.Id.Value : Guid.Empty); + _logger.LogWarning("Ticket Priority master {TicketPriorityId} not found in database", priorityMasterDto.Id != null ? priorityMasterDto.Id.Value : Guid.Empty); return NotFound(ApiResponse.ErrorResponse("Ticket Priority master not found", "Ticket Priority master not found", 404)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); } @@ -457,7 +457,7 @@ namespace Marco.Pms.Services.Controllers } else { - _logger.LogError("Ticket Priority {TickePriorityId} not found in database", id); + _logger.LogWarning("Ticket Priority {TickePriorityId} not found in database", id); return NotFound(ApiResponse.ErrorResponse("Ticket Priority not found", "Ticket Priority not found", 404)); } } @@ -494,7 +494,7 @@ namespace Marco.Pms.Services.Controllers return Ok(ApiResponse.SuccessResponse(typeVM, "Ticket tag master added successfully", 200)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); } @@ -515,10 +515,10 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Ticket Tag master {TicketTypeId} updated successfully from tenant {tenantId}", tagMaster.Id, tenantId); return Ok(ApiResponse.SuccessResponse(typeVM, "Ticket tag master updated successfully", 200)); } - _logger.LogError("Ticket tag master {TicketTypeId} not found in database", tagMasterDto.Id != null ? tagMasterDto.Id.Value : Guid.Empty); + _logger.LogWarning("Ticket tag master {TicketTypeId} not found in database", tagMasterDto.Id != null ? tagMasterDto.Id.Value : Guid.Empty); return NotFound(ApiResponse.ErrorResponse("Ticket tag master not found", "Ticket tag master not found", 404)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); } @@ -545,7 +545,7 @@ namespace Marco.Pms.Services.Controllers } else { - _logger.LogError("Ticket Tag {TickeTagId} not found in database", id); + _logger.LogWarning("Ticket Tag {TickeTagId} not found in database", id); return NotFound(ApiResponse.ErrorResponse("Ticket tag not found", "Ticket tag not found", 404)); } } @@ -609,7 +609,7 @@ namespace Marco.Pms.Services.Controllers return Ok(ApiResponse.SuccessResponse(workCategoryMasterVM, "Work category master added successfully", 200)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); } @@ -624,7 +624,7 @@ namespace Marco.Pms.Services.Controllers { if (workCategory.IsSystem) { - _logger.LogError("User tries to update system-defined work category"); + _logger.LogWarning("User tries to update system-defined work category"); return BadRequest(ApiResponse.ErrorResponse("Cannot update system-defined work", "Cannot update system-defined work", 400)); } workCategory = workCategoryMasterDto.ToWorkCategoryMasterFromWorkCategoryMasterDto(tenantId); @@ -635,10 +635,10 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Work category master {WorkCategoryId} updated successfully from tenant {tenantId}", workCategory.Id, tenantId); return Ok(ApiResponse.SuccessResponse(workCategoryMasterVM, "Work category master updated successfully", 200)); } - _logger.LogError("Work category master {WorkCategoryId} not found in database", workCategoryMasterDto.Id ?? Guid.Empty); + _logger.LogWarning("Work category master {WorkCategoryId} not found in database", workCategoryMasterDto.Id ?? Guid.Empty); return NotFound(ApiResponse.ErrorResponse("Work category master not found", "Work category master not found", 404)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); } @@ -666,7 +666,7 @@ namespace Marco.Pms.Services.Controllers } else { - _logger.LogError("Work category {WorkCategoryId} not found in database", id); + _logger.LogWarning("Work category {WorkCategoryId} not found in database", id); return NotFound(ApiResponse.ErrorResponse("Work category not found", "Work category not found", 404)); } } @@ -689,7 +689,7 @@ namespace Marco.Pms.Services.Controllers .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - _logger.LogError("User sent Invalid Date while marking attendance"); + _logger.LogWarning("User sent Invalid Date while marking attendance"); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } var response = await _masterHelper.CreateWorkStatus(createWorkStatusDto); @@ -803,7 +803,7 @@ namespace Marco.Pms.Services.Controllers .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - _logger.LogError("User sent Invalid Date while marking attendance"); + _logger.LogWarning("User sent Invalid Date while marking attendance"); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } var response = await _masterHelper.CreateContactTag(contactTagDto); diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index e7d257f..236e0cb 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -6,19 +6,18 @@ using Marco.Pms.Model.Mapper; using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; -using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Model.ViewModels.Projects; using Marco.Pms.Services.Helpers; -using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Service; using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.SignalR; +using Microsoft.CodeAnalysis; using Microsoft.EntityFrameworkCore; using MongoDB.Driver; +using Project = Marco.Pms.Model.Projects.Project; namespace MarcoBMS.Services.Controllers { @@ -31,14 +30,20 @@ namespace MarcoBMS.Services.Controllers private readonly ApplicationDbContext _context; private readonly UserHelper _userHelper; private readonly ILoggingService _logger; - private readonly IHubContext _signalR; + private readonly ISignalRService _signalR; private readonly PermissionServices _permission; private readonly CacheUpdateHelper _cache; private readonly Guid tenantId; - public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, - IHubContext signalR, CacheUpdateHelper cache, PermissionServices permission, IProjectServices projectServices) + public ProjectController( + ApplicationDbContext context, + UserHelper userHelper, + ILoggingService logger, + ISignalRService signalR, + CacheUpdateHelper cache, + PermissionServices permission, + IProjectServices projectServices) { _context = context; _userHelper = userHelper; @@ -174,7 +179,7 @@ namespace MarcoBMS.Services.Controllers if (response.Success) { var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Create_Project", Response = response.Data }; - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + await _signalR.SendNotificationAsync(notification); } return StatusCode(response.StatusCode, response); } @@ -204,7 +209,7 @@ namespace MarcoBMS.Services.Controllers if (response.Success) { var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Update_Project", Response = response.Data }; - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + await _signalR.SendNotificationAsync(notification); } return StatusCode(response.StatusCode, response); } @@ -213,90 +218,38 @@ namespace MarcoBMS.Services.Controllers #region =================================================================== Project Allocation APIs =================================================================== - [HttpGet] - [Route("employees/get/{projectid?}/{includeInactive?}")] - public async Task GetEmployeeByProjectID(Guid? projectid, bool includeInactive = false) + [HttpGet("employees/get/{projectid?}/{includeInactive?}")] + public async Task GetEmployeeByProjectId(Guid? projectId, bool includeInactive = false) { + // --- Step 1: Input Validation --- if (!ModelState.IsValid) { - var errors = ModelState.Values - .SelectMany(v => v.Errors) - .Select(e => e.ErrorMessage) - .ToList(); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); - + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("Get employee list by ProjectId called with invalid model state \n Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - if (projectid != null) - { - // Fetch assigned project - List result = new List(); - - if ((bool)includeInactive) - { - - result = await (from rpm in _context.Employees.Include(c => c.JobRole) - join fp in _context.ProjectAllocations.Where(c => c.TenantId == tenantId && c.ProjectId == projectid) - on rpm.Id equals fp.EmployeeId - select rpm).ToListAsync(); - } - else - { - result = await (from rpm in _context.Employees.Include(c => c.JobRole) - join fp in _context.ProjectAllocations.Where(c => c.TenantId == tenantId && c.ProjectId == projectid && c.IsActive) - on rpm.Id equals fp.EmployeeId - select rpm).ToListAsync(); - } - - List resultVM = new List(); - foreach (Employee employee in result) - { - EmployeeVM vm = employee.ToEmployeeVMFromEmployee(); - resultVM.Add(vm); - } - - return Ok(ApiResponse.SuccessResponse(resultVM, "Success.", 200)); - } - else - { - return NotFound(ApiResponse.ErrorResponse("Invalid Input Parameter", 404)); - } - - + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetEmployeeByProjectIdAsync(projectId, includeInactive, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } - [HttpGet] - [Route("allocation/{projectId}")] + [HttpGet("allocation/{projectId}")] public async Task GetProjectAllocation(Guid? projectId) { + // --- Step 1: Input Validation --- if (!ModelState.IsValid) { - var errors = ModelState.Values - .SelectMany(v => v.Errors) - .Select(e => e.ErrorMessage) - .ToList(); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); - + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("Get employee list by ProjectId called with invalid model state \n Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - var employees = await _context.ProjectAllocations - .Where(c => c.TenantId == tenantId && c.ProjectId == projectId && c.Employee != null) - .Include(e => e.Employee) - .Select(e => new - { - ID = e.Id, - EmployeeId = e.EmployeeId, - ProjectId = e.ProjectId, - AllocationDate = e.AllocationDate, - ReAllocationDate = e.ReAllocationDate, - FirstName = e.Employee != null ? e.Employee.FirstName : string.Empty, - LastName = e.Employee != null ? e.Employee.LastName : string.Empty, - MiddleName = e.Employee != null ? e.Employee.MiddleName : string.Empty, - IsActive = e.IsActive, - JobRoleId = (e.JobRoleId != null ? e.JobRoleId : e.Employee != null ? e.Employee.JobRoleId : null) - }).ToListAsync(); - - return Ok(ApiResponse.SuccessResponse(employees, "Success.", 200)); + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetProjectAllocationAsync(projectId, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } [HttpPost("allocation")] @@ -375,7 +328,7 @@ namespace MarcoBMS.Services.Controllers } var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeList = employeeIds }; - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + await _signalR.SendNotificationAsync(notification); return Ok(ApiResponse.SuccessResponse(result, "Data saved successfully", 200)); } @@ -494,7 +447,7 @@ namespace MarcoBMS.Services.Controllers await _cache.ClearAllProjectIds(employeeId); var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeId = employeeId }; - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + await _signalR.SendNotificationAsync(notification); return Ok(ApiResponse.SuccessResponse(result, "Data saved successfully", 200)); } @@ -799,7 +752,7 @@ namespace MarcoBMS.Services.Controllers var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = message }; - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + await _signalR.SendNotificationAsync(notification); return Ok(ApiResponse.SuccessResponse(responseList, responseMessage, 200)); } @@ -826,9 +779,15 @@ namespace MarcoBMS.Services.Controllers workAreaIds.Add(task.WorkAreaId); + var projectId = floor?.Building?.ProjectId; var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = $"Task Deleted in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}, in Area: {task.WorkArea?.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}" }; - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + await _signalR.SendNotificationAsync(notification); + await _cache.DeleteWorkItemByIdAsync(task.Id); + if (projectId != null) + { + await _cache.DeleteProjectByIdAsync(projectId.Value); + } } else { @@ -847,7 +806,7 @@ namespace MarcoBMS.Services.Controllers } else { - _logger.LogError("Task with ID {WorkItemId} not found ID database", id); + _logger.LogWarning("Task with ID {WorkItemId} not found ID database", id); } return Ok(ApiResponse.SuccessResponse(new { }, "Task deleted successfully", 200)); } @@ -973,7 +932,7 @@ namespace MarcoBMS.Services.Controllers message = $"{message} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}"; var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = message }; - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + await _signalR.SendNotificationAsync(notification); return Ok(ApiResponse.SuccessResponse(responseData, responseMessage, 200)); } return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Infra Details are not valid.", 400)); diff --git a/Marco.Pms.Services/Controllers/ReportController.cs b/Marco.Pms.Services/Controllers/ReportController.cs index 717a273..87382d7 100644 --- a/Marco.Pms.Services/Controllers/ReportController.cs +++ b/Marco.Pms.Services/Controllers/ReportController.cs @@ -106,7 +106,7 @@ namespace Marco.Pms.Services.Controllers } catch (Exception ex) { - _logger.LogError("Database Error: Failed to check existence of MailListId '{MailListId}' for TenantId: {TenantId}. : {Error}", mailDetailsDto.MailListId, tenantId, ex.Message); + _logger.LogError(ex, "Database Error: Failed to check existence of MailListId '{MailListId}' for TenantId: {TenantId}.", mailDetailsDto.MailListId, tenantId); return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An error occurred while validating mail template.", 500)); } @@ -143,13 +143,13 @@ namespace Marco.Pms.Services.Controllers } catch (DbUpdateException dbEx) { - _logger.LogError("Database Error: Failed to save new mail details for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}. : {Error}", newMailDetails.ProjectId, newMailDetails.Recipient, tenantId, dbEx.Message); + _logger.LogError(dbEx, "Database Error: Failed to save new mail details for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}.", newMailDetails.ProjectId, newMailDetails.Recipient, tenantId); // Check for specific constraint violations if applicable (e.g., duplicate recipient for a project) return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An error occurred while saving the mail details.", 500)); } catch (Exception ex) { - _logger.LogError("Unexpected Error: An unhandled exception occurred while adding mail details for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}. : {Error}", newMailDetails.ProjectId, newMailDetails.Recipient, tenantId, ex.Message); + _logger.LogError(ex, "Unexpected Error: An unhandled exception occurred while adding mail details for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}.", newMailDetails.ProjectId, newMailDetails.Recipient, tenantId); return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500)); } } @@ -234,7 +234,7 @@ namespace Marco.Pms.Services.Controllers } catch (Exception ex) { - _logger.LogError("Database Error: Failed to check for existing mail template with title '{Title}' for TenantId: {TenantId}.: {Error}", mailTemplateDto.Title, tenantId, ex.Message); + _logger.LogError(ex, "Database Error: Failed to check for existing mail template with title '{Title}' for TenantId: {TenantId}.", mailTemplateDto.Title, tenantId); return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An error occurred while checking for existing templates.", 500)); } @@ -270,12 +270,12 @@ namespace Marco.Pms.Services.Controllers } catch (DbUpdateException dbEx) { - _logger.LogError("Database Error: Failed to save new mail template '{Title}' for TenantId: {TenantId}. : {Error}", mailTemplateDto.Title, tenantId, dbEx.Message); + _logger.LogError(dbEx, "Database Error: Failed to save new mail template '{Title}' for TenantId: {TenantId}. : {Error}", mailTemplateDto.Title, tenantId); return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An error occurred while saving the mail template.", 500)); } catch (Exception ex) { - _logger.LogError("Unexpected Error: An unhandled exception occurred while adding mail template '{Title}' for TenantId: {TenantId}. : {Error}", mailTemplateDto.Title, tenantId, ex.Message); + _logger.LogError(ex, "Unexpected Error: An unhandled exception occurred while adding mail template '{Title}' for TenantId: {TenantId}.", mailTemplateDto.Title, tenantId); return StatusCode(500, ApiResponse.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500)); } } @@ -350,7 +350,7 @@ namespace Marco.Pms.Services.Controllers { // 3. OPTIMIZATION: Make the process resilient. // If one task fails unexpectedly, log it and continue with others. - _logger.LogError("Failed to send report for project {ProjectId} : {Error}", mailGroup.ProjectId, ex.Message); + _logger.LogError(ex, "Failed to send report for project {ProjectId}", mailGroup.ProjectId); Interlocked.Increment(ref failureCount); } } @@ -527,7 +527,7 @@ namespace Marco.Pms.Services.Controllers catch (Exception ex) { // It's good practice to log any unexpected errors within a concurrent task. - _logger.LogError("Failed to process project report for ProjectId {ProjectId} : {Error}", group.ProjectId, ex.Message); + _logger.LogError(ex, "Failed to process project report for ProjectId {ProjectId}", group.ProjectId); } } }); diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 5bae90f..aca439b 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -118,8 +118,8 @@ namespace Marco.Pms.Services.Helpers projectDetails.ProjectStatus = new StatusMasterMongoDB { - Id = status?.Id.ToString(), - Status = status?.Status + Id = status!.Id.ToString(), + Status = status.Status }; // Use fast in-memory lookups instead of .Where() in loops. @@ -797,7 +797,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogError("Error occured while fetching project report mail bodys: {Error}", ex.Message); + _logger.LogError(ex, "Error occured while fetching project report mail bodys"); return null; } } @@ -809,7 +809,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogError("Error occured while adding project report mail bodys: {Error}", ex.Message); + _logger.LogError(ex, "Error occured while adding project report mail bodys"); } } } diff --git a/Marco.Pms.Services/Helpers/DirectoryHelper.cs b/Marco.Pms.Services/Helpers/DirectoryHelper.cs index 199a410..ad9001c 100644 --- a/Marco.Pms.Services/Helpers/DirectoryHelper.cs +++ b/Marco.Pms.Services/Helpers/DirectoryHelper.cs @@ -52,7 +52,7 @@ namespace Marco.Pms.Services.Helpers } else { - _logger.LogError("Employee {EmployeeId} attemped to access a contacts, but do not have permission", LoggedInEmployee.Id); + _logger.LogWarning("Employee {EmployeeId} attemped to access a contacts, but do not have permission", LoggedInEmployee.Id); return ApiResponse.ErrorResponse("You don't have permission", "You don't have permission", 401); } @@ -202,7 +202,7 @@ namespace Marco.Pms.Services.Helpers } else { - _logger.LogError("Employee {EmployeeId} attemped to access a contacts with in bucket {BucketId}, but do not have permission", LoggedInEmployee.Id, id); + _logger.LogWarning("Employee {EmployeeId} attemped to access a contacts with in bucket {BucketId}, but do not have permission", LoggedInEmployee.Id, id); return ApiResponse.ErrorResponse("You don't have permission", "You don't have permission", 401); } @@ -490,7 +490,7 @@ namespace Marco.Pms.Services.Helpers } else { - _logger.LogError("Employee {EmployeeId} attemped to update a contact, but do not have permission", LoggedInEmployee.Id); + _logger.LogWarning("Employee {EmployeeId} attemped to update a contact, but do not have permission", LoggedInEmployee.Id); return ApiResponse.ErrorResponse("You don't have permission", "You don't have permission", 401); } @@ -1169,7 +1169,7 @@ namespace Marco.Pms.Services.Helpers } else { - _logger.LogError("Employee {EmployeeId} attemped to access a buckets list, but do not have permission", LoggedInEmployee.Id); + _logger.LogWarning("Employee {EmployeeId} attemped to access a buckets list, but do not have permission", LoggedInEmployee.Id); return ApiResponse.ErrorResponse("You don't have permission", "You don't have permission", 401); } @@ -1204,7 +1204,7 @@ namespace Marco.Pms.Services.Helpers var demo = !permissionIds.Contains(PermissionsMaster.DirectoryUser); if (!permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && !permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && !permissionIds.Contains(PermissionsMaster.DirectoryUser)) { - _logger.LogError("Employee {EmployeeId} attemped to create a bucket, but do not have permission", LoggedInEmployee.Id); + _logger.LogWarning("Employee {EmployeeId} attemped to create a bucket, but do not have permission", LoggedInEmployee.Id); return ApiResponse.ErrorResponse("You don't have permission", "You don't have permission", 401); } @@ -1276,7 +1276,7 @@ namespace Marco.Pms.Services.Helpers } if (accessableBucket == null) { - _logger.LogError("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id); + _logger.LogWarning("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id); return ApiResponse.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 401); } @@ -1342,7 +1342,7 @@ namespace Marco.Pms.Services.Helpers } if (accessableBucket == null) { - _logger.LogError("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id); + _logger.LogWarning("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id); return ApiResponse.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 401); } var employeeIds = await _context.Employees.Where(e => e.TenantId == tenantId && e.IsActive).Select(e => e.Id).ToListAsync(); @@ -1396,7 +1396,7 @@ namespace Marco.Pms.Services.Helpers } if (removededEmployee > 0) { - _logger.LogError("Employee {EmployeeId} removed {conut} number of employees from bucket {BucketId}", LoggedInEmployee.Id, removededEmployee, bucketId); + _logger.LogWarning("Employee {EmployeeId} removed {conut} number of employees from bucket {BucketId}", LoggedInEmployee.Id, removededEmployee, bucketId); } return ApiResponse.SuccessResponse(bucketVM, "Details updated successfully", 200); } @@ -1443,7 +1443,7 @@ namespace Marco.Pms.Services.Helpers } if (accessableBucket == null) { - _logger.LogError("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id); + _logger.LogWarning("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id); return ApiResponse.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 401); } diff --git a/Marco.Pms.Services/Helpers/EmployeeHelper.cs b/Marco.Pms.Services/Helpers/EmployeeHelper.cs index 343144a..09dcbe2 100644 --- a/Marco.Pms.Services/Helpers/EmployeeHelper.cs +++ b/Marco.Pms.Services/Helpers/EmployeeHelper.cs @@ -33,7 +33,7 @@ namespace MarcoBMS.Services.Helpers } catch (Exception ex) { - _logger.LogError("{Error}", ex.Message); + _logger.LogError(ex, "Error occured while fetching employee by application user ID {ApplicationUserId}", ApplicationUserID); return new Employee(); } } @@ -66,7 +66,7 @@ namespace MarcoBMS.Services.Helpers } catch (Exception ex) { - _logger.LogError("{Error}", ex.Message); + _logger.LogError(ex, "Error occoured while filtering employees by string {SearchString} or project {ProjectId}", searchString, ProjectId ?? Guid.Empty); return new List(); } } @@ -102,7 +102,7 @@ namespace MarcoBMS.Services.Helpers } catch (Exception ex) { - _logger.LogError("{Error}", ex.Message); + _logger.LogError(ex, "Error occured while featching list of employee by project ID {ProjectId}", ProjectId ?? Guid.Empty); return new List(); } } diff --git a/Marco.Pms.Services/Helpers/MasterHelper.cs b/Marco.Pms.Services/Helpers/MasterHelper.cs index f994639..83bc007 100644 --- a/Marco.Pms.Services/Helpers/MasterHelper.cs +++ b/Marco.Pms.Services/Helpers/MasterHelper.cs @@ -218,7 +218,7 @@ namespace Marco.Pms.Services.Helpers _logger.LogInfo("Contact tag master {ConatctTagId} updated successfully by employee {EmployeeId}", contactTagVm.Id, LoggedInEmployee.Id); return ApiResponse.SuccessResponse(contactTagVm, "Contact Tag master updated successfully", 200); } - _logger.LogError("Contact Tag master {ContactTagId} not found in database", id); + _logger.LogWarning("Contact Tag master {ContactTagId} not found in database", id); return ApiResponse.ErrorResponse("Contact Tag master not found", "Contact tag master not found", 404); } _logger.LogWarning("Employee with ID {LoggedInEmployeeId} sended empty payload", LoggedInEmployee.Id); @@ -294,7 +294,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogError("Error occurred while fetching work status list : {Error}", ex.Message); + _logger.LogWarning("Error occurred while fetching work status list : {Error}", ex.Message); return ApiResponse.ErrorResponse("An error occurred", "Unable to fetch work status list", 500); } } @@ -343,7 +343,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogError("Error occurred while creating work status : {Error}", ex.Message); + _logger.LogWarning("Error occurred while creating work status : {Error}", ex.Message); return ApiResponse.ErrorResponse("An error occurred", "Unable to create work status", 500); } } @@ -403,7 +403,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogError("Error occurred while updating work status ID: {Id} : {Error}", id, ex.Message); + _logger.LogError(ex, "Error occurred while updating work status ID: {Id}", id); return ApiResponse.ErrorResponse("An error occurred", "Unable to update the work status at this time", 500); } } @@ -458,7 +458,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogError("Error occurred while deleting WorkStatus Id: {Id} : {Error}", id, ex.Message); + _logger.LogError(ex, "Error occurred while deleting WorkStatus Id: {Id}", id); return ApiResponse.ErrorResponse("An error occurred", "Unable to delete work status", 500); } } diff --git a/Marco.Pms.Services/Helpers/ReportHelper.cs b/Marco.Pms.Services/Helpers/ReportHelper.cs index 4ec0978..4ec9453 100644 --- a/Marco.Pms.Services/Helpers/ReportHelper.cs +++ b/Marco.Pms.Services/Helpers/ReportHelper.cs @@ -289,13 +289,13 @@ namespace Marco.Pms.Services.Helpers // --- Input Validation --- if (projectId == Guid.Empty) { - _logger.LogError("Validation Error: Provided empty project ID while fetching project report."); + _logger.LogWarning("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); + _logger.LogWarning("Validation Error: No recipient emails provided for project ID {ProjectId}.", projectId); return ApiResponse.ErrorResponse("No recipient emails provided.", "No recipient emails provided.", 400); } @@ -316,7 +316,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogError("Email Sending Error: Failed to send project statistics email for project ID {ProjectId}. : {Error}", projectId, ex.Message); + _logger.LogError(ex, "Email Sending Error: Failed to send project statistics email for project ID {ProjectId}.", projectId); return ApiResponse.ErrorResponse("Failed to send email.", "An error occurred while sending the email.", 500); } @@ -350,14 +350,14 @@ namespace Marco.Pms.Services.Helpers } catch (DbUpdateException dbEx) { - _logger.LogError("Database Error: Failed to save mail logs for project ID {ProjectId}. : {Error}", projectId, dbEx.Message); + _logger.LogError(dbEx, "Database Error: Failed to save mail logs for project ID {ProjectId}.", projectId); // 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); + _logger.LogError(ex, "Unexpected Error: An unhandled exception occurred while processing project statistics for project ID {ProjectId}.", projectId); return ApiResponse.ErrorResponse("An unexpected error occurred.", "An unexpected error occurred.", 500); } } diff --git a/Marco.Pms.Services/Helpers/RolesHelper.cs b/Marco.Pms.Services/Helpers/RolesHelper.cs index cd73c0f..ef9f824 100644 --- a/Marco.Pms.Services/Helpers/RolesHelper.cs +++ b/Marco.Pms.Services/Helpers/RolesHelper.cs @@ -84,7 +84,7 @@ namespace MarcoBMS.Services.Helpers } catch (Exception ex) { - _logger.LogError("An error occurred while fetching permissions for EmployeeId {EmployeeId} : {Error}", EmployeeId, ex.Message); + _logger.LogError(ex, "An error occurred while fetching permissions for EmployeeId {EmployeeId}", EmployeeId); return new List(); } } @@ -144,7 +144,7 @@ namespace MarcoBMS.Services.Helpers } catch (Exception ex) { - _logger.LogError("An error occurred while fetching permissions for RoleId {RoleId}: {Error}", roleId, ex.Message); + _logger.LogError(ex, "An error occurred while fetching permissions for RoleId {RoleId}", roleId); // Return an empty list as a safe default to prevent downstream failures. return new List(); } diff --git a/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs similarity index 75% rename from Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs rename to Marco.Pms.Services/MappingProfiles/MappingProfile.cs index b811056..7d627bc 100644 --- a/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -1,16 +1,19 @@ using AutoMapper; using Marco.Pms.Model.Dtos.Project; +using Marco.Pms.Model.Employees; using Marco.Pms.Model.Master; using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.Projects; +using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Model.ViewModels.Projects; namespace Marco.Pms.Services.MappingProfiles { - public class ProjectMappingProfile : Profile + public class MappingProfile : Profile { - public ProjectMappingProfile() + public MappingProfile() { + #region ======================================================= Projects ======================================================= // Your mappings CreateMap(); CreateMap(); @@ -40,6 +43,11 @@ namespace Marco.Pms.Services.MappingProfiles CreateMap(); CreateMap(); CreateMap(); + #endregion + + #region ======================================================= Projects ======================================================= + CreateMap(); + #endregion } } } diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index 6553745..26d8eba 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -158,6 +158,7 @@ builder.Services.AddTransient(); #region Customs Services builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); #endregion diff --git a/Marco.Pms.Services/Service/ILoggingService.cs b/Marco.Pms.Services/Service/ILoggingService.cs index b835d0c..6d795cd 100644 --- a/Marco.Pms.Services/Service/ILoggingService.cs +++ b/Marco.Pms.Services/Service/ILoggingService.cs @@ -5,7 +5,7 @@ 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); + void LogError(Exception? ex, string? message, params object[]? args); } } diff --git a/Marco.Pms.Services/Service/LoggingServices.cs b/Marco.Pms.Services/Service/LoggingServices.cs index 5a016de..751f22c 100644 --- a/Marco.Pms.Services/Service/LoggingServices.cs +++ b/Marco.Pms.Services/Service/LoggingServices.cs @@ -11,16 +11,16 @@ namespace MarcoBMS.Services.Service _logger = logger; } - public void LogError(string? message, params object[]? args) + public void LogError(Exception? ex, string? message, params object[]? args) { using (LogContext.PushProperty("LogLevel", "Error")) if (args != null) { - _logger.LogError(message, args); + _logger.LogError(ex, message, args); } else { - _logger.LogError(message); + _logger.LogError(ex, message); } } diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs index 3280558..dcaf20e 100644 --- a/Marco.Pms.Services/Service/ProjectServices.cs +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -1,4 +1,5 @@ using AutoMapper; +using AutoMapper.QueryableExtensions; using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Activities; using Marco.Pms.Model.Dtos.Project; @@ -7,12 +8,15 @@ using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; +using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Model.ViewModels.Projects; using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; +using Microsoft.CodeAnalysis; using Microsoft.EntityFrameworkCore; +using Project = Marco.Pms.Model.Projects.Project; namespace Marco.Pms.Services.Service { @@ -75,7 +79,7 @@ namespace Marco.Pms.Services.Service catch (Exception ex) { // --- Step 5: Graceful Error Handling --- - _logger.LogError("An unexpected error occurred in GetAllProjectsBasic for tenant {TenantId}. \n {Error}", tenantId, ex.Message); + _logger.LogError(ex, "An unexpected error occurred in GetAllProjectsBasic for tenant {TenantId}.", tenantId); return ApiResponse.ErrorResponse("An internal server error occurred. Please try again later.", null, 500); } } @@ -134,7 +138,7 @@ namespace Marco.Pms.Services.Service catch (Exception ex) { // --- Step 5: Graceful Error Handling --- - _logger.LogError("An unexpected error occurred in GetAllProjects for tenant {TenantId}. \n {Error}", tenantId, ex.Message); + _logger.LogError(ex, "An unexpected error occurred in GetAllProjects for tenant {TenantId}.", tenantId); return ApiResponse.ErrorResponse("An internal server error occurred. Please try again later.", null, 500); } } @@ -178,7 +182,7 @@ namespace Marco.Pms.Services.Service } catch (Exception ex) { - _logger.LogError("An unexpected error occurred while getting project {ProjectId} : \n {Error}", id, ex.Message); + _logger.LogError(ex, "An unexpected error occurred while getting project {ProjectId}", id); return ApiResponse.ErrorResponse("An internal server error occurred.", null, 500); } } @@ -244,7 +248,7 @@ namespace Marco.Pms.Services.Service catch (Exception ex) { // --- Step 5: Graceful Error Handling --- - _logger.LogError("An unexpected error occurred in Get Project Details for project {ProjectId} for tenant {TenantId}. \n {Error}", id, tenantId, ex.Message); + _logger.LogError(ex, "An unexpected error occurred in Get Project Details for project {ProjectId} for tenant {TenantId}. ", id, tenantId); return ApiResponse.ErrorResponse("An internal server error occurred. Please try again later.", null, 500); } } @@ -360,7 +364,7 @@ namespace Marco.Pms.Services.Service catch (Exception ex) { // Log the detailed exception - _logger.LogError("Failed to create project in database. Rolling back transaction. \n {Error}", ex.Message); + _logger.LogError(ex, "Failed to create project in database. Rolling back transaction."); // Return a server error as the primary operation failed return ApiResponse.ErrorResponse("An error occurred while saving the project.", ex.Message, 500); } @@ -379,7 +383,7 @@ namespace Marco.Pms.Services.Service { // The project was created successfully, but a side-effect failed. // Log this as a warning, as the primary operation succeeded. Don't return an error to the user. - _logger.LogWarning("Project {ProjectId} was created, but a post-creation side-effect (caching/notification) failed. \n {Error}", project.Id, ex.Message); + _logger.LogError(ex, "Project {ProjectId} was created, but a post-creation side-effect (caching/notification) failed. ", project.Id); } // 4. Return a success response to the user as soon as the critical data is saved. @@ -435,7 +439,7 @@ namespace Marco.Pms.Services.Service { // --- Step 3: Handle Concurrency Conflicts --- // This happens if another user modified the project after we fetched it. - _logger.LogWarning("Concurrency conflict while updating project {ProjectId} \n {Error}", id, ex.Message); + _logger.LogError(ex, "Concurrency conflict while updating project {ProjectId} ", id); return ApiResponse.ErrorResponse("Conflict occurred.", "This project has been modified by someone else. Please refresh and try again.", 409); } @@ -458,13 +462,216 @@ namespace Marco.Pms.Services.Service catch (Exception ex) { // --- Step 6: Graceful Error Handling for Unexpected Errors --- - _logger.LogError("An unexpected error occurred while updating project {ProjectId} \n {Error}", id, ex.Message); + _logger.LogError(ex, "An unexpected error occurred while updating project {ProjectId} ", id); return ApiResponse.ErrorResponse("An internal server error occurred.", null, 500); } } #endregion + #region =================================================================== Project Allocation APIs =================================================================== + + public async Task> GetEmployeeByProjectID(Guid? projectid, bool includeInactive, Guid tenantId, Employee loggedInEmployee) + { + if (projectid == null) + { + return ApiResponse.ErrorResponse("Invalid Input Parameter", 404); + } + // Fetch assigned project + List result = new List(); + + var employeeQuery = _context.ProjectAllocations + .Include(pa => pa.Employee) + .Where(pa => pa.ProjectId == projectid && pa.TenantId == tenantId && pa.Employee != null); + + if (includeInactive) + { + + result = await employeeQuery.Select(pa => pa.Employee ?? new Employee()).ToListAsync(); + } + else + { + result = await employeeQuery + .Where(pa => pa.IsActive) + .Select(pa => pa.Employee ?? new Employee()).ToListAsync(); + } + + List resultVM = new List(); + foreach (Employee employee in result) + { + EmployeeVM vm = _mapper.Map(employee); + resultVM.Add(vm); + } + + return ApiResponse.SuccessResponse(resultVM, "Successfully fetched the list of employees for seleted project", 200); + } + + /// + /// Retrieves a list of employees for a specific project. + /// This method is optimized to perform all filtering and mapping on the database server. + /// + /// The ID of the project. + /// Whether to include employees from inactive allocations. + /// The ID of the current tenant. + /// The current authenticated employee (used for permission checks). + /// An ApiResponse containing a list of employees or an error. + public async Task> GetEmployeeByProjectIdAsync(Guid? projectId, bool includeInactive, Guid tenantId, Employee loggedInEmployee) + { + // --- Step 1: Input Validation --- + if (projectId == null) + { + _logger.LogWarning("GetEmployeeByProjectID called with a null projectId."); + // 400 Bad Request is more appropriate for invalid input than 404 Not Found. + return ApiResponse.ErrorResponse("Project ID is required.", "Invalid Input Parameter", 400); + } + + _logger.LogInfo("Fetching employees for ProjectID: {ProjectId}, IncludeInactive: {IncludeInactive}", projectId, includeInactive); + + try + { + // --- CRITICAL: Security Check --- + // Before fetching data, you MUST verify the user has permission to see it. + // This is a placeholder for your actual permission logic. + var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.Value); + var hasAllEmployeePermission = await _permission.HasPermission(PermissionsMaster.ViewAllEmployees, loggedInEmployee.Id); + var hasviewTeamPermission = await _permission.HasPermission(PermissionsMaster.ViewTeamMembers, loggedInEmployee.Id); + + if (!(hasProjectPermission && (hasAllEmployeePermission || hasviewTeamPermission))) + { + _logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, projectId); + return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to view this project's team.", 403); + } + + // --- Step 2: Build a Single, Efficient IQueryable --- + // We start with the base query and conditionally add filters before executing it. + // This avoids code duplication and is highly performant. + var employeeQuery = _context.ProjectAllocations + .Where(pa => pa.ProjectId == projectId && pa.TenantId == tenantId); + + // Conditionally apply the filter for active allocations. + if (!includeInactive) + { + employeeQuery = employeeQuery.Where(pa => pa.IsActive); + } + + // --- Step 3: Project Directly to the ViewModel on the Database Server --- + // This is the most significant performance optimization. + // Instead of fetching full Employee entities, we select only the data needed for the EmployeeVM. + // AutoMapper's ProjectTo is perfect for this, as it translates the mapping configuration into an efficient SQL SELECT statement. + var resultVM = await employeeQuery + .Where(pa => pa.Employee != null) // Safety check for data integrity + .Select(pa => pa.Employee) // Navigate to the Employee entity + .ProjectTo(_mapper.ConfigurationProvider) // Let AutoMapper generate the SELECT + .ToListAsync(); + + _logger.LogInfo("Successfully fetched {EmployeeCount} employees for project {ProjectId}.", resultVM.Count, projectId); + + // Note: The original mapping loop is now completely gone, replaced by the single efficient query above. + + return ApiResponse.SuccessResponse(resultVM, "Successfully fetched the list of employees for the selected project.", 200); + } + catch (Exception ex) + { + // --- Step 4: Graceful Error Handling --- + _logger.LogError(ex, "An error occurred while fetching employees for project {ProjectId}. ", projectId); + return ApiResponse.ErrorResponse("An internal server error occurred.", "Database Query Failed", 500); + } + } + + public async Task> GetProjectAllocation(Guid? projectId, Guid tenantId, Employee loggedInEmployee) + { + var employees = await _context.ProjectAllocations + .Where(c => c.TenantId == tenantId && c.ProjectId == projectId && c.Employee != null) + .Include(e => e.Employee) + .Select(e => new + { + ID = e.Id, + EmployeeId = e.EmployeeId, + ProjectId = e.ProjectId, + AllocationDate = e.AllocationDate, + ReAllocationDate = e.ReAllocationDate, + FirstName = e.Employee != null ? e.Employee.FirstName : string.Empty, + LastName = e.Employee != null ? e.Employee.LastName : string.Empty, + MiddleName = e.Employee != null ? e.Employee.MiddleName : string.Empty, + IsActive = e.IsActive, + JobRoleId = (e.JobRoleId != null ? e.JobRoleId : e.Employee != null ? e.Employee.JobRoleId : null) + }).ToListAsync(); + + return ApiResponse.SuccessResponse(employees, "Success.", 200); + } + + /// + /// Retrieves project allocation details for a specific project. + /// This method is optimized for performance and includes security checks. + /// + /// The ID of the project. + /// The ID of the current tenant. + /// The current authenticated employee for permission checks. + /// An ApiResponse containing allocation details or an appropriate error. + public async Task> GetProjectAllocationAsync(Guid? projectId, Guid tenantId, Employee loggedInEmployee) + { + // --- Step 1: Input Validation --- + if (projectId == null) + { + _logger.LogWarning("GetProjectAllocation called with a null projectId."); + return ApiResponse.ErrorResponse("Project ID is required.", "Invalid Input Parameter", 400); + } + + _logger.LogInfo("Fetching allocations for ProjectID: {ProjectId} for user {UserId}", projectId, loggedInEmployee.Id); + + try + { + // --- Step 2: Security and Existence Checks --- + // Before fetching data, you MUST verify the user has permission to see it. + // This is a placeholder for your actual permission logic. + var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.Value); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, projectId); + return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to view this project's team.", 403); + } + + // --- Step 3: Execute a Single, Optimized Database Query --- + // This query projects directly to a new object on the database server, which is highly efficient. + var allocations = await _context.ProjectAllocations + // Filter down to the relevant records first. + .Where(pa => pa.ProjectId == projectId && pa.TenantId == tenantId && pa.Employee != null) + // Project directly to the final shape. This tells EF Core which columns to select. + // The redundant .Include() is removed as EF Core infers the JOIN from this Select. + .Select(pa => new + { + // Fields from ProjectAllocation + ID = pa.Id, + pa.EmployeeId, + pa.ProjectId, + pa.AllocationDate, + pa.ReAllocationDate, + pa.IsActive, + + // Fields from the joined Employee table (no null checks needed due to the 'Where' clause) + FirstName = pa.Employee!.FirstName, + LastName = pa.Employee.LastName, + MiddleName = pa.Employee.MiddleName, + + // Simplified JobRoleId logic: Use the allocation's role if it exists, otherwise fall back to the employee's default role. + JobRoleId = pa.JobRoleId ?? pa.Employee.JobRoleId + }) + .ToListAsync(); + + _logger.LogInfo("Successfully fetched {AllocationCount} allocations for project {ProjectId}.", allocations.Count, projectId); + + return ApiResponse.SuccessResponse(allocations, "Project allocations retrieved successfully.", 200); + } + catch (Exception ex) + { + // --- Step 4: Graceful Error Handling --- + // Log the full exception for debugging, but return a generic, safe error message. + _logger.LogError(ex, "An error occurred while fetching allocations for project {ProjectId}.", projectId); + return ApiResponse.ErrorResponse("An internal server error occurred.", "Database query failed.", 500); + } + } + #endregion + #region =================================================================== Helper Functions =================================================================== /// @@ -661,7 +868,7 @@ namespace Marco.Pms.Services.Service } catch (Exception ex) { - _logger.LogWarning("Failed to update cache for project {ProjectId} : \n {Error}", projectId, ex.Message); + _logger.LogError(ex, "Failed to update cache for project {ProjectId} : ", projectId); } // Map from the database entity to the response ViewModel. @@ -682,7 +889,7 @@ namespace Marco.Pms.Services.Service } catch (Exception ex) { - _logger.LogError("Background cache update failed for project {ProjectId} \n {Error}", project.Id, ex.Message); + _logger.LogError(ex, "Background cache update failed for project {ProjectId} ", project.Id); } } diff --git a/Marco.Pms.Services/Service/RefreshTokenService.cs b/Marco.Pms.Services/Service/RefreshTokenService.cs index 231e27c..84ef3fd 100644 --- a/Marco.Pms.Services/Service/RefreshTokenService.cs +++ b/Marco.Pms.Services/Service/RefreshTokenService.cs @@ -1,11 +1,11 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Text; -using Marco.Pms.DataAccess.Data; +using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Authentication; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; #nullable disable namespace MarcoBMS.Services.Service @@ -94,7 +94,7 @@ namespace MarcoBMS.Services.Service } catch (Exception ex) { - _logger.LogError("{Error}", ex.Message); + _logger.LogError(ex, "Error occured while creating new JWT token for user {UserId}", userId); throw; } } @@ -132,7 +132,7 @@ namespace MarcoBMS.Services.Service } catch (Exception ex) { - _logger.LogError("Error creating MPIN token for userId: {UserId}, tenantId: {TenantId}, error : {Error}", userId, tenantId, ex.Message); + _logger.LogError(ex, "Error creating MPIN token for userId: {UserId}, tenantId: {TenantId}", userId, tenantId); throw; } } @@ -218,7 +218,7 @@ namespace MarcoBMS.Services.Service catch (Exception ex) { // Token is invalid - _logger.LogError($"Token validation failed: {ex.Message}"); + _logger.LogError(ex, "Token validation failed"); return null; } } diff --git a/Marco.Pms.Services/Service/S3UploadService.cs b/Marco.Pms.Services/Service/S3UploadService.cs index c29cfdd..4ce7a4b 100644 --- a/Marco.Pms.Services/Service/S3UploadService.cs +++ b/Marco.Pms.Services/Service/S3UploadService.cs @@ -64,7 +64,7 @@ namespace Marco.Pms.Services.Service } catch (Exception ex) { - _logger.LogError("{error} while uploading file to S3", ex.Message); + _logger.LogError(ex, "error occured while uploading file to S3"); } @@ -87,7 +87,7 @@ namespace Marco.Pms.Services.Service } catch (Exception ex) { - _logger.LogError("{error} while requesting presigned url from Amazon S3", ex.Message); + _logger.LogError(ex, "error occured while requesting presigned url from Amazon S3", ex.Message); return string.Empty; } } @@ -107,7 +107,7 @@ namespace Marco.Pms.Services.Service } catch (Exception ex) { - _logger.LogError("{error} while deleting from Amazon S3", ex.Message); + _logger.LogError(ex, "error ocured while deleting from Amazon S3"); return false; } } @@ -202,7 +202,7 @@ namespace Marco.Pms.Services.Service } else { - _logger.LogError("Warning: Could not find MimeType, Type, or ContentType property in Definition."); + _logger.LogWarning("Warning: Could not find MimeType, Type, or ContentType property in Definition."); return "application/octet-stream"; } } @@ -211,16 +211,16 @@ namespace Marco.Pms.Services.Service return "application/octet-stream"; // Default if type cannot be determined } } - catch (FormatException) + catch (FormatException fEx) { // Handle cases where the input string is not valid Base64 - _logger.LogError("Invalid Base64 string."); + _logger.LogError(fEx, "Invalid Base64 string."); return string.Empty; } catch (Exception ex) { // Handle other potential errors during decoding or inspection - _logger.LogError($"An error occurred: {ex.Message}"); + _logger.LogError(ex, "errors during decoding or inspection"); return string.Empty; } } diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs index a23eba0..d0539b0 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs @@ -13,5 +13,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> GetProjectDetailsOldAsync(Guid id, Guid tenantId, Employee loggedInEmployee); Task> CreateProjectAsync(CreateProjectDto projectDto, Guid tenantId, Employee loggedInEmployee); Task> UpdateProjectAsync(Guid id, UpdateProjectDto updateProjectDto, Guid tenantId, Employee loggedInEmployee); + Task> GetEmployeeByProjectIdAsync(Guid? projectId, bool includeInactive, Guid tenantId, Employee loggedInEmployee); + Task> GetProjectAllocationAsync(Guid? projectId, Guid tenantId, Employee loggedInEmployee); } } diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/ISignalRService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/ISignalRService.cs new file mode 100644 index 0000000..c37322b --- /dev/null +++ b/Marco.Pms.Services/Service/ServiceInterfaces/ISignalRService.cs @@ -0,0 +1,7 @@ +namespace Marco.Pms.Services.Service.ServiceInterfaces +{ + public interface ISignalRService + { + Task SendNotificationAsync(object notification); + } +} diff --git a/Marco.Pms.Services/Service/SignalRService.cs b/Marco.Pms.Services/Service/SignalRService.cs new file mode 100644 index 0000000..fecc9b0 --- /dev/null +++ b/Marco.Pms.Services/Service/SignalRService.cs @@ -0,0 +1,29 @@ +using Marco.Pms.Services.Hubs; +using Marco.Pms.Services.Service.ServiceInterfaces; +using MarcoBMS.Services.Service; +using Microsoft.AspNetCore.SignalR; + +namespace Marco.Pms.Services.Service +{ + public class SignalRService : ISignalRService + { + private readonly IHubContext _signalR; + private readonly ILoggingService _logger; + public SignalRService(IHubContext signalR, ILoggingService logger) + { + _signalR = signalR ?? throw new ArgumentNullException(nameof(signalR)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + public async Task SendNotificationAsync(object notification) + { + try + { + await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception occured during sending notification through signalR"); + } + } + } +} From 80149f05f78ade88758d9508554980972744f93d Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 15 Jul 2025 13:09:27 +0530 Subject: [PATCH 102/124] Solved the issue of project is not updating properly --- Marco.Pms.CacheHelper/ProjectCache.cs | 4 +- .../Controllers/ProjectController.cs | 101 +++-------- Marco.Pms.Services/Service/ProjectServices.cs | 158 ++++++++++-------- 3 files changed, 111 insertions(+), 152 deletions(-) diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index 183bbc4..c7d7e84 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -42,8 +42,8 @@ namespace Marco.Pms.CacheHelper Builders.Update.Set(r => r.ShortName, project.ShortName), Builders.Update.Set(r => r.ProjectStatus, new StatusMasterMongoDB { - Id = projectStatus?.Id.ToString(), - Status = projectStatus?.Status + Id = projectStatus.Id.ToString(), + Status = projectStatus.Status }), Builders.Update.Set(r => r.StartDate, project.StartDate), Builders.Update.Set(r => r.EndDate, project.EndDate), diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 236e0cb..0122003 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -252,89 +252,28 @@ namespace MarcoBMS.Services.Controllers return StatusCode(response.StatusCode, response); } - [HttpPost("allocation")] - public async Task ManageAllocation(List projectAllocationDot) - { - if (projectAllocationDot != null) - { - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + //[HttpPost("allocation")] + //public async Task ManageAllocation(List projectAllocationDot) + //{ + // // --- Step 1: Input Validation --- + // if (!ModelState.IsValid) + // { + // var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + // _logger.LogWarning("Update project called with invalid model state for ID {ProjectId}. Errors: {Errors}", id, string.Join(", ", errors)); + // return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); + // } - List? result = new List(); - List employeeIds = new List(); - List projectIds = new List(); + // // --- Step 2: Prepare data without I/O --- + // Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + // var response = await _projectServices.UpdateProjectAsync(id, updateProjectDto, tenantId, loggedInEmployee); + // if (response.Success) + // { + // var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeList = employeeIds }; + // await _signalR.SendNotificationAsync(notification); + // } + // return StatusCode(response.StatusCode, response); - foreach (var item in projectAllocationDot) - { - try - { - ProjectAllocation projectAllocation = item.ToProjectAllocationFromProjectAllocationDto(tenantId); - ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == projectAllocation.EmployeeId - && c.ProjectId == projectAllocation.ProjectId - && c.ReAllocationDate == null - && c.TenantId == tenantId).SingleOrDefaultAsync(); - - if (projectAllocationFromDb != null) - { - _context.ProjectAllocations.Attach(projectAllocationFromDb); - - if (item.Status) - { - projectAllocationFromDb.JobRoleId = projectAllocation.JobRoleId; ; - projectAllocationFromDb.IsActive = true; - _context.Entry(projectAllocationFromDb).Property(e => e.JobRoleId).IsModified = true; - _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; - } - else - { - projectAllocationFromDb.ReAllocationDate = DateTime.Now; - projectAllocationFromDb.IsActive = false; - _context.Entry(projectAllocationFromDb).Property(e => e.ReAllocationDate).IsModified = true; - _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; - - employeeIds.Add(projectAllocation.EmployeeId); - projectIds.Add(projectAllocation.ProjectId); - } - await _context.SaveChangesAsync(); - var result1 = new - { - Id = projectAllocationFromDb.Id, - EmployeeId = projectAllocation.EmployeeId, - JobRoleId = projectAllocation.JobRoleId, - IsActive = projectAllocation.IsActive, - ProjectId = projectAllocation.ProjectId, - AllocationDate = projectAllocation.AllocationDate, - ReAllocationDate = projectAllocation.ReAllocationDate, - TenantId = projectAllocation.TenantId - }; - result.Add(result1); - } - else - { - projectAllocation.AllocationDate = DateTime.Now; - projectAllocation.IsActive = true; - _context.ProjectAllocations.Add(projectAllocation); - await _context.SaveChangesAsync(); - - employeeIds.Add(projectAllocation.EmployeeId); - projectIds.Add(projectAllocation.ProjectId); - } - await _cache.ClearAllProjectIds(item.EmpID); - - } - catch (Exception ex) - { - return Ok(ApiResponse.ErrorResponse(ex.Message, ex, 400)); - } - } - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeList = employeeIds }; - - await _signalR.SendNotificationAsync(notification); - return Ok(ApiResponse.SuccessResponse(result, "Data saved successfully", 200)); - - } - return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Work Item Details are not valid.", 400)); - - } + //} [HttpGet("assigned-projects/{employeeId}")] public async Task GetProjectsByEmployee([FromRoute] Guid employeeId) diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs index dcaf20e..7717584 100644 --- a/Marco.Pms.Services/Service/ProjectServices.cs +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -443,21 +443,16 @@ namespace Marco.Pms.Services.Service return ApiResponse.ErrorResponse("Conflict occurred.", "This project has been modified by someone else. Please refresh and try again.", 409); } - // --- Step 4: Perform Side-Effects in the Background (Fire and Forget) --- - // The core database operation is done. Now, we perform non-blocking cache and notification updates. - _ = Task.Run(async () => - { - // Create a DTO of the updated project to pass to background tasks. - var projectDto = _mapper.Map(existingProject); + // --- Step 4: Perform Side-Effects (Fire and Forget) --- + // Create a DTO of the updated project to pass to background tasks. + var projectDto = _mapper.Map(existingProject); - // 4a. Update Cache - await UpdateCacheInBackground(existingProject); - - }); + // 4a. Update Cache + await UpdateCacheInBackground(existingProject); // --- Step 5: Return Success Response Immediately --- // The client gets a fast response without waiting for caching or SignalR. - return ApiResponse.SuccessResponse(_mapper.Map(existingProject), "Project updated successfully.", 200); + return ApiResponse.SuccessResponse(projectDto, "Project updated successfully.", 200); } catch (Exception ex) { @@ -471,41 +466,6 @@ namespace Marco.Pms.Services.Service #region =================================================================== Project Allocation APIs =================================================================== - public async Task> GetEmployeeByProjectID(Guid? projectid, bool includeInactive, Guid tenantId, Employee loggedInEmployee) - { - if (projectid == null) - { - return ApiResponse.ErrorResponse("Invalid Input Parameter", 404); - } - // Fetch assigned project - List result = new List(); - - var employeeQuery = _context.ProjectAllocations - .Include(pa => pa.Employee) - .Where(pa => pa.ProjectId == projectid && pa.TenantId == tenantId && pa.Employee != null); - - if (includeInactive) - { - - result = await employeeQuery.Select(pa => pa.Employee ?? new Employee()).ToListAsync(); - } - else - { - result = await employeeQuery - .Where(pa => pa.IsActive) - .Select(pa => pa.Employee ?? new Employee()).ToListAsync(); - } - - List resultVM = new List(); - foreach (Employee employee in result) - { - EmployeeVM vm = _mapper.Map(employee); - resultVM.Add(vm); - } - - return ApiResponse.SuccessResponse(resultVM, "Successfully fetched the list of employees for seleted project", 200); - } - /// /// Retrieves a list of employees for a specific project. /// This method is optimized to perform all filtering and mapping on the database server. @@ -578,28 +538,6 @@ namespace Marco.Pms.Services.Service } } - public async Task> GetProjectAllocation(Guid? projectId, Guid tenantId, Employee loggedInEmployee) - { - var employees = await _context.ProjectAllocations - .Where(c => c.TenantId == tenantId && c.ProjectId == projectId && c.Employee != null) - .Include(e => e.Employee) - .Select(e => new - { - ID = e.Id, - EmployeeId = e.EmployeeId, - ProjectId = e.ProjectId, - AllocationDate = e.AllocationDate, - ReAllocationDate = e.ReAllocationDate, - FirstName = e.Employee != null ? e.Employee.FirstName : string.Empty, - LastName = e.Employee != null ? e.Employee.LastName : string.Empty, - MiddleName = e.Employee != null ? e.Employee.MiddleName : string.Empty, - IsActive = e.IsActive, - JobRoleId = (e.JobRoleId != null ? e.JobRoleId : e.Employee != null ? e.Employee.JobRoleId : null) - }).ToListAsync(); - - return ApiResponse.SuccessResponse(employees, "Success.", 200); - } - /// /// Retrieves project allocation details for a specific project. /// This method is optimized for performance and includes security checks. @@ -670,6 +608,87 @@ namespace Marco.Pms.Services.Service return ApiResponse.ErrorResponse("An internal server error occurred.", "Database query failed.", 500); } } + + //public async Task> ManageAllocation(List projectAllocationDot, Guid tenantId, Employee loggedInEmployee) + //{ + // if (projectAllocationDot != null) + // { + // List? result = new List(); + // List employeeIds = new List(); + // List projectIds = new List(); + + // foreach (var item in projectAllocationDot) + // { + // try + // { + // //ProjectAllocation projectAllocation = item.ToProjectAllocationFromProjectAllocationDto(tenantId); + // ProjectAllocation projectAllocation = item.ToProjectAllocationFromProjectAllocationDto(tenantId); + // ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == projectAllocation.EmployeeId + // && c.ProjectId == projectAllocation.ProjectId + // && c.ReAllocationDate == null + // && c.TenantId == tenantId).SingleOrDefaultAsync(); + + // if (projectAllocationFromDb != null) + // { + // _context.ProjectAllocations.Attach(projectAllocationFromDb); + + // if (item.Status) + // { + // projectAllocationFromDb.JobRoleId = projectAllocation.JobRoleId; ; + // projectAllocationFromDb.IsActive = true; + // _context.Entry(projectAllocationFromDb).Property(e => e.JobRoleId).IsModified = true; + // _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; + // } + // else + // { + // projectAllocationFromDb.ReAllocationDate = DateTime.Now; + // projectAllocationFromDb.IsActive = false; + // _context.Entry(projectAllocationFromDb).Property(e => e.ReAllocationDate).IsModified = true; + // _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; + + // employeeIds.Add(projectAllocation.EmployeeId); + // projectIds.Add(projectAllocation.ProjectId); + // } + // await _context.SaveChangesAsync(); + // var result1 = new + // { + // Id = projectAllocationFromDb.Id, + // EmployeeId = projectAllocation.EmployeeId, + // JobRoleId = projectAllocation.JobRoleId, + // IsActive = projectAllocation.IsActive, + // ProjectId = projectAllocation.ProjectId, + // AllocationDate = projectAllocation.AllocationDate, + // ReAllocationDate = projectAllocation.ReAllocationDate, + // TenantId = projectAllocation.TenantId + // }; + // result.Add(result1); + // } + // else + // { + // projectAllocation.AllocationDate = DateTime.Now; + // projectAllocation.IsActive = true; + // _context.ProjectAllocations.Add(projectAllocation); + // await _context.SaveChangesAsync(); + + // employeeIds.Add(projectAllocation.EmployeeId); + // projectIds.Add(projectAllocation.ProjectId); + // } + // await _cache.ClearAllProjectIds(item.EmpID); + + // } + // catch (Exception ex) + // { + // return ApiResponse.ErrorResponse(ex.Message, ex, 400); + // } + // } + + // return ApiResponse.SuccessResponse(result, "Data saved successfully", 200); + + // } + // return ApiResponse.ErrorResponse("Invalid details.", "Work Item Details are not valid.", 400); + + //} + #endregion #region =================================================================== Helper Functions =================================================================== @@ -881,7 +900,8 @@ namespace Marco.Pms.Services.Service try { // This logic can be more complex, but the idea is to update or add. - if (!await _cache.UpdateProjectDetailsOnly(project)) + var demo = await _cache.UpdateProjectDetailsOnly(project); + if (!demo) { await _cache.AddProjectDetails(project); } From 7914cf20d44c1816f0f754a67ccae984a73ded0f Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 15 Jul 2025 14:34:26 +0530 Subject: [PATCH 103/124] Removed unused code from employee cache class --- Marco.Pms.CacheHelper/EmployeeCache.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Marco.Pms.CacheHelper/EmployeeCache.cs b/Marco.Pms.CacheHelper/EmployeeCache.cs index 4a668f0..2211393 100644 --- a/Marco.Pms.CacheHelper/EmployeeCache.cs +++ b/Marco.Pms.CacheHelper/EmployeeCache.cs @@ -1,5 +1,4 @@ -using Marco.Pms.DataAccess.Data; -using Marco.Pms.Model.MongoDBModels; +using Marco.Pms.Model.MongoDBModels; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using MongoDB.Driver; @@ -8,13 +7,10 @@ namespace Marco.Pms.CacheHelper { public class EmployeeCache { - private readonly ApplicationDbContext _context; - //private readonly IMongoDatabase _mongoDB; private readonly IMongoCollection _collection; - public EmployeeCache(ApplicationDbContext context, IConfiguration configuration) + public EmployeeCache(IConfiguration configuration) { var connectionString = configuration["MongoDB:ConnectionString"]; - _context = context; var mongoUrl = new MongoUrl(connectionString); var client = new MongoClient(mongoUrl); // Your MongoDB connection string var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name From 72dccc0c6a7090b81d04079e31afe6b0609e66ad Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 15 Jul 2025 15:21:48 +0530 Subject: [PATCH 104/124] Added Employee ID of creater to bucket in Employee IDs --- Marco.Pms.Services/Helpers/DirectoryHelper.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Marco.Pms.Services/Helpers/DirectoryHelper.cs b/Marco.Pms.Services/Helpers/DirectoryHelper.cs index ad9001c..2963ff2 100644 --- a/Marco.Pms.Services/Helpers/DirectoryHelper.cs +++ b/Marco.Pms.Services/Helpers/DirectoryHelper.cs @@ -1184,7 +1184,11 @@ namespace Marco.Pms.Services.Helpers var emplyeeIds = employeeBucketMappings.Select(eb => eb.EmployeeId).ToList(); List? contactBuckets = contactBucketMappings.Where(cb => cb.BucketId == bucket.Id).ToList(); AssignBucketVM bucketVM = bucket.ToAssignBucketVMFromBucket(); - bucketVM.EmployeeIds = emplyeeIds; + if (bucketVM.CreatedBy != null) + { + emplyeeIds.Add(bucketVM.CreatedBy.Id); + } + bucketVM.EmployeeIds = emplyeeIds.Distinct().ToList(); bucketVM.NumberOfContacts = contactBuckets.Count; bucketVMs.Add(bucketVM); } From 168922c278dd5777de8641fd05cd8e955f8f106f Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 15 Jul 2025 15:30:41 +0530 Subject: [PATCH 105/124] Optimized the Project Allocation API --- Marco.Pms.CacheHelper/EmployeeCache.cs | 2 +- .../Projects/ProjectAllocationVM.cs | 13 ++ .../Controllers/ProjectController.cs | 43 ++--- .../MappingProfiles/MappingProfile.cs | 6 + Marco.Pms.Services/Service/ProjectServices.cs | 167 ++++++++++-------- .../ServiceInterfaces/IProjectServices.cs | 2 + 6 files changed, 142 insertions(+), 91 deletions(-) create mode 100644 Marco.Pms.Model/ViewModels/Projects/ProjectAllocationVM.cs diff --git a/Marco.Pms.CacheHelper/EmployeeCache.cs b/Marco.Pms.CacheHelper/EmployeeCache.cs index 2211393..f7b7066 100644 --- a/Marco.Pms.CacheHelper/EmployeeCache.cs +++ b/Marco.Pms.CacheHelper/EmployeeCache.cs @@ -97,7 +97,7 @@ namespace Marco.Pms.CacheHelper var result = await _collection.UpdateOneAsync(filter, update); - if (result.MatchedCount == 0) + if (result.ModifiedCount == 0) return false; return true; diff --git a/Marco.Pms.Model/ViewModels/Projects/ProjectAllocationVM.cs b/Marco.Pms.Model/ViewModels/Projects/ProjectAllocationVM.cs new file mode 100644 index 0000000..6d9138e --- /dev/null +++ b/Marco.Pms.Model/ViewModels/Projects/ProjectAllocationVM.cs @@ -0,0 +1,13 @@ +namespace Marco.Pms.Model.ViewModels.Projects +{ + public class ProjectAllocationVM + { + public Guid Id { get; set; } + public Guid EmployeeId { get; set; } + public Guid? JobRoleId { get; set; } + public bool IsActive { get; set; } = true; + public Guid ProjectId { get; set; } + public DateTime AllocationDate { get; set; } + public DateTime? ReAllocationDate { get; set; } + } +} diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 0122003..b833064 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -252,28 +252,31 @@ namespace MarcoBMS.Services.Controllers return StatusCode(response.StatusCode, response); } - //[HttpPost("allocation")] - //public async Task ManageAllocation(List projectAllocationDot) - //{ - // // --- Step 1: Input Validation --- - // if (!ModelState.IsValid) - // { - // var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); - // _logger.LogWarning("Update project called with invalid model state for ID {ProjectId}. Errors: {Errors}", id, string.Join(", ", errors)); - // return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); - // } + [HttpPost("allocation")] + public async Task ManageAllocation([FromBody] List projectAllocationDot) + { + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) + { + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); + } - // // --- Step 2: Prepare data without I/O --- - // Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - // var response = await _projectServices.UpdateProjectAsync(id, updateProjectDto, tenantId, loggedInEmployee); - // if (response.Success) - // { - // var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeList = employeeIds }; - // await _signalR.SendNotificationAsync(notification); - // } - // return StatusCode(response.StatusCode, response); + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.ManageAllocationAsync(projectAllocationDot, tenantId, loggedInEmployee); + if (response.Success) + { + List employeeIds = response.Data.Select(pa => pa.EmployeeId).ToList(); + List projectIds = response.Data.Select(pa => pa.ProjectId).ToList(); - //} + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeList = employeeIds }; + await _signalR.SendNotificationAsync(notification); + } + return StatusCode(response.StatusCode, response); + + } [HttpGet("assigned-projects/{employeeId}")] public async Task GetProjectsByEmployee([FromRoute] Guid employeeId) diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index 7d627bc..3ca1271 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -43,6 +43,12 @@ namespace Marco.Pms.Services.MappingProfiles CreateMap(); CreateMap(); CreateMap(); + CreateMap() + .ForMember( + dest => dest.EmployeeId, + // Explicitly and safely convert string ProjectStatusId to Guid ProjectStatusId + opt => opt.MapFrom(src => src.EmpID)); + CreateMap(); #endregion #region ======================================================= Projects ======================================================= diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs index 7717584..33df2c0 100644 --- a/Marco.Pms.Services/Service/ProjectServices.cs +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -609,85 +609,112 @@ namespace Marco.Pms.Services.Service } } - //public async Task> ManageAllocation(List projectAllocationDot, Guid tenantId, Employee loggedInEmployee) - //{ - // if (projectAllocationDot != null) - // { - // List? result = new List(); - // List employeeIds = new List(); - // List projectIds = new List(); + /// + /// Manages project allocations for a list of employees, either adding new allocations or deactivating existing ones. + /// This method is optimized to perform all database operations in a single transaction. + /// + /// The list of allocation changes to process. + /// The ID of the current tenant. + /// The current authenticated employee for permission checks. + /// An ApiResponse containing the list of processed allocations. + public async Task>> ManageAllocationAsync(List allocationsDto, Guid tenantId, Employee loggedInEmployee) + { + // --- Step 1: Input Validation --- + if (allocationsDto == null || !allocationsDto.Any()) + { + return ApiResponse>.ErrorResponse("Invalid details.", "Allocation details list cannot be null or empty.", 400); + } - // foreach (var item in projectAllocationDot) - // { - // try - // { - // //ProjectAllocation projectAllocation = item.ToProjectAllocationFromProjectAllocationDto(tenantId); - // ProjectAllocation projectAllocation = item.ToProjectAllocationFromProjectAllocationDto(tenantId); - // ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == projectAllocation.EmployeeId - // && c.ProjectId == projectAllocation.ProjectId - // && c.ReAllocationDate == null - // && c.TenantId == tenantId).SingleOrDefaultAsync(); + _logger.LogInfo("Starting to manage {AllocationCount} allocations for user {UserId}.", allocationsDto.Count, loggedInEmployee.Id); - // if (projectAllocationFromDb != null) - // { - // _context.ProjectAllocations.Attach(projectAllocationFromDb); + // --- (Placeholder) Security Check --- + // In a real application, you would check if the loggedInEmployee has permission + // to manage allocations for ALL projects involved in this batch. + var projectIdsInBatch = allocationsDto.Select(a => a.ProjectId).Distinct().ToList(); + var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageTeam, loggedInEmployee.Id); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} trying to manage allocations for projects.", loggedInEmployee.Id); + return ApiResponse>.ErrorResponse("Access Denied.", "You do not have permission to manage one or more projects in this request.", 403); + } - // if (item.Status) - // { - // projectAllocationFromDb.JobRoleId = projectAllocation.JobRoleId; ; - // projectAllocationFromDb.IsActive = true; - // _context.Entry(projectAllocationFromDb).Property(e => e.JobRoleId).IsModified = true; - // _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; - // } - // else - // { - // projectAllocationFromDb.ReAllocationDate = DateTime.Now; - // projectAllocationFromDb.IsActive = false; - // _context.Entry(projectAllocationFromDb).Property(e => e.ReAllocationDate).IsModified = true; - // _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; + // --- Step 2: Fetch all relevant existing data in ONE database call --- + var employeeProjectPairs = allocationsDto.Select(a => new { a.EmpID, a.ProjectId }).ToList(); + List employeeIds = allocationsDto.Select(a => a.EmpID).Distinct().ToList(); - // employeeIds.Add(projectAllocation.EmployeeId); - // projectIds.Add(projectAllocation.ProjectId); - // } - // await _context.SaveChangesAsync(); - // var result1 = new - // { - // Id = projectAllocationFromDb.Id, - // EmployeeId = projectAllocation.EmployeeId, - // JobRoleId = projectAllocation.JobRoleId, - // IsActive = projectAllocation.IsActive, - // ProjectId = projectAllocation.ProjectId, - // AllocationDate = projectAllocation.AllocationDate, - // ReAllocationDate = projectAllocation.ReAllocationDate, - // TenantId = projectAllocation.TenantId - // }; - // result.Add(result1); - // } - // else - // { - // projectAllocation.AllocationDate = DateTime.Now; - // projectAllocation.IsActive = true; - // _context.ProjectAllocations.Add(projectAllocation); - // await _context.SaveChangesAsync(); + // Fetch all currently active allocations for the employees and projects in this batch. + // We use a dictionary for fast O(1) lookups inside the loop. + var existingAllocations = await _context.ProjectAllocations + .Where(pa => pa.TenantId == tenantId && + employeeIds.Contains(pa.EmployeeId) && + pa.ReAllocationDate == null) + .ToDictionaryAsync(pa => (pa.EmployeeId, pa.ProjectId)); - // employeeIds.Add(projectAllocation.EmployeeId); - // projectIds.Add(projectAllocation.ProjectId); - // } - // await _cache.ClearAllProjectIds(item.EmpID); + var processedAllocations = new List(); - // } - // catch (Exception ex) - // { - // return ApiResponse.ErrorResponse(ex.Message, ex, 400); - // } - // } + // --- Step 3: Process logic IN MEMORY --- + foreach (var dto in allocationsDto) + { + var key = (dto.EmpID, dto.ProjectId); + existingAllocations.TryGetValue(key, out var existingAllocation); - // return ApiResponse.SuccessResponse(result, "Data saved successfully", 200); + if (dto.Status == false) // User wants to DEACTIVATE the allocation + { + if (existingAllocation != null) + { + // Mark the existing allocation for deactivation + existingAllocation.ReAllocationDate = DateTime.UtcNow; // Use UtcNow for servers + existingAllocation.IsActive = false; + _context.ProjectAllocations.Update(existingAllocation); + processedAllocations.Add(existingAllocation); + } + // If it doesn't exist, we do nothing. The desired state is "not allocated". + } + else // User wants to ACTIVATE the allocation + { + if (existingAllocation == null) + { + // Create a new allocation because one doesn't exist + var newAllocation = _mapper.Map(dto); + newAllocation.TenantId = tenantId; + newAllocation.AllocationDate = DateTime.UtcNow; + newAllocation.IsActive = true; + _context.ProjectAllocations.Add(newAllocation); + processedAllocations.Add(newAllocation); + } + // If it already exists and is active, we do nothing. The state is already correct. + } + try + { + await _cache.ClearAllProjectIds(dto.EmpID); + _logger.LogInfo("Successfully completed cache invalidation for employee {EmployeeId}.", dto.EmpID); + } + catch (Exception ex) + { + // Log the error but don't fail the entire request, as the primary DB operation succeeded. + _logger.LogError(ex, "Cache invalidation failed for employees after a successful database update."); + } + } - // } - // return ApiResponse.ErrorResponse("Invalid details.", "Work Item Details are not valid.", 400); + try + { + // --- Step 4: Save all changes in a SINGLE TRANSACTION --- + // All Adds and Updates are sent to the database in one batch. + // If any part fails, the entire transaction is rolled back. + await _context.SaveChangesAsync(); + _logger.LogInfo("Successfully saved {ChangeCount} allocation changes to the database.", processedAllocations.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to save allocation changes to the database."); + return ApiResponse>.ErrorResponse("Database Error.", "An error occurred while saving the changes.", 500); + } - //} + + // --- Step 5: Map results and return success --- + var resultVm = _mapper.Map>(processedAllocations); + return ApiResponse>.SuccessResponse(resultVm, "Allocations managed successfully.", 200); + } #endregion diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs index d0539b0..2552444 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs @@ -1,6 +1,7 @@ using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Utilities; +using Marco.Pms.Model.ViewModels.Projects; namespace Marco.Pms.Services.Service.ServiceInterfaces { @@ -15,5 +16,6 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> UpdateProjectAsync(Guid id, UpdateProjectDto updateProjectDto, Guid tenantId, Employee loggedInEmployee); Task> GetEmployeeByProjectIdAsync(Guid? projectId, bool includeInactive, Guid tenantId, Employee loggedInEmployee); Task> GetProjectAllocationAsync(Guid? projectId, Guid tenantId, Employee loggedInEmployee); + Task>> ManageAllocationAsync(List projectAllocationDots, Guid tenantId, Employee loggedInEmployee); } } From c03fae4b65dd4d4fe58e6cf79d850d90cb136808 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 15 Jul 2025 15:37:15 +0530 Subject: [PATCH 106/124] Added Sonar files in git ignore --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9491a2f..a6a47c3 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,7 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd + +# Sonar +/.sonarqube \ No newline at end of file From c90f39082a76e0e2a9cd49d025ee30fd8cd6e6a0 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 15 Jul 2025 16:37:57 +0530 Subject: [PATCH 107/124] Optimized the project allocation by employee Id Apis --- .../Controllers/ProjectController.cs | 130 +++---------- .../MappingProfiles/MappingProfile.cs | 1 + Marco.Pms.Services/Service/ProjectServices.cs | 180 ++++++++++++++++++ .../ServiceInterfaces/IProjectServices.cs | 2 + 4 files changed, 207 insertions(+), 106 deletions(-) diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index b833064..82ce0dd 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -17,7 +17,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.CodeAnalysis; using Microsoft.EntityFrameworkCore; using MongoDB.Driver; -using Project = Marco.Pms.Model.Projects.Project; namespace MarcoBMS.Services.Controllers { @@ -281,123 +280,42 @@ namespace MarcoBMS.Services.Controllers [HttpGet("assigned-projects/{employeeId}")] public async Task GetProjectsByEmployee([FromRoute] Guid employeeId) { - if (employeeId == Guid.Empty) + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) { - return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Employee id not valid.", 400)); + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("Get project list by employee Id called with invalid model state \n Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - List projectList = await _context.ProjectAllocations - .Where(c => c.TenantId == tenantId && c.EmployeeId == employeeId && c.IsActive) - .Select(c => c.ProjectId).Distinct() - .ToListAsync(); - - if (!projectList.Any()) - { - return NotFound(ApiResponse.SuccessResponse(new List(), "No projects found.", 200)); - } - - - List projectlist = await _context.Projects - .Where(p => projectList.Contains(p.Id)) - .ToListAsync(); - - List projects = new List(); - - - foreach (var project in projectlist) - { - - projects.Add(project.ToProjectListVMFromProject()); - } - - - - return Ok(ApiResponse.SuccessResponse(projects, "Success.", 200)); + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetProjectsByEmployeeAsync(employeeId, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } [HttpPost("assign-projects/{employeeId}")] - public async Task AssigneProjectsToEmployee([FromBody] List projectAllocationDtos, [FromRoute] Guid employeeId) + public async Task AssigneProjectsToEmployee([FromBody] List projectAllocationDtos, [FromRoute] Guid employeeId) { - if (projectAllocationDtos != null && employeeId != Guid.Empty) + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) { - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - List? result = new List(); - List projectIds = new List(); + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); + } - foreach (var projectAllocationDto in projectAllocationDtos) - { - try - { - ProjectAllocation projectAllocation = projectAllocationDto.ToProjectAllocationFromProjectsAllocationDto(tenantId, employeeId); - ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.ProjectId == projectAllocationDto.ProjectId && c.ReAllocationDate == null && c.TenantId == tenantId).SingleOrDefaultAsync(); - - if (projectAllocationFromDb != null) - { - - - _context.ProjectAllocations.Attach(projectAllocationFromDb); - - if (projectAllocationDto.Status) - { - projectAllocationFromDb.JobRoleId = projectAllocation.JobRoleId; ; - projectAllocationFromDb.IsActive = true; - _context.Entry(projectAllocationFromDb).Property(e => e.JobRoleId).IsModified = true; - _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; - } - else - { - projectAllocationFromDb.ReAllocationDate = DateTime.UtcNow; - projectAllocationFromDb.IsActive = false; - _context.Entry(projectAllocationFromDb).Property(e => e.ReAllocationDate).IsModified = true; - _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; - - projectIds.Add(projectAllocation.ProjectId); - } - await _context.SaveChangesAsync(); - var result1 = new - { - Id = projectAllocationFromDb.Id, - EmployeeId = projectAllocation.EmployeeId, - JobRoleId = projectAllocation.JobRoleId, - IsActive = projectAllocation.IsActive, - ProjectId = projectAllocation.ProjectId, - AllocationDate = projectAllocation.AllocationDate, - ReAllocationDate = projectAllocation.ReAllocationDate, - TenantId = projectAllocation.TenantId - }; - result.Add(result1); - } - else - { - projectAllocation.AllocationDate = DateTime.Now; - projectAllocation.IsActive = true; - _context.ProjectAllocations.Add(projectAllocation); - await _context.SaveChangesAsync(); - - projectIds.Add(projectAllocation.ProjectId); - - } - - - } - catch (Exception ex) - { - - return Ok(ApiResponse.ErrorResponse(ex.Message, ex, 400)); - } - } - await _cache.ClearAllProjectIds(employeeId); - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeId = employeeId }; + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.AssigneProjectsToEmployeeAsync(projectAllocationDtos, employeeId, tenantId, loggedInEmployee); + if (response.Success) + { + List projectIds = response.Data.Select(pa => pa.ProjectId).ToList(); + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeId = employeeId }; await _signalR.SendNotificationAsync(notification); - - return Ok(ApiResponse.SuccessResponse(result, "Data saved successfully", 200)); } - else - { - return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "All Field is required", 400)); - } - + return StatusCode(response.StatusCode, response); } #endregion diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index 3ca1271..ea42d16 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -48,6 +48,7 @@ namespace Marco.Pms.Services.MappingProfiles dest => dest.EmployeeId, // Explicitly and safely convert string ProjectStatusId to Guid ProjectStatusId opt => opt.MapFrom(src => src.EmpID)); + CreateMap(); CreateMap(); #endregion diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs index 33df2c0..9024112 100644 --- a/Marco.Pms.Services/Service/ProjectServices.cs +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -716,6 +716,186 @@ namespace Marco.Pms.Services.Service return ApiResponse>.SuccessResponse(resultVm, "Allocations managed successfully.", 200); } + /// + /// Retrieves a list of active projects assigned to a specific employee. + /// + /// The ID of the employee whose projects are being requested. + /// The ID of the current tenant. + /// The current authenticated employee for permission checks. + /// An ApiResponse containing a list of basic project details or an error. + public async Task> GetProjectsByEmployeeAsync(Guid employeeId, Guid tenantId, Employee loggedInEmployee) + { + // --- Step 1: Input Validation --- + if (employeeId == Guid.Empty) + { + return ApiResponse.ErrorResponse("Invalid details.", "A valid employee ID is required.", 400); + } + + _logger.LogInfo("Fetching projects for Employee {EmployeeId} by User {UserId}", employeeId, loggedInEmployee.Id); + + try + { + // --- Step 2: Clarified Security Check --- + // The permission should be about viewing another employee's assignments, not a generic "Manage Team". + // This is a placeholder for your actual, more specific permission logic. + // It should also handle the case where a user is requesting their own projects (employeeId == loggedInEmployee.Id). + var hasPermission = await _permission.HasPermission(PermissionsMaster.ViewProject, loggedInEmployee.Id); + var projectIds = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} trying to view projects for employee {TargetEmployeeId}.", loggedInEmployee.Id, employeeId); + return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to view this employee's projects.", 403); + } + + // --- Step 3: Execute a Single, Highly Efficient Database Query --- + // This query projects directly to the ViewModel on the database server. + var projects = await _context.ProjectAllocations + // 1. Filter the linking table down to the relevant records. + .Where(pa => + pa.TenantId == tenantId && + pa.EmployeeId == employeeId && // Target the specified employee + pa.IsActive && // Only active assignments + projectIds.Contains(pa.ProjectId) && + pa.Project != null) // Safety check for data integrity + + // 2. Navigate to the Project entity. + .Select(pa => pa.Project) + + // 3. Ensure the final result set is unique (in case of multiple active allocations to the same project). + .Distinct() + + // 4. Project directly to the ViewModel using AutoMapper's IQueryable Extensions. + // This generates an efficient SQL "SELECT Id, Name, Code FROM..." statement. + .ProjectTo(_mapper.ConfigurationProvider) + + // 5. Execute the query. + .ToListAsync(); + + _logger.LogInfo("Successfully retrieved {ProjectCount} projects for employee {EmployeeId}.", projects.Count, employeeId); + + // The original check for an empty list is still good practice. + if (!projects.Any()) + { + return ApiResponse.SuccessResponse(new List(), "No active projects found for this employee.", 200); + } + + return ApiResponse.SuccessResponse(projects, "Projects retrieved successfully.", 200); + } + catch (Exception ex) + { + // --- Step 4: Graceful Error Handling --- + _logger.LogError(ex, "An error occurred while fetching projects for employee {EmployeeId}.", employeeId); + return ApiResponse.ErrorResponse("An internal server error occurred.", "Database query failed.", 500); + } + } + + /// + /// Manages project assignments for a single employee, processing a batch of projects to activate or deactivate. + /// This method is optimized to perform all database operations in a single, atomic transaction. + /// + /// A list of projects to assign or un-assign. + /// The ID of the employee whose assignments are being managed. + /// The ID of the current tenant. + /// The current authenticated employee for permission checks. + /// An ApiResponse containing the list of processed allocations. + public async Task>> AssigneProjectsToEmployeeAsync(List allocationsDto, Guid employeeId, Guid tenantId, Employee loggedInEmployee) + { + // --- Step 1: Input Validation --- + if (allocationsDto == null || !allocationsDto.Any() || employeeId == Guid.Empty) + { + return ApiResponse>.ErrorResponse("Invalid details.", "A valid employee ID and a list of projects are required.", 400); + } + + _logger.LogInfo("Starting to manage {AllocationCount} project assignments for Employee {EmployeeId}.", allocationsDto.Count, employeeId); + + // --- (Placeholder) Security Check --- + // You MUST verify that the loggedInEmployee has permission to modify the assignments for the target employeeId. + var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageTeam, loggedInEmployee.Id); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} trying to manage assignments for employee {TargetEmployeeId}.", loggedInEmployee.Id, employeeId); + return ApiResponse>.ErrorResponse("Access Denied.", "You do not have permission to manage this employee's assignments.", 403); + } + + // --- Step 2: Fetch all relevant existing data in ONE database call --- + var projectIdsInDto = allocationsDto.Select(p => p.ProjectId).ToList(); + + // Fetch all currently active allocations for this employee for the projects in the request. + // We use a dictionary keyed by ProjectId for fast O(1) lookups inside the loop. + var existingActiveAllocations = await _context.ProjectAllocations + .Where(pa => pa.TenantId == tenantId && + pa.EmployeeId == employeeId && + projectIdsInDto.Contains(pa.ProjectId) && + pa.ReAllocationDate == null) // Only fetch active ones + .ToDictionaryAsync(pa => pa.ProjectId); + + var processedAllocations = new List(); + + // --- Step 3: Process all logic IN MEMORY, tracking changes --- + foreach (var dto in allocationsDto) + { + existingActiveAllocations.TryGetValue(dto.ProjectId, out var existingAllocation); + + if (dto.Status == false) // DEACTIVATE this project assignment + { + if (existingAllocation != null) + { + // Correct Update Pattern: Modify the fetched entity directly. + existingAllocation.ReAllocationDate = DateTime.UtcNow; // Use UTC for servers + existingAllocation.IsActive = false; + _context.ProjectAllocations.Update(existingAllocation); + processedAllocations.Add(existingAllocation); + } + // If it's not in our dictionary, it's already inactive. Do nothing. + } + else // ACTIVATE this project assignment + { + if (existingAllocation == null) + { + // Create a new allocation because an active one doesn't exist. + var newAllocation = _mapper.Map(dto); + newAllocation.EmployeeId = employeeId; + newAllocation.TenantId = tenantId; + newAllocation.AllocationDate = DateTime.UtcNow; + newAllocation.IsActive = true; + _context.ProjectAllocations.Add(newAllocation); + processedAllocations.Add(newAllocation); + } + // If it already exists in our dictionary, it's already active. Do nothing. + } + } + + try + { + // --- Step 4: Save all Adds and Updates in a SINGLE ATOMIC TRANSACTION --- + if (processedAllocations.Any()) + { + await _context.SaveChangesAsync(); + _logger.LogInfo("Successfully saved {ChangeCount} assignment changes for employee {EmployeeId}.", processedAllocations.Count, employeeId); + } + } + catch (DbUpdateException ex) + { + _logger.LogError(ex, "Failed to save assignment changes for employee {EmployeeId}.", employeeId); + return ApiResponse>.ErrorResponse("Database Error.", "An error occurred while saving the changes.", 500); + } + + // --- Step 5: Invalidate Cache ONCE after successful save --- + try + { + await _cache.ClearAllProjectIds(employeeId); + _logger.LogInfo("Successfully queued cache invalidation for employee {EmployeeId}.", employeeId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Background cache invalidation failed for employee {EmployeeId}", employeeId); + } + + // --- Step 6: Map results using AutoMapper and return success --- + var resultVm = _mapper.Map>(processedAllocations); + return ApiResponse>.SuccessResponse(resultVm, "Assignments managed successfully.", 200); + } + #endregion #region =================================================================== Helper Functions =================================================================== diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs index 2552444..bafa582 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs @@ -17,5 +17,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> GetEmployeeByProjectIdAsync(Guid? projectId, bool includeInactive, Guid tenantId, Employee loggedInEmployee); Task> GetProjectAllocationAsync(Guid? projectId, Guid tenantId, Employee loggedInEmployee); Task>> ManageAllocationAsync(List projectAllocationDots, Guid tenantId, Employee loggedInEmployee); + Task> GetProjectsByEmployeeAsync(Guid employeeId, Guid tenantId, Employee loggedInEmployee); + Task>> AssigneProjectsToEmployeeAsync(List projectAllocationDtos, Guid employeeId, Guid tenantId, Employee loggedInEmployee); } } From 57b7f941e61204fea4d7e85e7d1224c76f93ca67 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 16 Jul 2025 15:08:53 +0530 Subject: [PATCH 108/124] Optimized the manage task API in projectController --- Marco.Pms.CacheHelper/ProjectCache.cs | 33 +- .../{WorkItemDot.cs => WorkItemDto.cs} | 2 +- Marco.Pms.Model/Mapper/InfraMapper.cs | 2 +- .../Controllers/ProjectController.cs | 298 ++-------- .../Helpers/CacheUpdateHelper.cs | 17 +- Marco.Pms.Services/Helpers/GeneralHelper.cs | 214 +++++++ Marco.Pms.Services/Helpers/ProjectsHelper.cs | 4 +- .../MappingProfiles/MappingProfile.cs | 5 + Marco.Pms.Services/Program.cs | 1 + Marco.Pms.Services/Service/ProjectServices.cs | 547 +++++++++++++++++- .../ServiceInterfaces/IProjectServices.cs | 4 + 11 files changed, 826 insertions(+), 301 deletions(-) rename Marco.Pms.Model/Dtos/Projects/{WorkItemDot.cs => WorkItemDto.cs} (94%) create mode 100644 Marco.Pms.Services/Helpers/GeneralHelper.cs diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index c7d7e84..833e1a0 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -406,45 +406,22 @@ namespace Marco.Pms.CacheHelper return workItems; } - public async Task ManageWorkItemDetailsToCache(List workItems) + public async Task ManageWorkItemDetailsToCache(List workItems) { - var activityIds = workItems.Select(wi => wi.ActivityId).ToList(); - var workCategoryIds = workItems.Select(wi => wi.WorkCategoryId).ToList(); - var workItemIds = workItems.Select(wi => wi.Id).ToList(); - // fetching Activity master - var activities = await _context.ActivityMasters.Where(a => activityIds.Contains(a.Id)).ToListAsync() ?? new List(); - - // Fetching Work Category - var workCategories = await _context.WorkCategoryMasters.Where(wc => workCategoryIds.Contains(wc.Id)).ToListAsync() ?? new List(); - var task = await _context.TaskAllocations.Where(t => workItemIds.Contains(t.WorkItemId) && t.AssignmentDate == DateTime.UtcNow).ToListAsync(); - var todaysAssign = task.Sum(t => t.PlannedTask); - foreach (WorkItem workItem in workItems) + foreach (WorkItemMongoDB workItem in workItems) { - var activity = activities.FirstOrDefault(a => a.Id == workItem.ActivityId) ?? new ActivityMaster(); - var workCategory = workCategories.FirstOrDefault(a => a.Id == workItem.WorkCategoryId) ?? new WorkCategoryMaster(); - var filter = Builders.Filter.Eq(p => p.Id, workItem.Id.ToString()); var updates = Builders.Update.Combine( Builders.Update.Set(r => r.WorkAreaId, workItem.WorkAreaId.ToString()), Builders.Update.Set(r => r.ParentTaskId, (workItem.ParentTaskId != null ? workItem.ParentTaskId.ToString() : null)), Builders.Update.Set(r => r.PlannedWork, workItem.PlannedWork), - Builders.Update.Set(r => r.TodaysAssigned, todaysAssign), + Builders.Update.Set(r => r.TodaysAssigned, workItem.TodaysAssigned), Builders.Update.Set(r => r.CompletedWork, workItem.CompletedWork), Builders.Update.Set(r => r.Description, workItem.Description), Builders.Update.Set(r => r.TaskDate, workItem.TaskDate), Builders.Update.Set(r => r.ExpireAt, DateTime.UtcNow.Date.AddDays(1)), - Builders.Update.Set(r => r.ActivityMaster, new ActivityMasterMongoDB - { - Id = activity.Id.ToString(), - ActivityName = activity.ActivityName, - UnitOfMeasurement = activity.UnitOfMeasurement - }), - Builders.Update.Set(r => r.WorkCategoryMaster, new WorkCategoryMasterMongoDB - { - Id = workCategory.Id.ToString(), - Name = workCategory.Name, - Description = workCategory.Description, - }) + Builders.Update.Set(r => r.ActivityMaster, workItem.ActivityMaster), + Builders.Update.Set(r => r.WorkCategoryMaster, workItem.WorkCategoryMaster) ); var options = new UpdateOptions { IsUpsert = true }; var result = await _taskCollection.UpdateOneAsync(filter, updates, options); diff --git a/Marco.Pms.Model/Dtos/Projects/WorkItemDot.cs b/Marco.Pms.Model/Dtos/Projects/WorkItemDto.cs similarity index 94% rename from Marco.Pms.Model/Dtos/Projects/WorkItemDot.cs rename to Marco.Pms.Model/Dtos/Projects/WorkItemDto.cs index e6ba436..7c98051 100644 --- a/Marco.Pms.Model/Dtos/Projects/WorkItemDot.cs +++ b/Marco.Pms.Model/Dtos/Projects/WorkItemDto.cs @@ -2,7 +2,7 @@ namespace Marco.Pms.Model.Dtos.Project { - public class WorkItemDot + public class WorkItemDto { [Key] public Guid? Id { get; set; } diff --git a/Marco.Pms.Model/Mapper/InfraMapper.cs b/Marco.Pms.Model/Mapper/InfraMapper.cs index 4ccb7c8..89097d1 100644 --- a/Marco.Pms.Model/Mapper/InfraMapper.cs +++ b/Marco.Pms.Model/Mapper/InfraMapper.cs @@ -48,7 +48,7 @@ namespace Marco.Pms.Model.Mapper } public static class WorkItemMapper { - public static WorkItem ToWorkItemFromWorkItemDto(this WorkItemDot model, Guid tenantId) + public static WorkItem ToWorkItemFromWorkItemDto(this WorkItemDto model, Guid tenantId) { return new WorkItem { diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 82ce0dd..a10fc66 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -1,9 +1,7 @@ using Marco.Pms.DataAccess.Data; 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.MongoDBModels; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Projects; @@ -325,188 +323,36 @@ namespace MarcoBMS.Services.Controllers [HttpGet("infra-details/{projectId}")] public async Task GetInfraDetails(Guid projectId) { - _logger.LogInfo("GetInfraDetails called for ProjectId: {ProjectId}", projectId); - - // Step 1: Get logged-in employee - var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - // Step 2: Check project-specific permission - var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId); - if (!hasProjectPermission) + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) { - _logger.LogWarning("Project access denied for EmployeeId: {EmployeeId} on ProjectId: {ProjectId}", loggedInEmployee.Id, projectId); - return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have access to this project", 403)); + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("Get Project Infrastructure by ProjectId called with invalid model state \n Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - // Step 3: Check 'ViewInfra' permission - var hasViewInfraPermission = await _permission.HasPermission(PermissionsMaster.ViewProjectInfra, loggedInEmployee.Id); - if (!hasViewInfraPermission) - { - _logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); - return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have access to view infra", 403)); - } - var result = await _cache.GetBuildingInfra(projectId); - if (result == null) - { + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetInfraDetailsAsync(projectId, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); - // Step 4: Fetch buildings for the project - var buildings = await _context.Buildings - .Where(b => b.ProjectId == projectId) - .ToListAsync(); - - var buildingIds = buildings.Select(b => b.Id).ToList(); - - // Step 5: Fetch floors associated with the buildings - var floors = await _context.Floor - .Where(f => buildingIds.Contains(f.BuildingId)) - .ToListAsync(); - - var floorIds = floors.Select(f => f.Id).ToList(); - - // Step 6: Fetch work areas associated with the floors - var workAreas = await _context.WorkAreas - .Where(wa => floorIds.Contains(wa.FloorId)) - .ToListAsync(); - var workAreaIds = workAreas.Select(wa => wa.Id).ToList(); - - // Step 7: Fetch work items associated with the work area - var workItems = await _context.WorkItems - .Where(wi => workAreaIds.Contains(wi.WorkAreaId)) - .ToListAsync(); - - // Step 8: Build the infra hierarchy (Building > Floors > Work Areas) - List Buildings = new List(); - foreach (var building in buildings) - { - double buildingPlannedWorks = 0; - double buildingCompletedWorks = 0; - - var selectedFloors = floors.Where(f => f.BuildingId == building.Id).ToList(); - List Floors = new List(); - foreach (var floor in selectedFloors) - { - double floorPlannedWorks = 0; - double floorCompletedWorks = 0; - var selectedWorkAreas = workAreas.Where(wa => wa.FloorId == floor.Id).ToList(); - List WorkAreas = new List(); - foreach (var workArea in selectedWorkAreas) - { - double workAreaPlannedWorks = 0; - double workAreaCompletedWorks = 0; - var selectedWorkItems = workItems.Where(wi => wi.WorkAreaId == workArea.Id).ToList(); - foreach (var workItem in selectedWorkItems) - { - workAreaPlannedWorks += workItem.PlannedWork; - workAreaCompletedWorks += workItem.CompletedWork; - } - WorkAreaMongoDB workAreaMongo = new WorkAreaMongoDB - { - Id = workArea.Id.ToString(), - AreaName = workArea.AreaName, - PlannedWork = workAreaPlannedWorks, - CompletedWork = workAreaCompletedWorks - }; - WorkAreas.Add(workAreaMongo); - floorPlannedWorks += workAreaPlannedWorks; - floorCompletedWorks += workAreaCompletedWorks; - } - FloorMongoDB floorMongoDB = new FloorMongoDB - { - Id = floor.Id.ToString(), - FloorName = floor.FloorName, - PlannedWork = floorPlannedWorks, - CompletedWork = floorCompletedWorks, - WorkAreas = WorkAreas - }; - Floors.Add(floorMongoDB); - buildingPlannedWorks += floorPlannedWorks; - buildingCompletedWorks += floorCompletedWorks; - } - - var buildingMongo = new BuildingMongoDB - { - Id = building.Id.ToString(), - BuildingName = building.Name, - Description = building.Description, - PlannedWork = buildingPlannedWorks, - CompletedWork = buildingCompletedWorks, - Floors = Floors - }; - Buildings.Add(buildingMongo); - } - result = Buildings; - } - - _logger.LogInfo("Infra details fetched successfully for ProjectId: {ProjectId}, EmployeeId: {EmployeeId}, Buildings: {Count}", - projectId, loggedInEmployee.Id, result.Count); - - return Ok(ApiResponse.SuccessResponse(result, "Infra details fetched successfully", 200)); } [HttpGet("tasks/{workAreaId}")] public async Task GetWorkItems(Guid workAreaId) { - _logger.LogInfo("GetWorkItems called for WorkAreaId: {WorkAreaId}", workAreaId); - - // Step 1: Get the currently logged-in employee - var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - // Step 2: Check if the employee has ViewInfra permission - var hasViewInfraPermission = await _permission.HasPermission(PermissionsMaster.ViewProjectInfra, loggedInEmployee.Id); - if (!hasViewInfraPermission) + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) { - _logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); - return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "You don't have permission to view infrastructure", 403)); + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("Get Work Items by WorkAreaId called with invalid model state \n Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - // Step 3: Check if the specified Work Area exists - var isWorkAreaExist = await _context.WorkAreas.AnyAsync(wa => wa.Id == workAreaId); - if (!isWorkAreaExist) - { - _logger.LogWarning("Work Area not found for WorkAreaId: {WorkAreaId}", workAreaId); - return NotFound(ApiResponse.ErrorResponse("Work Area not found", "Work Area not found in database", 404)); - } - - // Step 4: Fetch WorkItems with related Activity and Work Category data - var workItemVMs = await _cache.GetWorkItemDetailsByWorkArea(workAreaId); - if (workItemVMs == null) - { - var workItems = await _context.WorkItems - .Include(wi => wi.ActivityMaster) - .Include(wi => wi.WorkCategoryMaster) - .Where(wi => wi.WorkAreaId == workAreaId) - .ToListAsync(); - - workItemVMs = workItems.Select(wi => new WorkItemMongoDB - { - Id = wi.Id.ToString(), - WorkAreaId = wi.WorkAreaId.ToString(), - ParentTaskId = wi.ParentTaskId.ToString(), - ActivityMaster = new ActivityMasterMongoDB - { - Id = wi.ActivityId.ToString(), - ActivityName = wi.ActivityMaster != null ? wi.ActivityMaster.ActivityName : null, - UnitOfMeasurement = wi.ActivityMaster != null ? wi.ActivityMaster.UnitOfMeasurement : null - }, - WorkCategoryMaster = new WorkCategoryMasterMongoDB - { - Id = wi.WorkCategoryId.ToString() ?? "", - Name = wi.WorkCategoryMaster != null ? wi.WorkCategoryMaster.Name : "", - Description = wi.WorkCategoryMaster != null ? wi.WorkCategoryMaster.Description : "" - }, - PlannedWork = wi.PlannedWork, - CompletedWork = wi.CompletedWork, - Description = wi.Description, - TaskDate = wi.TaskDate, - }).ToList(); - - await _cache.ManageWorkItemDetails(workItems); - } - - _logger.LogInfo("{Count} work items fetched successfully for WorkAreaId: {WorkAreaId}", workItemVMs.Count, workAreaId); - - // Step 5: Return result - return Ok(ApiResponse.SuccessResponse(workItemVMs, $"{workItemVMs.Count} records of tasks fetched successfully", 200)); + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetWorkItemsAsync(workAreaId, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } #endregion @@ -514,107 +360,29 @@ namespace MarcoBMS.Services.Controllers #region =================================================================== Project Infrastructre Manage APIs =================================================================== [HttpPost("task")] - public async Task CreateProjectTask(List workItemDtos) + public async Task CreateProjectTask([FromBody] List workItemDtos) { - _logger.LogInfo("CreateProjectTask called with {Count} items", workItemDtos?.Count ?? 0); - - // Validate request - if (workItemDtos == null || !workItemDtos.Any()) + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) { - _logger.LogWarning("No work items provided in the request."); - return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Work Item details are not valid.", 400)); + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - var workItemsToCreate = new List(); - var workItemsToUpdate = new List(); - var responseList = new List(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - string message = ""; - List workAreaIds = new List(); - var workItemIds = workItemDtos.Where(wi => wi.Id != null && wi.Id != Guid.Empty).Select(wi => wi.Id).ToList(); - var workItems = await _context.WorkItems.AsNoTracking().Where(wi => workItemIds.Contains(wi.Id)).ToListAsync(); - - foreach (var itemDto in workItemDtos) + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.CreateProjectTaskAsync(workItemDtos, tenantId, loggedInEmployee); + if (response.Success) { - var workItem = itemDto.ToWorkItemFromWorkItemDto(tenantId); - var workArea = await _context.WorkAreas.Include(a => a.Floor).FirstOrDefaultAsync(a => a.Id == workItem.WorkAreaId) ?? new WorkArea(); - - Building building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == (workArea.Floor != null ? workArea.Floor.BuildingId : Guid.Empty)) ?? new Building(); - - if (itemDto.Id != null && itemDto.Id != Guid.Empty) - { - // Update existing - workItemsToUpdate.Add(workItem); - message = $"Task Updated in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}"; - var existingWorkItem = workItems.FirstOrDefault(wi => wi.Id == workItem.Id); - double plannedWork = 0; - double completedWork = 0; - if (existingWorkItem != null) - { - if (existingWorkItem.PlannedWork != workItem.PlannedWork && existingWorkItem.CompletedWork != workItem.CompletedWork) - { - plannedWork = workItem.PlannedWork - existingWorkItem.PlannedWork; - completedWork = workItem.CompletedWork - existingWorkItem.CompletedWork; - } - else if (existingWorkItem.PlannedWork == workItem.PlannedWork && existingWorkItem.CompletedWork != workItem.CompletedWork) - { - plannedWork = 0; - completedWork = workItem.CompletedWork - existingWorkItem.CompletedWork; - } - else if (existingWorkItem.PlannedWork != workItem.PlannedWork && existingWorkItem.CompletedWork == workItem.CompletedWork) - { - plannedWork = workItem.PlannedWork - existingWorkItem.PlannedWork; - completedWork = 0; - } - await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, plannedWork, completedWork); - } - } - else - { - // Create new - workItem.Id = Guid.NewGuid(); - workItemsToCreate.Add(workItem); - message = $"Task Added in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}"; - await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, workItem.PlannedWork, workItem.CompletedWork); - } - - responseList.Add(new WorkItemVM - { - WorkItemId = workItem.Id, - WorkItem = workItem - }); - workAreaIds.Add(workItem.WorkAreaId); + List workAreaIds = response.Data.Select(pa => pa.WorkItem?.WorkAreaId ?? Guid.Empty).ToList(); + string message = response.Message; + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = message }; + await _signalR.SendNotificationAsync(notification); } - string responseMessage = ""; - // Apply DB changes - if (workItemsToCreate.Any()) - { - _logger.LogInfo("Adding {Count} new work items", workItemsToCreate.Count); - await _context.WorkItems.AddRangeAsync(workItemsToCreate); - responseMessage = "Task Added Successfully"; - await _cache.ManageWorkItemDetails(workItemsToCreate); - } + return StatusCode(response.StatusCode, response); - if (workItemsToUpdate.Any()) - { - _logger.LogInfo("Updating {Count} existing work items", workItemsToUpdate.Count); - _context.WorkItems.UpdateRange(workItemsToUpdate); - responseMessage = "Task Updated Successfully"; - await _cache.ManageWorkItemDetails(workItemsToUpdate); - } - - await _context.SaveChangesAsync(); - - _logger.LogInfo("CreateProjectTask completed successfully. Created: {Created}, Updated: {Updated}", workItemsToCreate.Count, workItemsToUpdate.Count); - - - - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = message }; - - await _signalR.SendNotificationAsync(notification); - - return Ok(ApiResponse.SuccessResponse(responseList, responseMessage, 200)); } [HttpDelete("task/{id}")] diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index aca439b..9a01b83 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -17,9 +17,10 @@ namespace Marco.Pms.Services.Helpers private readonly ILoggingService _logger; private readonly IDbContextFactory _dbContextFactory; private readonly ApplicationDbContext _context; + private readonly GeneralHelper _generalHelper; public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache, ReportCache reportCache, ILoggingService logger, - IDbContextFactory dbContextFactory, ApplicationDbContext context) + IDbContextFactory dbContextFactory, ApplicationDbContext context, GeneralHelper generalHelper) { _projectCache = projectCache; _employeeCache = employeeCache; @@ -27,6 +28,7 @@ namespace Marco.Pms.Services.Helpers _logger = logger; _dbContextFactory = dbContextFactory; _context = context; + _generalHelper = generalHelper; } // ------------------------------------ Project Details Cache --------------------------------------- @@ -563,6 +565,19 @@ namespace Marco.Pms.Services.Helpers } } public async Task ManageWorkItemDetails(List workItems) + { + try + { + var workAreaId = workItems.First().WorkAreaId; + var workItemDB = await _generalHelper.GetWorkItemsListFromDB(workAreaId); + await _projectCache.ManageWorkItemDetailsToCache(workItemDB); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while saving workItems form Cache: {Error}", ex.Message); + } + } + public async Task ManageWorkItemDetailsByVM(List workItems) { try { diff --git a/Marco.Pms.Services/Helpers/GeneralHelper.cs b/Marco.Pms.Services/Helpers/GeneralHelper.cs new file mode 100644 index 0000000..c2f8fe4 --- /dev/null +++ b/Marco.Pms.Services/Helpers/GeneralHelper.cs @@ -0,0 +1,214 @@ +using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.MongoDBModels; +using MarcoBMS.Services.Service; +using Microsoft.EntityFrameworkCore; + +namespace Marco.Pms.Services.Helpers +{ + public class GeneralHelper + { + private readonly IDbContextFactory _dbContextFactory; + private readonly ApplicationDbContext _context; // Keeping this for direct scoped context use where appropriate + private readonly ILoggingService _logger; + public GeneralHelper(IDbContextFactory dbContextFactory, + ApplicationDbContext context, + ILoggingService logger) + { + _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); + _context = context ?? throw new ArgumentNullException(nameof(context)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + public async Task> GetProjectInfraFromDB(Guid projectId) + { + // Each task uses its own DbContext instance for thread safety. Projections are used for efficiency. + + // Task to fetch Buildings, Floors, and WorkAreas using projections + var hierarchyTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + var buildings = await context.Buildings.AsNoTracking().Where(b => b.ProjectId == projectId).Select(b => new { b.Id, b.Name, b.Description }).ToListAsync(); + var buildingIds = buildings.Select(b => b.Id).ToList(); + var floors = await context.Floor.AsNoTracking().Where(f => buildingIds.Contains(f.BuildingId)).Select(f => new { f.Id, f.BuildingId, f.FloorName }).ToListAsync(); + var floorIds = floors.Select(f => f.Id).ToList(); + var workAreas = await context.WorkAreas.AsNoTracking().Where(wa => floorIds.Contains(wa.FloorId)).Select(wa => new { wa.Id, wa.FloorId, wa.AreaName }).ToListAsync(); + return (buildings, floors, workAreas); + }); + + // Task to get work summaries, AGGREGATED ON THE DATABASE SERVER + var workSummaryTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + // This is the most powerful optimization. It avoids pulling all WorkItem rows. + return await context.WorkItems.AsNoTracking() + .Where(wi => wi.WorkArea != null && wi.WorkArea.Floor != null && wi.WorkArea.Floor.Building != null && wi.WorkArea.Floor.Building.ProjectId == projectId) + .GroupBy(wi => wi.WorkAreaId) // Group by the parent WorkArea + .Select(g => new + { + WorkAreaId = g.Key, + PlannedWork = g.Sum(i => i.PlannedWork), + CompletedWork = g.Sum(i => i.CompletedWork) + }) + .ToDictionaryAsync(x => x.WorkAreaId); // Return a ready-to-use dictionary for fast lookups + }); + + await Task.WhenAll(hierarchyTask, workSummaryTask); + + var (buildings, floors, workAreas) = await hierarchyTask; + var workSummariesByWorkAreaId = await workSummaryTask; + + // --- Step 4: Build the hierarchy efficiently using Lookups --- + // Using lookups is much faster (O(1)) than repeated .Where() calls (O(n)). + var floorsByBuildingId = floors.ToLookup(f => f.BuildingId); + var workAreasByFloorId = workAreas.ToLookup(wa => wa.FloorId); + + var buildingMongoList = new List(); + foreach (var building in buildings) + { + 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 workArea in workAreasByFloorId[floor.Id]) // Fast lookup + { + // Get the pre-calculated summary from the dictionary. O(1) operation. + workSummariesByWorkAreaId.TryGetValue(workArea.Id, out var summary); + var waPlanned = summary?.PlannedWork ?? 0; + var waCompleted = summary?.CompletedWork ?? 0; + + workAreaMongoList.Add(new WorkAreaMongoDB + { + Id = workArea.Id.ToString(), + AreaName = workArea.AreaName, + PlannedWork = waPlanned, + CompletedWork = waCompleted + }); + + floorPlanned += waPlanned; + floorCompleted += waCompleted; + } + + floorMongoList.Add(new FloorMongoDB + { + Id = floor.Id.ToString(), + FloorName = floor.FloorName, + PlannedWork = floorPlanned, + CompletedWork = floorCompleted, + WorkAreas = workAreaMongoList + }); + + buildingPlanned += floorPlanned; + buildingCompleted += floorCompleted; + } + + buildingMongoList.Add(new BuildingMongoDB + { + Id = building.Id.ToString(), + BuildingName = building.Name, + Description = building.Description, + PlannedWork = buildingPlanned, + CompletedWork = buildingCompleted, + Floors = floorMongoList + }); + } + return buildingMongoList; + } + + /// + /// Retrieves a list of work items for a specific work area, including a summary of tasks assigned for the current day. + /// This method is highly optimized to run database operations in parallel and perform aggregations on the server. + /// + /// The ID of the work area. + /// A list of WorkItemMongoDB objects with calculated daily assignments. + public async Task> GetWorkItemsListFromDB(Guid workAreaId) + { + _logger.LogInfo("Fetching DB work items for WorkAreaId: {WorkAreaId}", workAreaId); + + try + { + // --- Step 1: Run independent database queries in PARALLEL --- + // We can fetch the WorkItems and the aggregated TaskAllocations at the same time. + + // Task 1: Fetch the WorkItem entities and their related data. + var workItemsTask = _context.WorkItems + .Include(wi => wi.ActivityMaster) + .Include(wi => wi.WorkCategoryMaster) + .Where(wi => wi.WorkAreaId == workAreaId) + .AsNoTracking() + .ToListAsync(); + + // Task 2: Fetch and AGGREGATE today's task allocations ON THE DATABASE SERVER. + var todaysAssignmentsTask = Task.Run(async () => + { + // Correctly define "today's" date range to avoid precision issues. + var today = DateTime.UtcNow.Date; + var tomorrow = today.AddDays(1); + + using var context = _dbContextFactory.CreateDbContext(); // Use a factory for thread safety + + // This is the most powerful optimization: + // 1. It filters by WorkAreaId directly, making it independent of the first query. + // 2. It filters by a correct date range. + // 3. It groups and sums on the DB server, returning only a small summary. + return await context.TaskAllocations + .Where(t => t.WorkItem != null && t.WorkItem.WorkAreaId == workAreaId && + t.AssignmentDate >= today && t.AssignmentDate < tomorrow) + .GroupBy(t => t.WorkItemId) + .Select(g => new + { + WorkItemId = g.Key, + TodaysAssigned = g.Sum(x => x.PlannedTask) + }) + // Return a dictionary for instant O(1) lookups later. + .ToDictionaryAsync(x => x.WorkItemId, x => x.TodaysAssigned); + }); + + // Await both parallel database operations to complete. + await Task.WhenAll(workItemsTask, todaysAssignmentsTask); + + // Retrieve the results from the completed tasks. + var workItemsFromDb = await workItemsTask; + var todaysAssignments = await todaysAssignmentsTask; + + // --- Step 2: Map to the ViewModel/MongoDB model efficiently --- + var workItemVMs = workItemsFromDb.Select(wi => new WorkItemMongoDB + { + Id = wi.Id.ToString(), + WorkAreaId = wi.WorkAreaId.ToString(), + ParentTaskId = wi.ParentTaskId.ToString(), + ActivityMaster = wi.ActivityMaster != null ? new ActivityMasterMongoDB + { + Id = wi.ActivityMaster.Id.ToString(), + ActivityName = wi.ActivityMaster.ActivityName, + UnitOfMeasurement = wi.ActivityMaster.UnitOfMeasurement + } : null, + WorkCategoryMaster = wi.WorkCategoryMaster != null ? new WorkCategoryMasterMongoDB + { + Id = wi.WorkCategoryMaster.Id.ToString(), + Name = wi.WorkCategoryMaster.Name, + Description = wi.WorkCategoryMaster.Description + } : null, + PlannedWork = wi.PlannedWork, + CompletedWork = wi.CompletedWork, + Description = wi.Description, + TaskDate = wi.TaskDate, + // Use the fast dictionary lookup instead of the slow in-memory Where/Sum. + TodaysAssigned = todaysAssignments.GetValueOrDefault(wi.Id, 0) + }).ToList(); + + _logger.LogInfo("Successfully processed {WorkItemCount} work items for WorkAreaId: {WorkAreaId}", workItemVMs.Count, workAreaId); + + return workItemVMs; + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while fetching DB work items for WorkAreaId: {WorkAreaId}", workAreaId); + // Return an empty list or re-throw, depending on your application's error handling strategy. + return new List(); + } + } + } +} diff --git a/Marco.Pms.Services/Helpers/ProjectsHelper.cs b/Marco.Pms.Services/Helpers/ProjectsHelper.cs index fe70a0a..e7e1dd6 100644 --- a/Marco.Pms.Services/Helpers/ProjectsHelper.cs +++ b/Marco.Pms.Services/Helpers/ProjectsHelper.cs @@ -11,14 +11,12 @@ namespace MarcoBMS.Services.Helpers public class ProjectsHelper { private readonly ApplicationDbContext _context; - private readonly RolesHelper _rolesHelper; private readonly CacheUpdateHelper _cache; private readonly PermissionServices _permission; - public ProjectsHelper(ApplicationDbContext context, RolesHelper rolesHelper, CacheUpdateHelper cache, PermissionServices permission) + public ProjectsHelper(ApplicationDbContext context, CacheUpdateHelper cache, PermissionServices permission) { _context = context; - _rolesHelper = rolesHelper; _cache = cache; _permission = permission; } diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index ea42d16..50d2ea9 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -50,6 +50,11 @@ namespace Marco.Pms.Services.MappingProfiles opt => opt.MapFrom(src => src.EmpID)); CreateMap(); CreateMap(); + + CreateMap() + .ForMember( + dest => dest.Description, + opt => opt.MapFrom(src => src.Comment)); #endregion #region ======================================================= Projects ======================================================= diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index 26d8eba..3c73416 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -163,6 +163,7 @@ builder.Services.AddScoped(); #endregion #region Helpers +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs index 9024112..6d811fc 100644 --- a/Marco.Pms.Services/Service/ProjectServices.cs +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -29,6 +29,7 @@ namespace Marco.Pms.Services.Service private readonly PermissionServices _permission; private readonly CacheUpdateHelper _cache; private readonly IMapper _mapper; + private readonly GeneralHelper _generalHelper; public ProjectServices( IDbContextFactory dbContextFactory, ApplicationDbContext context, @@ -36,7 +37,8 @@ namespace Marco.Pms.Services.Service ProjectsHelper projectsHelper, PermissionServices permission, CacheUpdateHelper cache, - IMapper mapper) + IMapper mapper, + GeneralHelper generalHelper) { _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); _context = context ?? throw new ArgumentNullException(nameof(context)); @@ -45,6 +47,7 @@ namespace Marco.Pms.Services.Service _permission = permission ?? throw new ArgumentNullException(nameof(permission)); _cache = cache ?? throw new ArgumentNullException(nameof(cache)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + _generalHelper = generalHelper ?? throw new ArgumentNullException(nameof(generalHelper)); } #region =================================================================== Project Get APIs =================================================================== @@ -898,6 +901,525 @@ namespace Marco.Pms.Services.Service #endregion + #region =================================================================== Project InfraStructure Get APIs =================================================================== + + /// + /// Retrieves the full infrastructure hierarchy (Buildings, Floors, Work Areas) for a project, + /// including aggregated work summaries. + /// + public async Task> GetInfraDetailsAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee) + { + _logger.LogInfo("GetInfraDetails called for ProjectId: {ProjectId}", projectId); + + try + { + // --- Step 1: Run independent permission checks in PARALLEL --- + var projectPermissionTask = _permission.HasProjectPermission(loggedInEmployee, projectId); + var viewInfraPermissionTask = _permission.HasPermission(PermissionsMaster.ViewProjectInfra, loggedInEmployee.Id); + + await Task.WhenAll(projectPermissionTask, viewInfraPermissionTask); + + if (!await projectPermissionTask) + { + _logger.LogWarning("Project access denied for EmployeeId: {EmployeeId} on ProjectId: {ProjectId}", loggedInEmployee.Id, projectId); + return ApiResponse.ErrorResponse("Access denied", "You don't have access to this project", 403); + } + if (!await viewInfraPermissionTask) + { + _logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Access denied", "You don't have access to view this project's infrastructure", 403); + } + + // --- Step 2: Cache-First Strategy --- + var cachedResult = await _cache.GetBuildingInfra(projectId); + if (cachedResult != null) + { + _logger.LogInfo("Cache HIT for infra details for ProjectId: {ProjectId}", projectId); + return ApiResponse.SuccessResponse(cachedResult, "Infra details fetched successfully from cache.", 200); + } + + _logger.LogInfo("Cache MISS for infra details for ProjectId: {ProjectId}. Fetching from database.", projectId); + + // --- Step 3: Fetch all required data from the database --- + + var buildingMongoList = await _generalHelper.GetProjectInfraFromDB(projectId); + // --- Step 5: Proactively update the cache --- + //await _cache.SetBuildingInfra(projectId, buildingMongoList); + + _logger.LogInfo("Infra details fetched successfully for ProjectId: {ProjectId}, Buildings: {Count}", projectId, buildingMongoList.Count); + return ApiResponse.SuccessResponse(buildingMongoList, "Infra details fetched successfully", 200); + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while fetching infra details for ProjectId: {ProjectId}", projectId); + return ApiResponse.ErrorResponse("An internal server error occurred.", "An error occurred while processing your request.", 500); + } + } + + /// + /// Retrieves a list of work items for a specific work area, ensuring the user has appropriate permissions. + /// + /// The ID of the work area. + /// The ID of the current tenant. + /// The current authenticated employee for permission checks. + /// An ApiResponse containing a list of work items or an error. + public async Task> GetWorkItemsAsync(Guid workAreaId, Guid tenantId, Employee loggedInEmployee) + { + _logger.LogInfo("GetWorkItems called for WorkAreaId: {WorkAreaId} by User: {UserId}", workAreaId, loggedInEmployee.Id); + + try + { + // --- Step 1: Cache-First Strategy --- + var cachedWorkItems = await _cache.GetWorkItemDetailsByWorkArea(workAreaId); + if (cachedWorkItems != null) + { + _logger.LogInfo("Cache HIT for WorkAreaId: {WorkAreaId}. Returning {Count} items from cache.", workAreaId, cachedWorkItems.Count); + return ApiResponse.SuccessResponse(cachedWorkItems, $"{cachedWorkItems.Count} tasks retrieved successfully from cache.", 200); + } + + _logger.LogInfo("Cache MISS for WorkAreaId: {WorkAreaId}. Fetching from database.", workAreaId); + + // --- Step 2: Security Check First --- + // This pattern remains the most robust: verify permissions before fetching a large list. + var projectInfo = await _context.WorkAreas + .Where(wa => wa.Id == workAreaId && wa.TenantId == tenantId && wa.Floor != null && wa.Floor.Building != null) + .Select(wa => new { wa.Floor!.Building!.ProjectId }) + .FirstOrDefaultAsync(); + + if (projectInfo == null) + { + _logger.LogWarning("Work Area not found for WorkAreaId: {WorkAreaId}", workAreaId); + return ApiResponse.ErrorResponse("Not Found", $"Work Area with ID {workAreaId} not found.", 404); + } + + var hasProjectAccess = await _permission.HasProjectPermission(loggedInEmployee, projectInfo.ProjectId); + var hasGenericViewInfraPermission = await _permission.HasPermission(PermissionsMaster.ViewProjectInfra, loggedInEmployee.Id); + + if (!hasProjectAccess || !hasGenericViewInfraPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} on WorkAreaId {WorkAreaId}.", loggedInEmployee.Id, workAreaId); + return ApiResponse.ErrorResponse("Access Denied", "You do not have sufficient permissions to view these work items.", 403); + } + + // --- Step 3: Fetch Full Entities for Caching and Mapping --- + var workItemVMs = await _generalHelper.GetWorkItemsListFromDB(workAreaId); + + // --- Step 5: Proactively Update the Cache with the Correct Object Type --- + // We now pass the 'workItemsFromDb' list, which is the required List. + + try + { + await _cache.ManageWorkItemDetailsByVM(workItemVMs); + _logger.LogInfo("Successfully queued cache update for WorkAreaId: {WorkAreaId}", workAreaId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Background cache update failed for WorkAreaId: {WorkAreaId}", workAreaId); + } + + + _logger.LogInfo("{Count} work items fetched successfully for WorkAreaId: {WorkAreaId}", workItemVMs.Count, workAreaId); + return ApiResponse.SuccessResponse(workItemVMs, $"{workItemVMs.Count} tasks fetched successfully.", 200); + } + catch (Exception ex) + { + // --- Step 6: Graceful Error Handling --- + _logger.LogError(ex, "An unexpected error occurred while getting work items for WorkAreaId: {WorkAreaId}", workAreaId); + return ApiResponse.ErrorResponse("An internal server error occurred.", null, 500); + } + } + + #endregion + + #region =================================================================== Project Infrastructre Manage APIs =================================================================== + + public async Task> CreateProjectTask1(List workItemDtos, Guid tenantId, Employee loggedInEmployee) + { + _logger.LogInfo("CreateProjectTask called with {Count} items", workItemDtos?.Count ?? 0); + + // Validate request + if (workItemDtos == null || !workItemDtos.Any()) + { + _logger.LogWarning("No work items provided in the request."); + return ApiResponse.ErrorResponse("Invalid details.", "Work Item details are not valid.", 400); + } + + var workItemsToCreate = new List(); + var workItemsToUpdate = new List(); + var responseList = new List(); + string message = ""; + List workAreaIds = new List(); + var workItemIds = workItemDtos.Where(wi => wi.Id != null && wi.Id != Guid.Empty).Select(wi => wi.Id).ToList(); + var workItems = await _context.WorkItems.AsNoTracking().Where(wi => workItemIds.Contains(wi.Id)).ToListAsync(); + + foreach (var itemDto in workItemDtos) + { + var workItem = _mapper.Map(itemDto); + workItem.TenantId = tenantId; + var workArea = await _context.WorkAreas.Include(a => a.Floor).FirstOrDefaultAsync(a => a.Id == workItem.WorkAreaId) ?? new WorkArea(); + + Building building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == (workArea.Floor != null ? workArea.Floor.BuildingId : Guid.Empty)) ?? new Building(); + + if (itemDto.Id != null && itemDto.Id != Guid.Empty) + { + // Update existing + workItemsToUpdate.Add(workItem); + message = $"Task Updated in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; + var existingWorkItem = workItems.FirstOrDefault(wi => wi.Id == workItem.Id); + if (existingWorkItem != null) + { + double plannedWork = workItem.PlannedWork - existingWorkItem.PlannedWork; + double completedWork = workItem.CompletedWork - existingWorkItem.CompletedWork; + await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, plannedWork, completedWork); + } + } + else + { + // Create new + workItem.Id = Guid.NewGuid(); + workItemsToCreate.Add(workItem); + message = $"Task Added in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; + await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, workItem.PlannedWork, workItem.CompletedWork); + } + + responseList.Add(new WorkItemVM + { + WorkItemId = workItem.Id, + WorkItem = workItem + }); + workAreaIds.Add(workItem.WorkAreaId); + + } + // Apply DB changes + if (workItemsToCreate.Any()) + { + _logger.LogInfo("Adding {Count} new work items", workItemsToCreate.Count); + await _context.WorkItems.AddRangeAsync(workItemsToCreate); + await _cache.ManageWorkItemDetails(workItemsToCreate); + } + + if (workItemsToUpdate.Any()) + { + _logger.LogInfo("Updating {Count} existing work items", workItemsToUpdate.Count); + _context.WorkItems.UpdateRange(workItemsToUpdate); + await _cache.ManageWorkItemDetails(workItemsToUpdate); + } + + await _context.SaveChangesAsync(); + + _logger.LogInfo("CreateProjectTask completed successfully. Created: {Created}, Updated: {Updated}", workItemsToCreate.Count, workItemsToUpdate.Count); + + return ApiResponse.SuccessResponse(responseList, message, 200); + } + + /// + /// Creates or updates a batch of work items. + /// This method is optimized to perform all database operations in a single, atomic transaction. + /// + public async Task>> CreateProjectTaskAsync(List workItemDtos, Guid tenantId, Employee loggedInEmployee) + { + _logger.LogInfo("CreateProjectTask called with {Count} items by user {UserId}", workItemDtos?.Count ?? 0, loggedInEmployee.Id); + + // --- Step 1: Input Validation --- + if (workItemDtos == null || !workItemDtos.Any()) + { + _logger.LogWarning("No work items provided in the request."); + return ApiResponse>.ErrorResponse("Invalid details.", "Work Item details list cannot be empty.", 400); + } + + // --- Step 2: Fetch all required existing data in bulk --- + var workAreaIds = workItemDtos.Select(d => d.WorkAreaID).Distinct().ToList(); + var workItemIdsToUpdate = workItemDtos.Where(d => d.Id.HasValue).Select(d => d.Id!.Value).ToList(); + + // Fetch all relevant WorkAreas and their parent hierarchy in ONE query + var workAreasFromDb = await _context.WorkAreas + .Where(wa => wa.Floor != null && wa.Floor.Building != null && workAreaIds.Contains(wa.Id) && wa.TenantId == tenantId) + .Include(wa => wa.Floor!.Building) // Eagerly load the entire path + .ToDictionaryAsync(wa => wa.Id); // Dictionary for fast lookups + + // Fetch all existing WorkItems that need updating in ONE query + var existingWorkItemsToUpdate = await _context.WorkItems + .Where(wi => workItemIdsToUpdate.Contains(wi.Id) && wi.TenantId == tenantId) + .ToDictionaryAsync(wi => wi.Id); // Dictionary for fast lookups + + // --- (Placeholder) Security Check --- + // You MUST verify the user has permission to modify ALL WorkAreas in the batch. + var projectIdsInBatch = workAreasFromDb.Values.Select(wa => wa.Floor!.Building!.ProjectId).Distinct(); + var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageProjectInfra, loggedInEmployee.Id); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} trying to create/update tasks.", loggedInEmployee.Id); + return ApiResponse>.ErrorResponse("Access Denied.", "You do not have permission to modify tasks in one or more of the specified work areas.", 403); + } + + var workItemsToCreate = new List(); + var workItemsToModify = new List(); + var workDeltaForCache = new Dictionary(); // WorkAreaId -> (Delta) + string message = ""; + + // --- Step 3: Process all logic IN MEMORY, tracking changes --- + foreach (var dto in workItemDtos) + { + if (!workAreasFromDb.TryGetValue(dto.WorkAreaID, out var workArea)) + { + _logger.LogWarning("Skipping item because WorkAreaId {WorkAreaId} was not found or is invalid.", dto.WorkAreaID); + continue; // Skip this item as its parent WorkArea is invalid + } + + if (dto.Id.HasValue && existingWorkItemsToUpdate.TryGetValue(dto.Id.Value, out var existingWorkItem)) + { + // --- UPDATE Logic --- + var plannedDelta = dto.PlannedWork - existingWorkItem.PlannedWork; + var completedDelta = dto.CompletedWork - existingWorkItem.CompletedWork; + + // Apply changes from DTO to the fetched entity to prevent data loss + _mapper.Map(dto, existingWorkItem); + workItemsToModify.Add(existingWorkItem); + + // Track the change in work for cache update + workDeltaForCache[workArea.Id] = ( + workDeltaForCache.GetValueOrDefault(workArea.Id).Planned + plannedDelta, + workDeltaForCache.GetValueOrDefault(workArea.Id).Completed + completedDelta + ); + message = $"Task Updated in Building: {workArea.Floor?.Building?.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; + } + else + { + // --- CREATE Logic --- + var newWorkItem = _mapper.Map(dto); + newWorkItem.Id = Guid.NewGuid(); // Ensure new GUID is set + newWorkItem.TenantId = tenantId; + workItemsToCreate.Add(newWorkItem); + + // Track the change in work for cache update + workDeltaForCache[workArea.Id] = ( + workDeltaForCache.GetValueOrDefault(workArea.Id).Planned + newWorkItem.PlannedWork, + workDeltaForCache.GetValueOrDefault(workArea.Id).Completed + newWorkItem.CompletedWork + ); + message = $"Task Added in Building: {workArea.Floor?.Building?.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; + } + } + + try + { + // --- Step 4: Save all database changes in a SINGLE TRANSACTION --- + if (workItemsToCreate.Any()) _context.WorkItems.AddRange(workItemsToCreate); + if (workItemsToModify.Any()) _context.WorkItems.UpdateRange(workItemsToModify); // EF Core handles individual updates correctly here + + if (workItemsToCreate.Any() || workItemsToModify.Any()) + { + await _context.SaveChangesAsync(); + _logger.LogInfo("Successfully saved {CreatedCount} new and {UpdatedCount} updated work items.", workItemsToCreate.Count, workItemsToModify.Count); + + // --- Step 5: Update Cache and SignalR AFTER successful DB save (non-blocking) --- + var allAffectedItems = workItemsToCreate.Concat(workItemsToModify).ToList(); + _ = Task.Run(async () => + { + await UpdateCacheAndNotify(workDeltaForCache, allAffectedItems); + }); + } + } + catch (DbUpdateException ex) + { + _logger.LogError(ex, "A database error occurred while creating/updating tasks."); + return ApiResponse>.ErrorResponse("Database Error", "Failed to save changes.", 500); + } + + // --- Step 6: Prepare and return the response --- + var allProcessedItems = workItemsToCreate.Concat(workItemsToModify).ToList(); + var responseList = allProcessedItems.Select(wi => new WorkItemVM + { + WorkItemId = wi.Id, + WorkItem = wi + }).ToList(); + + + return ApiResponse>.SuccessResponse(responseList, message, 200); + } + + + //public async Task DeleteProjectTask(Guid id) + //{ + // var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + // List workAreaIds = new List(); + // WorkItem? task = await _context.WorkItems.AsNoTracking().Include(t => t.WorkArea).FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId); + // if (task != null) + // { + // if (task.CompletedWork == 0) + // { + // var assignedTask = await _context.TaskAllocations.Where(t => t.WorkItemId == id).ToListAsync(); + // if (assignedTask.Count == 0) + // { + // _context.WorkItems.Remove(task); + // await _context.SaveChangesAsync(); + // _logger.LogInfo("Task with ID {WorkItemId} has been successfully deleted.", task.Id); + + // var floorId = task.WorkArea?.FloorId; + // var floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == floorId); + + + // workAreaIds.Add(task.WorkAreaId); + // var projectId = floor?.Building?.ProjectId; + + // var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = $"Task Deleted in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}, in Area: {task.WorkArea?.AreaName} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}" }; + // await _signalR.SendNotificationAsync(notification); + // await _cache.DeleteWorkItemByIdAsync(task.Id); + // if (projectId != null) + // { + // await _cache.DeleteProjectByIdAsync(projectId.Value); + // } + // } + // else + // { + // _logger.LogWarning("Task with ID {WorkItemId} is currently assigned and cannot be deleted.", task.Id); + // return BadRequest(ApiResponse.ErrorResponse("Task is currently assigned and cannot be deleted.", "Task is currently assigned and cannot be deleted.", 400)); + // } + // } + // else + // { + // double percentage = (task.CompletedWork / task.PlannedWork) * 100; + // percentage = Math.Round(percentage, 2); + // _logger.LogWarning("Task with ID {WorkItemId} is {CompletionPercentage}% complete and cannot be deleted", task.Id, percentage); + // return BadRequest(ApiResponse.ErrorResponse(System.String.Format("Task is {0}% complete and cannot be deleted", percentage), System.String.Format("Task is {0}% complete and cannot be deleted", percentage), 400)); + + // } + // } + // else + // { + // _logger.LogWarning("Task with ID {WorkItemId} not found ID database", id); + // } + // return Ok(ApiResponse.SuccessResponse(new { }, "Task deleted successfully", 200)); + //} + + //public async Task ManageProjectInfra(List infraDots) + //{ + // var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // var responseData = new InfraVM { }; + // string responseMessage = ""; + // string message = ""; + // List projectIds = new List(); + // if (infraDots != null) + // { + // foreach (var item in infraDots) + // { + // if (item.Building != null) + // { + + // Building building = item.Building.ToBuildingFromBuildingDto(tenantId); + // building.TenantId = tenantId; + + // if (item.Building.Id == null) + // { + // //create + // _context.Buildings.Add(building); + // await _context.SaveChangesAsync(); + // responseData.building = building; + // responseMessage = "Buliding Added Successfully"; + // message = "Building Added"; + // await _cache.AddBuildngInfra(building.ProjectId, building); + // } + // else + // { + // //update + // _context.Buildings.Update(building); + // await _context.SaveChangesAsync(); + // responseData.building = building; + // responseMessage = "Buliding Updated Successfully"; + // message = "Building Updated"; + // await _cache.UpdateBuildngInfra(building.ProjectId, building); + // } + // projectIds.Add(building.ProjectId); + // } + // if (item.Floor != null) + // { + // Floor floor = item.Floor.ToFloorFromFloorDto(tenantId); + // floor.TenantId = tenantId; + // bool isCreated = false; + + // if (item.Floor.Id == null) + // { + // //create + // _context.Floor.Add(floor); + // await _context.SaveChangesAsync(); + // responseData.floor = floor; + // responseMessage = "Floor Added Successfully"; + // message = "Floor Added"; + // isCreated = true; + // } + // else + // { + // //update + // _context.Floor.Update(floor); + // await _context.SaveChangesAsync(); + // responseData.floor = floor; + // responseMessage = "Floor Updated Successfully"; + // message = "Floor Updated"; + // } + // Building? building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == floor.BuildingId); + // var projectId = building?.ProjectId ?? Guid.Empty; + // projectIds.Add(projectId); + // message = $"{message} in Building: {building?.Name}"; + // if (isCreated) + // { + // await _cache.AddBuildngInfra(projectId, floor: floor); + // } + // else + // { + // await _cache.UpdateBuildngInfra(projectId, floor: floor); + // } + // } + // if (item.WorkArea != null) + // { + // WorkArea workArea = item.WorkArea.ToWorkAreaFromWorkAreaDto(tenantId); + // workArea.TenantId = tenantId; + // bool isCreated = false; + + // if (item.WorkArea.Id == null) + // { + // //create + // _context.WorkAreas.Add(workArea); + // await _context.SaveChangesAsync(); + // responseData.workArea = workArea; + // responseMessage = "Work Area Added Successfully"; + // message = "Work Area Added"; + // isCreated = true; + // } + // else + // { + // //update + // _context.WorkAreas.Update(workArea); + // await _context.SaveChangesAsync(); + // responseData.workArea = workArea; + // responseMessage = "Work Area Updated Successfully"; + // message = "Work Area Updated"; + // } + // Floor? floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == workArea.FloorId); + // var projectId = floor?.Building?.ProjectId ?? Guid.Empty; + // projectIds.Add(projectId); + // message = $"{message} in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}"; + // if (isCreated) + // { + // await _cache.AddBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); + // } + // else + // { + // await _cache.UpdateBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); + // } + // } + // } + // message = $"{message} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; + // var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = message }; + + // await _signalR.SendNotificationAsync(notification); + // return Ok(ApiResponse.SuccessResponse(responseData, responseMessage, 200)); + // } + // return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Infra Details are not valid.", 400)); + + //} + + #endregion + #region =================================================================== Helper Functions =================================================================== /// @@ -1101,7 +1623,6 @@ namespace Marco.Pms.Services.Service return dbProject; } - // Helper method for background cache update private async Task UpdateCacheInBackground(Project project) { try @@ -1120,6 +1641,28 @@ namespace Marco.Pms.Services.Service } } + private async Task UpdateCacheAndNotify(Dictionary workDelta, List affectedItems) + { + try + { + // Update planned/completed work totals + var cacheUpdateTasks = workDelta.Select(kvp => + _cache.UpdatePlannedAndCompleteWorksInBuilding(kvp.Key, kvp.Value.Planned, kvp.Value.Completed)); + await Task.WhenAll(cacheUpdateTasks); + _logger.LogInfo("Background cache work totals update completed for {AreaCount} areas.", workDelta.Count); + + // Update the details of the individual work items in the cache + await _cache.ManageWorkItemDetails(affectedItems); + _logger.LogInfo("Background cache work item details update completed for {ItemCount} items.", affectedItems.Count); + + // Add SignalR notification logic here if needed + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred during background cache update/notification."); + } + } + #endregion } } diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs index bafa582..2db004d 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs @@ -19,5 +19,9 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task>> ManageAllocationAsync(List projectAllocationDots, Guid tenantId, Employee loggedInEmployee); Task> GetProjectsByEmployeeAsync(Guid employeeId, Guid tenantId, Employee loggedInEmployee); Task>> AssigneProjectsToEmployeeAsync(List projectAllocationDtos, Guid employeeId, Guid tenantId, Employee loggedInEmployee); + Task> GetInfraDetailsAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee); + Task> GetWorkItemsAsync(Guid workAreaId, Guid tenantId, Employee loggedInEmployee); + Task>> CreateProjectTaskAsync(List workItemDtos, Guid tenantId, Employee loggedInEmployee); + } } From eabd31f8cfe529c64d153d7431052f7746f37666 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 16 Jul 2025 18:15:43 +0530 Subject: [PATCH 109/124] Optimized the Manage infra API in Project Controller --- Marco.Pms.CacheHelper/ProjectCache.cs | 7 + .../{BuildingDot.cs => BuildingDto.cs} | 2 +- .../Projects/{FloorDot.cs => FloorDto.cs} | 2 +- Marco.Pms.Model/Dtos/Projects/InfraDot.cs | 9 - Marco.Pms.Model/Dtos/Projects/InfraDto.cs | 9 + .../{WorkAreaDot.cs => WorkAreaDto.cs} | 2 +- Marco.Pms.Model/Mapper/InfraMapper.cs | 6 +- Marco.Pms.Model/Utilities/ServiceResponse.cs | 8 + .../Controllers/ProjectController.cs | 154 +---- .../Helpers/CacheUpdateHelper.cs | 12 + .../MappingProfiles/MappingProfile.cs | 3 + Marco.Pms.Services/Service/ProjectServices.cs | 612 ++++++++++++------ .../ServiceInterfaces/IProjectServices.cs | 1 + 13 files changed, 488 insertions(+), 339 deletions(-) rename Marco.Pms.Model/Dtos/Projects/{BuildingDot.cs => BuildingDto.cs} (92%) rename Marco.Pms.Model/Dtos/Projects/{FloorDot.cs => FloorDto.cs} (92%) delete mode 100644 Marco.Pms.Model/Dtos/Projects/InfraDot.cs create mode 100644 Marco.Pms.Model/Dtos/Projects/InfraDto.cs rename Marco.Pms.Model/Dtos/Projects/{WorkAreaDot.cs => WorkAreaDto.cs} (91%) create mode 100644 Marco.Pms.Model/Utilities/ServiceResponse.cs diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index 833e1a0..9417724 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -95,6 +95,13 @@ namespace Marco.Pms.CacheHelper var result = await _projetCollection.DeleteOneAsync(filter); return result.DeletedCount > 0; } + public async Task RemoveProjectsFromCacheAsync(List projectIds) + { + var stringIds = projectIds.Select(id => id.ToString()).ToList(); + var filter = Builders.Filter.In(p => p.Id, stringIds); + var result = await _projetCollection.DeleteManyAsync(filter); + return result.DeletedCount > 0; + } // ------------------------------------------------------- Project InfraStructure ------------------------------------------------------- diff --git a/Marco.Pms.Model/Dtos/Projects/BuildingDot.cs b/Marco.Pms.Model/Dtos/Projects/BuildingDto.cs similarity index 92% rename from Marco.Pms.Model/Dtos/Projects/BuildingDot.cs rename to Marco.Pms.Model/Dtos/Projects/BuildingDto.cs index a5b160b..e6a7b89 100644 --- a/Marco.Pms.Model/Dtos/Projects/BuildingDot.cs +++ b/Marco.Pms.Model/Dtos/Projects/BuildingDto.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; namespace Marco.Pms.Model.Dtos.Project { - public class BuildingDot + public class BuildingDto { [Key] public Guid? Id { get; set; } diff --git a/Marco.Pms.Model/Dtos/Projects/FloorDot.cs b/Marco.Pms.Model/Dtos/Projects/FloorDto.cs similarity index 92% rename from Marco.Pms.Model/Dtos/Projects/FloorDot.cs rename to Marco.Pms.Model/Dtos/Projects/FloorDto.cs index a3d1c86..3dbe06f 100644 --- a/Marco.Pms.Model/Dtos/Projects/FloorDot.cs +++ b/Marco.Pms.Model/Dtos/Projects/FloorDto.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; namespace Marco.Pms.Model.Dtos.Project { - public class FloorDot + public class FloorDto { public Guid? Id { get; set; } diff --git a/Marco.Pms.Model/Dtos/Projects/InfraDot.cs b/Marco.Pms.Model/Dtos/Projects/InfraDot.cs deleted file mode 100644 index 7c16c09..0000000 --- a/Marco.Pms.Model/Dtos/Projects/InfraDot.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Marco.Pms.Model.Dtos.Project -{ - public class InfraDot - { - public BuildingDot? Building { get; set; } - public FloorDot? Floor { get; set; } - public WorkAreaDot? WorkArea { get; set; } - } -} diff --git a/Marco.Pms.Model/Dtos/Projects/InfraDto.cs b/Marco.Pms.Model/Dtos/Projects/InfraDto.cs new file mode 100644 index 0000000..09d1462 --- /dev/null +++ b/Marco.Pms.Model/Dtos/Projects/InfraDto.cs @@ -0,0 +1,9 @@ +namespace Marco.Pms.Model.Dtos.Project +{ + public class InfraDto + { + public BuildingDto? Building { get; set; } + public FloorDto? Floor { get; set; } + public WorkAreaDto? WorkArea { get; set; } + } +} diff --git a/Marco.Pms.Model/Dtos/Projects/WorkAreaDot.cs b/Marco.Pms.Model/Dtos/Projects/WorkAreaDto.cs similarity index 91% rename from Marco.Pms.Model/Dtos/Projects/WorkAreaDot.cs rename to Marco.Pms.Model/Dtos/Projects/WorkAreaDto.cs index 604ee3e..ffc80c4 100644 --- a/Marco.Pms.Model/Dtos/Projects/WorkAreaDot.cs +++ b/Marco.Pms.Model/Dtos/Projects/WorkAreaDto.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; namespace Marco.Pms.Model.Dtos.Project { - public class WorkAreaDot + public class WorkAreaDto { [Key] public Guid? Id { get; set; } diff --git a/Marco.Pms.Model/Mapper/InfraMapper.cs b/Marco.Pms.Model/Mapper/InfraMapper.cs index 89097d1..5364494 100644 --- a/Marco.Pms.Model/Mapper/InfraMapper.cs +++ b/Marco.Pms.Model/Mapper/InfraMapper.cs @@ -5,7 +5,7 @@ namespace Marco.Pms.Model.Mapper { public static class BuildingMapper { - public static Building ToBuildingFromBuildingDto(this BuildingDot model, Guid tenantId) + public static Building ToBuildingFromBuildingDto(this BuildingDto model, Guid tenantId) { return new Building { @@ -20,7 +20,7 @@ namespace Marco.Pms.Model.Mapper public static class FloorMapper { - public static Floor ToFloorFromFloorDto(this FloorDot model, Guid tenantId) + public static Floor ToFloorFromFloorDto(this FloorDto model, Guid tenantId) { return new Floor { @@ -34,7 +34,7 @@ namespace Marco.Pms.Model.Mapper public static class WorAreaMapper { - public static WorkArea ToWorkAreaFromWorkAreaDto(this WorkAreaDot model, Guid tenantId) + public static WorkArea ToWorkAreaFromWorkAreaDto(this WorkAreaDto model, Guid tenantId) { return new WorkArea { diff --git a/Marco.Pms.Model/Utilities/ServiceResponse.cs b/Marco.Pms.Model/Utilities/ServiceResponse.cs new file mode 100644 index 0000000..a76c45c --- /dev/null +++ b/Marco.Pms.Model/Utilities/ServiceResponse.cs @@ -0,0 +1,8 @@ +namespace Marco.Pms.Model.Utilities +{ + public class ServiceResponse + { + public object? Notification { get; set; } + public ApiResponse Response { get; set; } = ApiResponse.ErrorResponse(""); + } +} diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index a10fc66..71ef1a5 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -1,10 +1,8 @@ using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Employees; -using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; -using Marco.Pms.Model.ViewModels.Projects; using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Service; using Marco.Pms.Services.Service.ServiceInterfaces; @@ -359,6 +357,30 @@ namespace MarcoBMS.Services.Controllers #region =================================================================== Project Infrastructre Manage APIs =================================================================== + [HttpPost("manage-infra")] + public async Task ManageProjectInfra(List infraDtos) + { + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) + { + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); + } + + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var serviceResponse = await _projectServices.ManageProjectInfraAsync(infraDtos, tenantId, loggedInEmployee); + var response = serviceResponse.Response; + var notification = serviceResponse.Notification; + if (notification != null) + { + await _signalR.SendNotificationAsync(notification); + } + return StatusCode(response.StatusCode, response); + + } + [HttpPost("task")] public async Task CreateProjectTask([FromBody] List workItemDtos) { @@ -439,134 +461,6 @@ namespace MarcoBMS.Services.Controllers return Ok(ApiResponse.SuccessResponse(new { }, "Task deleted successfully", 200)); } - [HttpPost("manage-infra")] - public async Task ManageProjectInfra(List infraDots) - { - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - var responseData = new InfraVM { }; - string responseMessage = ""; - string message = ""; - List projectIds = new List(); - if (infraDots != null) - { - foreach (var item in infraDots) - { - if (item.Building != null) - { - - Building building = item.Building.ToBuildingFromBuildingDto(tenantId); - building.TenantId = tenantId; - - if (item.Building.Id == null) - { - //create - _context.Buildings.Add(building); - await _context.SaveChangesAsync(); - responseData.building = building; - responseMessage = "Buliding Added Successfully"; - message = "Building Added"; - await _cache.AddBuildngInfra(building.ProjectId, building); - } - else - { - //update - _context.Buildings.Update(building); - await _context.SaveChangesAsync(); - responseData.building = building; - responseMessage = "Buliding Updated Successfully"; - message = "Building Updated"; - await _cache.UpdateBuildngInfra(building.ProjectId, building); - } - projectIds.Add(building.ProjectId); - } - if (item.Floor != null) - { - Floor floor = item.Floor.ToFloorFromFloorDto(tenantId); - floor.TenantId = tenantId; - bool isCreated = false; - - if (item.Floor.Id == null) - { - //create - _context.Floor.Add(floor); - await _context.SaveChangesAsync(); - responseData.floor = floor; - responseMessage = "Floor Added Successfully"; - message = "Floor Added"; - isCreated = true; - } - else - { - //update - _context.Floor.Update(floor); - await _context.SaveChangesAsync(); - responseData.floor = floor; - responseMessage = "Floor Updated Successfully"; - message = "Floor Updated"; - } - Building? building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == floor.BuildingId); - var projectId = building?.ProjectId ?? Guid.Empty; - projectIds.Add(projectId); - message = $"{message} in Building: {building?.Name}"; - if (isCreated) - { - await _cache.AddBuildngInfra(projectId, floor: floor); - } - else - { - await _cache.UpdateBuildngInfra(projectId, floor: floor); - } - } - if (item.WorkArea != null) - { - WorkArea workArea = item.WorkArea.ToWorkAreaFromWorkAreaDto(tenantId); - workArea.TenantId = tenantId; - bool isCreated = false; - - if (item.WorkArea.Id == null) - { - //create - _context.WorkAreas.Add(workArea); - await _context.SaveChangesAsync(); - responseData.workArea = workArea; - responseMessage = "Work Area Added Successfully"; - message = "Work Area Added"; - isCreated = true; - } - else - { - //update - _context.WorkAreas.Update(workArea); - await _context.SaveChangesAsync(); - responseData.workArea = workArea; - responseMessage = "Work Area Updated Successfully"; - message = "Work Area Updated"; - } - Floor? floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == workArea.FloorId); - var projectId = floor?.Building?.ProjectId ?? Guid.Empty; - projectIds.Add(projectId); - message = $"{message} in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}"; - if (isCreated) - { - await _cache.AddBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); - } - else - { - await _cache.UpdateBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); - } - } - } - message = $"{message} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}"; - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = message }; - - await _signalR.SendNotificationAsync(notification); - return Ok(ApiResponse.SuccessResponse(responseData, responseMessage, 200)); - } - return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Infra Details are not valid.", 400)); - - } - #endregion } diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 9a01b83..b0b1e06 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -478,6 +478,18 @@ namespace Marco.Pms.Services.Helpers } } + public async Task RemoveProjectsAsync(List projectIds) + { + try + { + var response = await _projectCache.RemoveProjectsFromCacheAsync(projectIds); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occured while deleting project list from to Cache"); + + } + } // ------------------------------------ Project Infrastructure Cache --------------------------------------- diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index 50d2ea9..bf3777c 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -51,6 +51,9 @@ namespace Marco.Pms.Services.MappingProfiles CreateMap(); CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); CreateMap() .ForMember( dest => dest.Description, diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs index 6d811fc..32e1285 100644 --- a/Marco.Pms.Services/Service/ProjectServices.cs +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -1033,83 +1033,360 @@ namespace Marco.Pms.Services.Service #region =================================================================== Project Infrastructre Manage APIs =================================================================== - public async Task> CreateProjectTask1(List workItemDtos, Guid tenantId, Employee loggedInEmployee) + public async Task> ManageProjectInfra(List infraDots, Guid tenantId, Employee loggedInEmployee) { - _logger.LogInfo("CreateProjectTask called with {Count} items", workItemDtos?.Count ?? 0); - - // Validate request - if (workItemDtos == null || !workItemDtos.Any()) - { - _logger.LogWarning("No work items provided in the request."); - return ApiResponse.ErrorResponse("Invalid details.", "Work Item details are not valid.", 400); - } - - var workItemsToCreate = new List(); - var workItemsToUpdate = new List(); - var responseList = new List(); + var responseData = new InfraVM { }; + string responseMessage = ""; string message = ""; - List workAreaIds = new List(); - var workItemIds = workItemDtos.Where(wi => wi.Id != null && wi.Id != Guid.Empty).Select(wi => wi.Id).ToList(); - var workItems = await _context.WorkItems.AsNoTracking().Where(wi => workItemIds.Contains(wi.Id)).ToListAsync(); - - foreach (var itemDto in workItemDtos) + List projectIds = new List(); + if (infraDots != null) { - var workItem = _mapper.Map(itemDto); - workItem.TenantId = tenantId; - var workArea = await _context.WorkAreas.Include(a => a.Floor).FirstOrDefaultAsync(a => a.Id == workItem.WorkAreaId) ?? new WorkArea(); - - Building building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == (workArea.Floor != null ? workArea.Floor.BuildingId : Guid.Empty)) ?? new Building(); - - if (itemDto.Id != null && itemDto.Id != Guid.Empty) + foreach (var item in infraDots) { - // Update existing - workItemsToUpdate.Add(workItem); - message = $"Task Updated in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; - var existingWorkItem = workItems.FirstOrDefault(wi => wi.Id == workItem.Id); - if (existingWorkItem != null) + if (item.Building != null) { - double plannedWork = workItem.PlannedWork - existingWorkItem.PlannedWork; - double completedWork = workItem.CompletedWork - existingWorkItem.CompletedWork; - await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, plannedWork, completedWork); + + Building building = _mapper.Map(item.Building); + building.TenantId = tenantId; + + if (item.Building.Id == null) + { + //create + _context.Buildings.Add(building); + await _context.SaveChangesAsync(); + responseData.building = building; + responseMessage = "Buliding Added Successfully"; + message = "Building Added"; + await _cache.AddBuildngInfra(building.ProjectId, building); + } + else + { + //update + _context.Buildings.Update(building); + await _context.SaveChangesAsync(); + responseData.building = building; + responseMessage = "Buliding Updated Successfully"; + message = "Building Updated"; + await _cache.UpdateBuildngInfra(building.ProjectId, building); + } + projectIds.Add(building.ProjectId); + } + if (item.Floor != null) + { + Floor floor = _mapper.Map(item.Floor); + floor.TenantId = tenantId; + bool isCreated = false; + + if (item.Floor.Id == null) + { + //create + _context.Floor.Add(floor); + await _context.SaveChangesAsync(); + responseData.floor = floor; + responseMessage = "Floor Added Successfully"; + message = "Floor Added"; + isCreated = true; + } + else + { + //update + _context.Floor.Update(floor); + await _context.SaveChangesAsync(); + responseData.floor = floor; + responseMessage = "Floor Updated Successfully"; + message = "Floor Updated"; + } + Building? building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == floor.BuildingId); + var projectId = building?.ProjectId ?? Guid.Empty; + projectIds.Add(projectId); + message = $"{message} in Building: {building?.Name}"; + if (isCreated) + { + await _cache.AddBuildngInfra(projectId, floor: floor); + } + else + { + await _cache.UpdateBuildngInfra(projectId, floor: floor); + } + } + if (item.WorkArea != null) + { + WorkArea workArea = _mapper.Map(item.WorkArea); + workArea.TenantId = tenantId; + bool isCreated = false; + + if (item.WorkArea.Id == null) + { + //create + _context.WorkAreas.Add(workArea); + await _context.SaveChangesAsync(); + responseData.workArea = workArea; + responseMessage = "Work Area Added Successfully"; + message = "Work Area Added"; + isCreated = true; + } + else + { + //update + _context.WorkAreas.Update(workArea); + await _context.SaveChangesAsync(); + responseData.workArea = workArea; + responseMessage = "Work Area Updated Successfully"; + message = "Work Area Updated"; + } + Floor? floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == workArea.FloorId); + var projectId = floor?.Building?.ProjectId ?? Guid.Empty; + projectIds.Add(projectId); + message = $"{message} in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}"; + if (isCreated) + { + await _cache.AddBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); + } + else + { + await _cache.UpdateBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); + } } } - else + message = $"{message} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = message }; + + return ApiResponse.SuccessResponse(responseData, responseMessage, 200); + } + return ApiResponse.ErrorResponse("Invalid details.", "Infra Details are not valid.", 400); + + } + + public async Task ManageProjectInfraAsync(List infraDtos, Guid tenantId, Employee loggedInEmployee) + { + // 1. Guard Clause: Handle null or empty input gracefully. + if (infraDtos == null || !infraDtos.Any()) + { + return new ServiceResponse { - // Create new - workItem.Id = Guid.NewGuid(); - workItemsToCreate.Add(workItem); - message = $"Task Added in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; - await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, workItem.PlannedWork, workItem.CompletedWork); + Response = ApiResponse.ErrorResponse("Invalid details.", "No infrastructure details were provided.", 400) + }; + } + + var responseData = new InfraVM(); + var messages = new List(); + var projectIds = new HashSet(); // Use HashSet for automatic duplicate handling. + var cacheUpdateTasks = new List(); + + // --- Pre-fetch parent entities to avoid N+1 query problem --- + // 2. Gather all parent IDs needed for validation and context. + var requiredBuildingIds = infraDtos + .Where(i => i.Floor?.BuildingId != null) + .Select(i => i.Floor!.BuildingId) + .Distinct() + .ToList(); + + var requiredFloorIds = infraDtos + .Where(i => i.WorkArea?.FloorId != null) + .Select(i => i.WorkArea!.FloorId) + .Distinct() + .ToList(); + + // 3. Fetch all required parent entities in single batch queries. + var buildingsDict = await _context.Buildings + .Where(b => requiredBuildingIds.Contains(b.Id)) + .ToDictionaryAsync(b => b.Id); + + var floorsDict = await _context.Floor + .Include(f => f.Building) // Eagerly load Building for later use + .Where(f => requiredFloorIds.Contains(f.Id)) + .ToDictionaryAsync(f => f.Id); + // --- End Pre-fetching --- + + // 4. Process all entities and add them to the context's change tracker. + foreach (var item in infraDtos) + { + if (item.Building != null) + { + ProcessBuilding(item.Building, tenantId, responseData, messages, projectIds, cacheUpdateTasks); + } + if (item.Floor != null) + { + ProcessFloor(item.Floor, tenantId, responseData, messages, projectIds, cacheUpdateTasks, buildingsDict); + } + if (item.WorkArea != null) + { + ProcessWorkArea(item.WorkArea, tenantId, responseData, messages, projectIds, cacheUpdateTasks, floorsDict); + } + } + + // 5. Save all changes to the database in a single transaction. + var changedRecordCount = await _context.SaveChangesAsync(); + + // If no changes were actually made, we can exit early. + if (changedRecordCount == 0) + { + return new ServiceResponse + { + Response = ApiResponse.SuccessResponse(responseData, "No changes detected in the provided infrastructure details.", 200) + }; + } + + // 6. Execute all cache updates concurrently after the DB save is successful. + await Task.WhenAll(cacheUpdateTasks); + + // 7. Consolidate messages and create notification payload. + string finalResponseMessage = messages.LastOrDefault() ?? "Infrastructure managed successfully."; + string logMessage = $"{string.Join(", ", messages)} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds.ToList(), Message = logMessage }; + + // TODO: Dispatch the 'notification' object to your notification service. + + return new ServiceResponse + { + Notification = notification, + Response = ApiResponse.SuccessResponse(responseData, finalResponseMessage, 200) + }; + } + + /// + /// Manages a batch of infrastructure changes (creates/updates for Buildings, Floors, and WorkAreas). + /// This method is optimized to perform all database operations in a single, atomic transaction. + /// + public async Task> ManageProjectInfraAsync1(List infraDtos, Guid tenantId, Employee loggedInEmployee) + { + // --- Step 1: Input Validation --- + if (infraDtos == null || !infraDtos.Any()) + { + _logger.LogWarning("ManageProjectInfraAsync called with null or empty DTO list."); + return ApiResponse.ErrorResponse("Invalid details.", "Infrastructure data cannot be empty.", 400); + } + + _logger.LogInfo("Begin ManageProjectInfraAsync for {DtoCount} items, TenantId: {TenantId}, User: {UserId}", infraDtos.Count, tenantId, loggedInEmployee.Id); + + // --- Step 2: Categorize DTOs by Type and Action --- + var buildingsToCreateDto = infraDtos.Where(i => i.Building != null && i.Building.Id == null).Select(i => i.Building!).ToList(); + var buildingsToUpdateDto = infraDtos.Where(i => i.Building != null && i.Building.Id != null).Select(i => i.Building!).ToList(); + var floorsToCreateDto = infraDtos.Where(i => i.Floor != null && i.Floor.Id == null).Select(i => i.Floor!).ToList(); + var floorsToUpdateDto = infraDtos.Where(i => i.Floor != null && i.Floor.Id != null).Select(i => i.Floor!).ToList(); + var workAreasToCreateDto = infraDtos.Where(i => i.WorkArea != null && i.WorkArea.Id == null).Select(i => i.WorkArea!).ToList(); + var workAreasToUpdateDto = infraDtos.Where(i => i.WorkArea != null && i.WorkArea.Id != null).Select(i => i.WorkArea!).ToList(); + + _logger.LogDebug("Categorized DTOs..."); + + try + { + // --- Step 3: Fetch all required existing data in bulk --- + + // Fetch existing entities to be updated + var buildingIdsToUpdate = buildingsToUpdateDto.Select(d => d.Id!.Value).ToList(); + var existingBuildings = await _context.Buildings.Where(b => buildingIdsToUpdate.Contains(b.Id) && b.TenantId == tenantId).ToDictionaryAsync(b => b.Id); + + var floorIdsToUpdate = floorsToUpdateDto.Select(d => d.Id!.Value).ToList(); + var existingFloors = await _context.Floor.Include(f => f.Building).Where(f => floorIdsToUpdate.Contains(f.Id) && f.TenantId == tenantId).ToDictionaryAsync(f => f.Id); + + var workAreaIdsToUpdate = workAreasToUpdateDto.Select(d => d.Id!.Value).ToList(); + var existingWorkAreas = await _context.WorkAreas.Include(wa => wa.Floor!.Building).Where(wa => workAreaIdsToUpdate.Contains(wa.Id) && wa.TenantId == tenantId).ToDictionaryAsync(wa => wa.Id); + + // Fetch parent entities for items being created to get their ProjectIds + var buildingIdsForNewFloors = floorsToCreateDto.Select(f => f.BuildingId).ToList(); + var parentBuildingsForNewFloors = await _context.Buildings.Where(b => buildingIdsForNewFloors.Contains(b.Id)).ToDictionaryAsync(b => b.Id); + + var floorIdsForNewWorkAreas = workAreasToCreateDto.Select(wa => wa.FloorId).ToList(); + var parentFloorsForNewWorkAreas = await _context.Floor.Include(f => f.Building).Where(f => floorIdsForNewWorkAreas.Contains(f.Id)).ToDictionaryAsync(f => f.Id); + + _logger.LogInfo("Fetched existing entities and parents for new items."); + + // --- Step 4: Aggregate all affected ProjectIds for Security Check --- + var affectedProjectIds = new HashSet(); + + // From buildings being created/updated + buildingsToCreateDto.ForEach(b => affectedProjectIds.Add(b.ProjectId)); + foreach (var b in existingBuildings.Values) { affectedProjectIds.Add(b.ProjectId); } + + // From floors being created/updated + foreach (var f in floorsToCreateDto) { if (parentBuildingsForNewFloors.TryGetValue(f.BuildingId, out var b)) affectedProjectIds.Add(b.ProjectId); } + foreach (var f in existingFloors.Values) { if (f.Building != null) affectedProjectIds.Add(f.Building.ProjectId); } + + // From work areas being created/updated + foreach (var wa in workAreasToCreateDto) { if (parentFloorsForNewWorkAreas.TryGetValue(wa.FloorId, out var f) && f.Building != null) affectedProjectIds.Add(f.Building.ProjectId); } + foreach (var wa in existingWorkAreas.Values) { if (wa.Floor?.Building != null) affectedProjectIds.Add(wa.Floor.Building.ProjectId); } + + // Security Check against the complete list of affected projects + var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageProjectInfra, loggedInEmployee.Id); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} trying to manage infrastructure for projects.", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to manage infrastructure for one or more of the specified projects.", 403); } - responseList.Add(new WorkItemVM + // --- Step 5: Process all logic IN MEMORY, tracking changes --- + + // Process Buildings + var createdBuildings = new List(); + foreach (var dto in buildingsToCreateDto) { - WorkItemId = workItem.Id, - WorkItem = workItem - }); - workAreaIds.Add(workItem.WorkAreaId); + var newBuilding = _mapper.Map(dto); + newBuilding.TenantId = tenantId; + createdBuildings.Add(newBuilding); + } + foreach (var dto in buildingsToUpdateDto) { if (existingBuildings.TryGetValue(dto.Id!.Value, out var b)) _mapper.Map(dto, b); } + // Process Floors + var createdFloors = new List(); + foreach (var dto in floorsToCreateDto) + { + var newFloor = _mapper.Map(dto); + newFloor.TenantId = tenantId; + createdFloors.Add(newFloor); + } + foreach (var dto in floorsToUpdateDto) { if (existingFloors.TryGetValue(dto.Id!.Value, out var f)) _mapper.Map(dto, f); } + + // Process WorkAreas + var createdWorkAreas = new List(); + foreach (var dto in workAreasToCreateDto) + { + var newWorkArea = _mapper.Map(dto); + newWorkArea.TenantId = tenantId; + createdWorkAreas.Add(newWorkArea); + } + foreach (var dto in workAreasToUpdateDto) { if (existingWorkAreas.TryGetValue(dto.Id!.Value, out var wa)) _mapper.Map(dto, wa); } + + // --- Step 6: Save all database changes in a SINGLE TRANSACTION --- + if (createdBuildings.Any()) _context.Buildings.AddRange(createdBuildings); + if (createdFloors.Any()) _context.Floor.AddRange(createdFloors); + if (createdWorkAreas.Any()) _context.WorkAreas.AddRange(createdWorkAreas); + + if (_context.ChangeTracker.HasChanges()) + { + await _context.SaveChangesAsync(); + _logger.LogInfo("Database save successful."); + } + + // --- Step 7: Update Cache using the aggregated ProjectIds (Non-blocking) --- + var finalProjectIds = affectedProjectIds.ToList(); + if (finalProjectIds.Any()) + { + _ = Task.Run(async () => + { + try + { + _logger.LogInfo("Queuing background cache update for {ProjectCount} projects.", finalProjectIds.Count); + // Assuming your cache service has a method to handle this. + await _cache.RemoveProjectsAsync(finalProjectIds); + _logger.LogInfo("Background cache update task completed for projects: {ProjectIds}", string.Join(", ", finalProjectIds)); + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred during the background cache update task for projects: {ProjectIds}", string.Join(", ", finalProjectIds)); + } + }); + } + + // --- Step 8: Prepare and return a clear response --- + var responseVm = new { /* ... as before ... */ }; + return ApiResponse.SuccessResponse(responseVm, "Infrastructure changes processed successfully.", 200); } - // Apply DB changes - if (workItemsToCreate.Any()) + catch (Exception ex) { - _logger.LogInfo("Adding {Count} new work items", workItemsToCreate.Count); - await _context.WorkItems.AddRangeAsync(workItemsToCreate); - await _cache.ManageWorkItemDetails(workItemsToCreate); + _logger.LogError(ex, "An unexpected error occurred in ManageProjectInfraAsync."); + return ApiResponse.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500); } - - if (workItemsToUpdate.Any()) - { - _logger.LogInfo("Updating {Count} existing work items", workItemsToUpdate.Count); - _context.WorkItems.UpdateRange(workItemsToUpdate); - await _cache.ManageWorkItemDetails(workItemsToUpdate); - } - - await _context.SaveChangesAsync(); - - _logger.LogInfo("CreateProjectTask completed successfully. Created: {Created}, Updated: {Updated}", workItemsToCreate.Count, workItemsToUpdate.Count); - - return ApiResponse.SuccessResponse(responseList, message, 200); } /// @@ -1211,12 +1488,10 @@ namespace Marco.Pms.Services.Service await _context.SaveChangesAsync(); _logger.LogInfo("Successfully saved {CreatedCount} new and {UpdatedCount} updated work items.", workItemsToCreate.Count, workItemsToModify.Count); - // --- Step 5: Update Cache and SignalR AFTER successful DB save (non-blocking) --- + // --- Step 5: Update Cache and SignalR AFTER successful DB save --- var allAffectedItems = workItemsToCreate.Concat(workItemsToModify).ToList(); - _ = Task.Run(async () => - { - await UpdateCacheAndNotify(workDeltaForCache, allAffectedItems); - }); + + await UpdateCacheAndNotify(workDeltaForCache, allAffectedItems); } } catch (DbUpdateException ex) @@ -1291,133 +1566,6 @@ namespace Marco.Pms.Services.Service // return Ok(ApiResponse.SuccessResponse(new { }, "Task deleted successfully", 200)); //} - //public async Task ManageProjectInfra(List infraDots) - //{ - // var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - // var responseData = new InfraVM { }; - // string responseMessage = ""; - // string message = ""; - // List projectIds = new List(); - // if (infraDots != null) - // { - // foreach (var item in infraDots) - // { - // if (item.Building != null) - // { - - // Building building = item.Building.ToBuildingFromBuildingDto(tenantId); - // building.TenantId = tenantId; - - // if (item.Building.Id == null) - // { - // //create - // _context.Buildings.Add(building); - // await _context.SaveChangesAsync(); - // responseData.building = building; - // responseMessage = "Buliding Added Successfully"; - // message = "Building Added"; - // await _cache.AddBuildngInfra(building.ProjectId, building); - // } - // else - // { - // //update - // _context.Buildings.Update(building); - // await _context.SaveChangesAsync(); - // responseData.building = building; - // responseMessage = "Buliding Updated Successfully"; - // message = "Building Updated"; - // await _cache.UpdateBuildngInfra(building.ProjectId, building); - // } - // projectIds.Add(building.ProjectId); - // } - // if (item.Floor != null) - // { - // Floor floor = item.Floor.ToFloorFromFloorDto(tenantId); - // floor.TenantId = tenantId; - // bool isCreated = false; - - // if (item.Floor.Id == null) - // { - // //create - // _context.Floor.Add(floor); - // await _context.SaveChangesAsync(); - // responseData.floor = floor; - // responseMessage = "Floor Added Successfully"; - // message = "Floor Added"; - // isCreated = true; - // } - // else - // { - // //update - // _context.Floor.Update(floor); - // await _context.SaveChangesAsync(); - // responseData.floor = floor; - // responseMessage = "Floor Updated Successfully"; - // message = "Floor Updated"; - // } - // Building? building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == floor.BuildingId); - // var projectId = building?.ProjectId ?? Guid.Empty; - // projectIds.Add(projectId); - // message = $"{message} in Building: {building?.Name}"; - // if (isCreated) - // { - // await _cache.AddBuildngInfra(projectId, floor: floor); - // } - // else - // { - // await _cache.UpdateBuildngInfra(projectId, floor: floor); - // } - // } - // if (item.WorkArea != null) - // { - // WorkArea workArea = item.WorkArea.ToWorkAreaFromWorkAreaDto(tenantId); - // workArea.TenantId = tenantId; - // bool isCreated = false; - - // if (item.WorkArea.Id == null) - // { - // //create - // _context.WorkAreas.Add(workArea); - // await _context.SaveChangesAsync(); - // responseData.workArea = workArea; - // responseMessage = "Work Area Added Successfully"; - // message = "Work Area Added"; - // isCreated = true; - // } - // else - // { - // //update - // _context.WorkAreas.Update(workArea); - // await _context.SaveChangesAsync(); - // responseData.workArea = workArea; - // responseMessage = "Work Area Updated Successfully"; - // message = "Work Area Updated"; - // } - // Floor? floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == workArea.FloorId); - // var projectId = floor?.Building?.ProjectId ?? Guid.Empty; - // projectIds.Add(projectId); - // message = $"{message} in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}"; - // if (isCreated) - // { - // await _cache.AddBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); - // } - // else - // { - // await _cache.UpdateBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); - // } - // } - // } - // message = $"{message} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; - // var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = message }; - - // await _signalR.SendNotificationAsync(notification); - // return Ok(ApiResponse.SuccessResponse(responseData, responseMessage, 200)); - // } - // return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Infra Details are not valid.", 400)); - - //} - #endregion #region =================================================================== Helper Functions =================================================================== @@ -1663,6 +1811,82 @@ namespace Marco.Pms.Services.Service } } + private void ProcessBuilding(BuildingDto dto, Guid tenantId, InfraVM responseData, List messages, ISet projectIds, List cacheTasks) + { + Building building = _mapper.Map(dto); + building.TenantId = tenantId; + + bool isNew = dto.Id == null; + if (isNew) + { + _context.Buildings.Add(building); + messages.Add("Building Added"); + cacheTasks.Add(_cache.AddBuildngInfra(building.ProjectId, building)); + } + else + { + _context.Buildings.Update(building); + messages.Add("Building Updated"); + cacheTasks.Add(_cache.UpdateBuildngInfra(building.ProjectId, building)); + } + + responseData.building = building; + projectIds.Add(building.ProjectId); + } + + private void ProcessFloor(FloorDto dto, Guid tenantId, InfraVM responseData, List messages, ISet projectIds, List cacheTasks, IDictionary buildings) + { + Floor floor = _mapper.Map(dto); + floor.TenantId = tenantId; + + // Use the pre-fetched dictionary for parent lookup. + Building? parentBuilding = buildings.TryGetValue(dto.BuildingId, out var b) ? b : null; + + bool isNew = dto.Id == null; + if (isNew) + { + _context.Floor.Add(floor); + messages.Add($"Floor Added in Building: {parentBuilding?.Name ?? "Unknown"}"); + cacheTasks.Add(_cache.AddBuildngInfra(parentBuilding?.ProjectId ?? Guid.Empty, floor: floor)); + } + else + { + _context.Floor.Update(floor); + messages.Add($"Floor Updated in Building: {parentBuilding?.Name ?? "Unknown"}"); + cacheTasks.Add(_cache.UpdateBuildngInfra(parentBuilding?.ProjectId ?? Guid.Empty, floor: floor)); + } + + responseData.floor = floor; + if (parentBuilding != null) projectIds.Add(parentBuilding.ProjectId); + } + + private void ProcessWorkArea(WorkAreaDto dto, Guid tenantId, InfraVM responseData, List messages, ISet projectIds, List cacheTasks, IDictionary floors) + { + WorkArea workArea = _mapper.Map(dto); + workArea.TenantId = tenantId; + + // Use the pre-fetched dictionary for parent lookup. + Floor? parentFloor = floors.TryGetValue(dto.FloorId, out var f) ? f : null; + var parentBuilding = parentFloor?.Building; + + bool isNew = dto.Id == null; + if (isNew) + { + _context.WorkAreas.Add(workArea); + messages.Add($"Work Area Added in Building: {parentBuilding?.Name ?? "Unknown"}, on Floor: {parentFloor?.FloorName ?? "Unknown"}"); + cacheTasks.Add(_cache.AddBuildngInfra(parentBuilding?.ProjectId ?? Guid.Empty, workArea: workArea, buildingId: parentBuilding?.Id)); + } + else + { + _context.WorkAreas.Update(workArea); + messages.Add($"Work Area Updated in Building: {parentBuilding?.Name ?? "Unknown"}, on Floor: {parentFloor?.FloorName ?? "Unknown"}"); + cacheTasks.Add(_cache.UpdateBuildngInfra(parentBuilding?.ProjectId ?? Guid.Empty, workArea: workArea, buildingId: parentBuilding?.Id)); + } + + responseData.workArea = workArea; + if (parentBuilding != null) projectIds.Add(parentBuilding.ProjectId); + } + #endregion } } diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs index 2db004d..f1c89cc 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs @@ -21,6 +21,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task>> AssigneProjectsToEmployeeAsync(List projectAllocationDtos, Guid employeeId, Guid tenantId, Employee loggedInEmployee); Task> GetInfraDetailsAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee); Task> GetWorkItemsAsync(Guid workAreaId, Guid tenantId, Employee loggedInEmployee); + Task ManageProjectInfraAsync(List infraDtos, Guid tenantId, Employee loggedInEmployee); Task>> CreateProjectTaskAsync(List workItemDtos, Guid tenantId, Employee loggedInEmployee); } From c8ca2d5c49da430880c9732b56441ff66cd1132c Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 16 Jul 2025 18:39:29 +0530 Subject: [PATCH 110/124] Optimization of WorkItem Delete API in Project Controller --- .../Controllers/ProjectController.cs | 61 +-- Marco.Pms.Services/Service/ProjectServices.cs | 391 ++++-------------- .../ServiceInterfaces/IProjectServices.cs | 1 + 3 files changed, 90 insertions(+), 363 deletions(-) diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 71ef1a5..362c2af 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -1,7 +1,6 @@ using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Employees; -using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Service; @@ -11,7 +10,6 @@ using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.CodeAnalysis; -using Microsoft.EntityFrameworkCore; using MongoDB.Driver; namespace MarcoBMS.Services.Controllers @@ -410,55 +408,24 @@ namespace MarcoBMS.Services.Controllers [HttpDelete("task/{id}")] public async Task DeleteProjectTask(Guid id) { - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - List workAreaIds = new List(); - WorkItem? task = await _context.WorkItems.AsNoTracking().Include(t => t.WorkArea).FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId); - if (task != null) + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) { - if (task.CompletedWork == 0) - { - var assignedTask = await _context.TaskAllocations.Where(t => t.WorkItemId == id).ToListAsync(); - if (assignedTask.Count == 0) - { - _context.WorkItems.Remove(task); - await _context.SaveChangesAsync(); - _logger.LogInfo("Task with ID {WorkItemId} has been successfully deleted.", task.Id); - - var floorId = task.WorkArea?.FloorId; - var floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == floorId); - - - workAreaIds.Add(task.WorkAreaId); - var projectId = floor?.Building?.ProjectId; - - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = $"Task Deleted in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}, in Area: {task.WorkArea?.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}" }; - await _signalR.SendNotificationAsync(notification); - await _cache.DeleteWorkItemByIdAsync(task.Id); - if (projectId != null) - { - await _cache.DeleteProjectByIdAsync(projectId.Value); - } - } - else - { - _logger.LogWarning("Task with ID {WorkItemId} is currently assigned and cannot be deleted.", task.Id); - return BadRequest(ApiResponse.ErrorResponse("Task is currently assigned and cannot be deleted.", "Task is currently assigned and cannot be deleted.", 400)); - } - } - else - { - double percentage = (task.CompletedWork / task.PlannedWork) * 100; - percentage = Math.Round(percentage, 2); - _logger.LogWarning("Task with ID {WorkItemId} is {CompletionPercentage}% complete and cannot be deleted", task.Id, percentage); - return BadRequest(ApiResponse.ErrorResponse(System.String.Format("Task is {0}% complete and cannot be deleted", percentage), System.String.Format("Task is {0}% complete and cannot be deleted", percentage), 400)); - - } + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - else + + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var serviceResponse = await _projectServices.DeleteProjectTaskAsync(id, tenantId, loggedInEmployee); + var response = serviceResponse.Response; + var notification = serviceResponse.Notification; + if (notification != null) { - _logger.LogWarning("Task with ID {WorkItemId} not found ID database", id); + await _signalR.SendNotificationAsync(notification); } - return Ok(ApiResponse.SuccessResponse(new { }, "Task deleted successfully", 200)); + return StatusCode(response.StatusCode, response); } #endregion diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs index 32e1285..d7ab2ac 100644 --- a/Marco.Pms.Services/Service/ProjectServices.cs +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -1033,130 +1033,6 @@ namespace Marco.Pms.Services.Service #region =================================================================== Project Infrastructre Manage APIs =================================================================== - public async Task> ManageProjectInfra(List infraDots, Guid tenantId, Employee loggedInEmployee) - { - var responseData = new InfraVM { }; - string responseMessage = ""; - string message = ""; - List projectIds = new List(); - if (infraDots != null) - { - foreach (var item in infraDots) - { - if (item.Building != null) - { - - Building building = _mapper.Map(item.Building); - building.TenantId = tenantId; - - if (item.Building.Id == null) - { - //create - _context.Buildings.Add(building); - await _context.SaveChangesAsync(); - responseData.building = building; - responseMessage = "Buliding Added Successfully"; - message = "Building Added"; - await _cache.AddBuildngInfra(building.ProjectId, building); - } - else - { - //update - _context.Buildings.Update(building); - await _context.SaveChangesAsync(); - responseData.building = building; - responseMessage = "Buliding Updated Successfully"; - message = "Building Updated"; - await _cache.UpdateBuildngInfra(building.ProjectId, building); - } - projectIds.Add(building.ProjectId); - } - if (item.Floor != null) - { - Floor floor = _mapper.Map(item.Floor); - floor.TenantId = tenantId; - bool isCreated = false; - - if (item.Floor.Id == null) - { - //create - _context.Floor.Add(floor); - await _context.SaveChangesAsync(); - responseData.floor = floor; - responseMessage = "Floor Added Successfully"; - message = "Floor Added"; - isCreated = true; - } - else - { - //update - _context.Floor.Update(floor); - await _context.SaveChangesAsync(); - responseData.floor = floor; - responseMessage = "Floor Updated Successfully"; - message = "Floor Updated"; - } - Building? building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == floor.BuildingId); - var projectId = building?.ProjectId ?? Guid.Empty; - projectIds.Add(projectId); - message = $"{message} in Building: {building?.Name}"; - if (isCreated) - { - await _cache.AddBuildngInfra(projectId, floor: floor); - } - else - { - await _cache.UpdateBuildngInfra(projectId, floor: floor); - } - } - if (item.WorkArea != null) - { - WorkArea workArea = _mapper.Map(item.WorkArea); - workArea.TenantId = tenantId; - bool isCreated = false; - - if (item.WorkArea.Id == null) - { - //create - _context.WorkAreas.Add(workArea); - await _context.SaveChangesAsync(); - responseData.workArea = workArea; - responseMessage = "Work Area Added Successfully"; - message = "Work Area Added"; - isCreated = true; - } - else - { - //update - _context.WorkAreas.Update(workArea); - await _context.SaveChangesAsync(); - responseData.workArea = workArea; - responseMessage = "Work Area Updated Successfully"; - message = "Work Area Updated"; - } - Floor? floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == workArea.FloorId); - var projectId = floor?.Building?.ProjectId ?? Guid.Empty; - projectIds.Add(projectId); - message = $"{message} in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}"; - if (isCreated) - { - await _cache.AddBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); - } - else - { - await _cache.UpdateBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); - } - } - } - message = $"{message} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; - var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = message }; - - return ApiResponse.SuccessResponse(responseData, responseMessage, 200); - } - return ApiResponse.ErrorResponse("Invalid details.", "Infra Details are not valid.", 400); - - } - public async Task ManageProjectInfraAsync(List infraDtos, Guid tenantId, Employee loggedInEmployee) { // 1. Guard Clause: Handle null or empty input gracefully. @@ -1244,151 +1120,6 @@ namespace Marco.Pms.Services.Service }; } - /// - /// Manages a batch of infrastructure changes (creates/updates for Buildings, Floors, and WorkAreas). - /// This method is optimized to perform all database operations in a single, atomic transaction. - /// - public async Task> ManageProjectInfraAsync1(List infraDtos, Guid tenantId, Employee loggedInEmployee) - { - // --- Step 1: Input Validation --- - if (infraDtos == null || !infraDtos.Any()) - { - _logger.LogWarning("ManageProjectInfraAsync called with null or empty DTO list."); - return ApiResponse.ErrorResponse("Invalid details.", "Infrastructure data cannot be empty.", 400); - } - - _logger.LogInfo("Begin ManageProjectInfraAsync for {DtoCount} items, TenantId: {TenantId}, User: {UserId}", infraDtos.Count, tenantId, loggedInEmployee.Id); - - // --- Step 2: Categorize DTOs by Type and Action --- - var buildingsToCreateDto = infraDtos.Where(i => i.Building != null && i.Building.Id == null).Select(i => i.Building!).ToList(); - var buildingsToUpdateDto = infraDtos.Where(i => i.Building != null && i.Building.Id != null).Select(i => i.Building!).ToList(); - var floorsToCreateDto = infraDtos.Where(i => i.Floor != null && i.Floor.Id == null).Select(i => i.Floor!).ToList(); - var floorsToUpdateDto = infraDtos.Where(i => i.Floor != null && i.Floor.Id != null).Select(i => i.Floor!).ToList(); - var workAreasToCreateDto = infraDtos.Where(i => i.WorkArea != null && i.WorkArea.Id == null).Select(i => i.WorkArea!).ToList(); - var workAreasToUpdateDto = infraDtos.Where(i => i.WorkArea != null && i.WorkArea.Id != null).Select(i => i.WorkArea!).ToList(); - - _logger.LogDebug("Categorized DTOs..."); - - try - { - // --- Step 3: Fetch all required existing data in bulk --- - - // Fetch existing entities to be updated - var buildingIdsToUpdate = buildingsToUpdateDto.Select(d => d.Id!.Value).ToList(); - var existingBuildings = await _context.Buildings.Where(b => buildingIdsToUpdate.Contains(b.Id) && b.TenantId == tenantId).ToDictionaryAsync(b => b.Id); - - var floorIdsToUpdate = floorsToUpdateDto.Select(d => d.Id!.Value).ToList(); - var existingFloors = await _context.Floor.Include(f => f.Building).Where(f => floorIdsToUpdate.Contains(f.Id) && f.TenantId == tenantId).ToDictionaryAsync(f => f.Id); - - var workAreaIdsToUpdate = workAreasToUpdateDto.Select(d => d.Id!.Value).ToList(); - var existingWorkAreas = await _context.WorkAreas.Include(wa => wa.Floor!.Building).Where(wa => workAreaIdsToUpdate.Contains(wa.Id) && wa.TenantId == tenantId).ToDictionaryAsync(wa => wa.Id); - - // Fetch parent entities for items being created to get their ProjectIds - var buildingIdsForNewFloors = floorsToCreateDto.Select(f => f.BuildingId).ToList(); - var parentBuildingsForNewFloors = await _context.Buildings.Where(b => buildingIdsForNewFloors.Contains(b.Id)).ToDictionaryAsync(b => b.Id); - - var floorIdsForNewWorkAreas = workAreasToCreateDto.Select(wa => wa.FloorId).ToList(); - var parentFloorsForNewWorkAreas = await _context.Floor.Include(f => f.Building).Where(f => floorIdsForNewWorkAreas.Contains(f.Id)).ToDictionaryAsync(f => f.Id); - - _logger.LogInfo("Fetched existing entities and parents for new items."); - - // --- Step 4: Aggregate all affected ProjectIds for Security Check --- - var affectedProjectIds = new HashSet(); - - // From buildings being created/updated - buildingsToCreateDto.ForEach(b => affectedProjectIds.Add(b.ProjectId)); - foreach (var b in existingBuildings.Values) { affectedProjectIds.Add(b.ProjectId); } - - // From floors being created/updated - foreach (var f in floorsToCreateDto) { if (parentBuildingsForNewFloors.TryGetValue(f.BuildingId, out var b)) affectedProjectIds.Add(b.ProjectId); } - foreach (var f in existingFloors.Values) { if (f.Building != null) affectedProjectIds.Add(f.Building.ProjectId); } - - // From work areas being created/updated - foreach (var wa in workAreasToCreateDto) { if (parentFloorsForNewWorkAreas.TryGetValue(wa.FloorId, out var f) && f.Building != null) affectedProjectIds.Add(f.Building.ProjectId); } - foreach (var wa in existingWorkAreas.Values) { if (wa.Floor?.Building != null) affectedProjectIds.Add(wa.Floor.Building.ProjectId); } - - // Security Check against the complete list of affected projects - var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageProjectInfra, loggedInEmployee.Id); - if (!hasPermission) - { - _logger.LogWarning("Access DENIED for user {UserId} trying to manage infrastructure for projects.", loggedInEmployee.Id); - return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to manage infrastructure for one or more of the specified projects.", 403); - } - - // --- Step 5: Process all logic IN MEMORY, tracking changes --- - - // Process Buildings - var createdBuildings = new List(); - foreach (var dto in buildingsToCreateDto) - { - var newBuilding = _mapper.Map(dto); - newBuilding.TenantId = tenantId; - createdBuildings.Add(newBuilding); - } - foreach (var dto in buildingsToUpdateDto) { if (existingBuildings.TryGetValue(dto.Id!.Value, out var b)) _mapper.Map(dto, b); } - - // Process Floors - var createdFloors = new List(); - foreach (var dto in floorsToCreateDto) - { - var newFloor = _mapper.Map(dto); - newFloor.TenantId = tenantId; - createdFloors.Add(newFloor); - } - foreach (var dto in floorsToUpdateDto) { if (existingFloors.TryGetValue(dto.Id!.Value, out var f)) _mapper.Map(dto, f); } - - // Process WorkAreas - var createdWorkAreas = new List(); - foreach (var dto in workAreasToCreateDto) - { - var newWorkArea = _mapper.Map(dto); - newWorkArea.TenantId = tenantId; - createdWorkAreas.Add(newWorkArea); - } - foreach (var dto in workAreasToUpdateDto) { if (existingWorkAreas.TryGetValue(dto.Id!.Value, out var wa)) _mapper.Map(dto, wa); } - - // --- Step 6: Save all database changes in a SINGLE TRANSACTION --- - if (createdBuildings.Any()) _context.Buildings.AddRange(createdBuildings); - if (createdFloors.Any()) _context.Floor.AddRange(createdFloors); - if (createdWorkAreas.Any()) _context.WorkAreas.AddRange(createdWorkAreas); - - if (_context.ChangeTracker.HasChanges()) - { - await _context.SaveChangesAsync(); - _logger.LogInfo("Database save successful."); - } - - // --- Step 7: Update Cache using the aggregated ProjectIds (Non-blocking) --- - var finalProjectIds = affectedProjectIds.ToList(); - if (finalProjectIds.Any()) - { - _ = Task.Run(async () => - { - try - { - _logger.LogInfo("Queuing background cache update for {ProjectCount} projects.", finalProjectIds.Count); - // Assuming your cache service has a method to handle this. - await _cache.RemoveProjectsAsync(finalProjectIds); - _logger.LogInfo("Background cache update task completed for projects: {ProjectIds}", string.Join(", ", finalProjectIds)); - } - catch (Exception ex) - { - _logger.LogError(ex, "An error occurred during the background cache update task for projects: {ProjectIds}", string.Join(", ", finalProjectIds)); - } - }); - } - - // --- Step 8: Prepare and return a clear response --- - var responseVm = new { /* ... as before ... */ }; - return ApiResponse.SuccessResponse(responseVm, "Infrastructure changes processed successfully.", 200); - } - catch (Exception ex) - { - _logger.LogError(ex, "An unexpected error occurred in ManageProjectInfraAsync."); - return ApiResponse.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500); - } - } - /// /// Creates or updates a batch of work items. /// This method is optimized to perform all database operations in a single, atomic transaction. @@ -1512,60 +1243,88 @@ namespace Marco.Pms.Services.Service return ApiResponse>.SuccessResponse(responseList, message, 200); } + public async Task DeleteProjectTaskAsync(Guid id, Guid tenantId, Employee loggedInEmployee) + { + // 1. Fetch the task and its parent data in a single query. + // This is still a major optimization, avoiding a separate query for the floor/building. + WorkItem? task = await _context.WorkItems + .AsNoTracking() // Use AsNoTracking because we will re-attach for deletion later. + .Include(t => t.WorkArea) + .ThenInclude(wa => wa!.Floor) + .ThenInclude(f => f!.Building) + .FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId); - //public async Task DeleteProjectTask(Guid id) - //{ - // var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - // List workAreaIds = new List(); - // WorkItem? task = await _context.WorkItems.AsNoTracking().Include(t => t.WorkArea).FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId); - // if (task != null) - // { - // if (task.CompletedWork == 0) - // { - // var assignedTask = await _context.TaskAllocations.Where(t => t.WorkItemId == id).ToListAsync(); - // if (assignedTask.Count == 0) - // { - // _context.WorkItems.Remove(task); - // await _context.SaveChangesAsync(); - // _logger.LogInfo("Task with ID {WorkItemId} has been successfully deleted.", task.Id); + // 2. Guard Clause: Handle non-existent task. + if (task == null) + { + _logger.LogWarning("Attempted to delete a non-existent task with ID {WorkItemId}", id); + return new ServiceResponse + { + Response = ApiResponse.ErrorResponse("Task not found.", $"A task with ID {id} was not found.", 404) + }; + } - // var floorId = task.WorkArea?.FloorId; - // var floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == floorId); + // 3. Guard Clause: Prevent deletion if work has started. + if (task.CompletedWork > 0) + { + double percentage = Math.Round((task.CompletedWork / task.PlannedWork) * 100, 2); + _logger.LogWarning("Task with ID {WorkItemId} is {CompletionPercentage}% complete and cannot be deleted.", task.Id, percentage); + return new ServiceResponse + { + Response = ApiResponse.ErrorResponse($"Task is {percentage}% complete and cannot be deleted.", "Deletion failed because the task has progress.", 400) + }; + } + // 4. Guard Clause: Efficiently check if the task is assigned in a separate, optimized query. + // AnyAsync() is highly efficient and translates to a `SELECT TOP 1` or `EXISTS` in SQL. + bool isAssigned = await _context.TaskAllocations.AnyAsync(t => t.WorkItemId == id); + if (isAssigned) + { + _logger.LogWarning("Task with ID {WorkItemId} is currently assigned and cannot be deleted.", task.Id); + return new ServiceResponse + { + Response = ApiResponse.ErrorResponse("Task is currently assigned and cannot be deleted.", "Deletion failed because the task is assigned to an employee.", 400) + }; + } - // workAreaIds.Add(task.WorkAreaId); - // var projectId = floor?.Building?.ProjectId; + // --- Success Path: All checks passed, proceed with deletion --- - // var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = $"Task Deleted in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}, in Area: {task.WorkArea?.AreaName} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}" }; - // await _signalR.SendNotificationAsync(notification); - // await _cache.DeleteWorkItemByIdAsync(task.Id); - // if (projectId != null) - // { - // await _cache.DeleteProjectByIdAsync(projectId.Value); - // } - // } - // else - // { - // _logger.LogWarning("Task with ID {WorkItemId} is currently assigned and cannot be deleted.", task.Id); - // return BadRequest(ApiResponse.ErrorResponse("Task is currently assigned and cannot be deleted.", "Task is currently assigned and cannot be deleted.", 400)); - // } - // } - // else - // { - // double percentage = (task.CompletedWork / task.PlannedWork) * 100; - // percentage = Math.Round(percentage, 2); - // _logger.LogWarning("Task with ID {WorkItemId} is {CompletionPercentage}% complete and cannot be deleted", task.Id, percentage); - // return BadRequest(ApiResponse.ErrorResponse(System.String.Format("Task is {0}% complete and cannot be deleted", percentage), System.String.Format("Task is {0}% complete and cannot be deleted", percentage), 400)); + var building = task.WorkArea?.Floor?.Building; + var notification = new + { + LoggedInUserId = loggedInEmployee.Id, + Keyword = "WorkItem", + WorkAreaIds = new[] { task.WorkAreaId }, + Message = $"Task Deleted in Building: {building?.Name ?? "N/A"}, on Floor: {task.WorkArea?.Floor?.FloorName ?? "N/A"}, in Area: {task.WorkArea?.AreaName ?? "N/A"} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}" + }; - // } - // } - // else - // { - // _logger.LogWarning("Task with ID {WorkItemId} not found ID database", id); - // } - // return Ok(ApiResponse.SuccessResponse(new { }, "Task deleted successfully", 200)); - //} + // 5. Perform the database deletion. + // We must attach a new instance or the original one without AsNoTracking. + // Since we used AsNoTracking, we create a 'stub' entity for deletion. + // This is more efficient than re-querying. + _context.WorkItems.Remove(new WorkItem { Id = task.Id }); + await _context.SaveChangesAsync(); + _logger.LogInfo("Task with ID {WorkItemId} has been successfully deleted.", task.Id); + // 6. Perform cache operations concurrently. + var cacheTasks = new List + { + _cache.DeleteWorkItemByIdAsync(task.Id) + }; + + if (building?.ProjectId != null) + { + cacheTasks.Add(_cache.DeleteProjectByIdAsync(building.ProjectId)); + } + await Task.WhenAll(cacheTasks); + + // 7. Return the final success response. + return new ServiceResponse + { + Notification = notification, + Response = ApiResponse.SuccessResponse(new { id = task.Id }, "Task deleted successfully.", 200) + }; + } #endregion #region =================================================================== Helper Functions =================================================================== diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs index f1c89cc..0c7c964 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs @@ -23,6 +23,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> GetWorkItemsAsync(Guid workAreaId, Guid tenantId, Employee loggedInEmployee); Task ManageProjectInfraAsync(List infraDtos, Guid tenantId, Employee loggedInEmployee); Task>> CreateProjectTaskAsync(List workItemDtos, Guid tenantId, Employee loggedInEmployee); + Task DeleteProjectTaskAsync(Guid id, Guid tenantId, Employee loggedInEmployee); } } From 8735de3d930b18116479d3bbeaee50d59f4bc97e Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 17 Jul 2025 10:17:57 +0530 Subject: [PATCH 111/124] Remove the projectHelper and ProjetsHelper and move its bussiness logic to project services --- Marco.Pms.CacheHelper/EmployeeCache.cs | 20 +++ Marco.Pms.CacheHelper/ProjectCache.cs | 74 ++++++++---- .../EmployeePermissionMongoDB.cs | 1 + .../MongoDBModels/ProjectMongoDB.cs | 1 + .../Controllers/AttendanceController.cs | 13 +- .../Controllers/EmployeeController.cs | 9 +- .../Controllers/UserController.cs | 11 +- Marco.Pms.Services/Helpers/ProjectHelper.cs | 37 ------ Marco.Pms.Services/Helpers/ProjectsHelper.cs | 81 ------------- Marco.Pms.Services/Program.cs | 1 - Marco.Pms.Services/Service/ProjectServices.cs | 114 ++++++++++++++++-- .../ServiceInterfaces/IProjectServices.cs | 6 + 12 files changed, 206 insertions(+), 162 deletions(-) delete mode 100644 Marco.Pms.Services/Helpers/ProjectHelper.cs delete mode 100644 Marco.Pms.Services/Helpers/ProjectsHelper.cs diff --git a/Marco.Pms.CacheHelper/EmployeeCache.cs b/Marco.Pms.CacheHelper/EmployeeCache.cs index f7b7066..0079106 100644 --- a/Marco.Pms.CacheHelper/EmployeeCache.cs +++ b/Marco.Pms.CacheHelper/EmployeeCache.cs @@ -33,6 +33,8 @@ namespace Marco.Pms.CacheHelper var result = await _collection.UpdateOneAsync(filter, update, options); + await InitializeCollectionAsync(); + // 6. Return a more accurate result indicating success for both updates and upserts. // The operation is successful if an existing document was modified OR a new one was created. return result.IsAcknowledged && (result.ModifiedCount > 0 || result.UpsertedId != null); @@ -51,6 +53,7 @@ namespace Marco.Pms.CacheHelper { return false; } + await InitializeCollectionAsync(); return true; } public async Task> GetProjectsFromCache(Guid employeeId) @@ -177,5 +180,22 @@ namespace Marco.Pms.CacheHelper return true; } + + // A private method to handle the one-time setup of the collection's indexes. + private async Task InitializeCollectionAsync() + { + // 1. Define the TTL (Time-To-Live) index on the 'ExpireAt' field. + var indexKeys = Builders.IndexKeys.Ascending(x => x.ExpireAt); + var indexOptions = new CreateIndexOptions + { + // This tells MongoDB to automatically delete documents when their 'ExpireAt' time is reached. + ExpireAfter = TimeSpan.FromSeconds(0) + }; + var indexModel = new CreateIndexModel(indexKeys, indexOptions); + + // 2. Create the index. This is an idempotent operation if the index already exists. + // Use CreateOneAsync since we are only creating a single index. + await _collection.Indexes.CreateOneAsync(indexModel); + } } } diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index 9417724..df95419 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -11,27 +11,59 @@ namespace Marco.Pms.CacheHelper { public class ProjectCache { - private readonly ApplicationDbContext _context; - private readonly IMongoCollection _projetCollection; + private readonly IMongoCollection _projectCollection; private readonly IMongoCollection _taskCollection; public ProjectCache(ApplicationDbContext context, IConfiguration configuration) { var connectionString = configuration["MongoDB:ConnectionString"]; - _context = context; var mongoUrl = new MongoUrl(connectionString); var client = new MongoClient(mongoUrl); // Your MongoDB connection string var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name - _projetCollection = mongoDB.GetCollection("ProjectDetails"); + _projectCollection = mongoDB.GetCollection("ProjectDetails"); _taskCollection = mongoDB.GetCollection("WorkItemDetails"); } public async Task AddProjectDetailsToCache(ProjectMongoDB projectDetails) { - await _projetCollection.InsertOneAsync(projectDetails); + await _projectCollection.InsertOneAsync(projectDetails); + + var indexKeys = Builders.IndexKeys.Ascending(x => x.ExpireAt); + var indexOptions = new CreateIndexOptions + { + ExpireAfter = TimeSpan.Zero // required for fixed expiration time + }; + var indexModel = new CreateIndexModel(indexKeys, indexOptions); + await _projectCollection.Indexes.CreateOneAsync(indexModel); + } + // The method should focus only on inserting data. public async Task AddProjectDetailsListToCache(List projectDetailsList) { - await _projetCollection.InsertManyAsync(projectDetailsList); + // 1. Add a guard clause to avoid an unnecessary database call for an empty list. + if (projectDetailsList == null || !projectDetailsList.Any()) + { + return; + } + + // 2. Perform the insert operation. This is the only responsibility of this method. + await _projectCollection.InsertManyAsync(projectDetailsList); + await InitializeCollectionAsync(); + } + // A private method to handle the one-time setup of the collection's indexes. + private async Task InitializeCollectionAsync() + { + // 1. Define the TTL (Time-To-Live) index on the 'ExpireAt' field. + var indexKeys = Builders.IndexKeys.Ascending(x => x.ExpireAt); + var indexOptions = new CreateIndexOptions + { + // This tells MongoDB to automatically delete documents when their 'ExpireAt' time is reached. + ExpireAfter = TimeSpan.FromSeconds(0) + }; + var indexModel = new CreateIndexModel(indexKeys, indexOptions); + + // 2. Create the index. This is an idempotent operation if the index already exists. + // Use CreateOneAsync since we are only creating a single index. + await _projectCollection.Indexes.CreateOneAsync(indexModel); } public async Task UpdateProjectDetailsOnlyToCache(Project project, StatusMaster projectStatus) { @@ -51,7 +83,7 @@ namespace Marco.Pms.CacheHelper ); // Perform the update - var result = await _projetCollection.UpdateOneAsync( + var result = await _projectCollection.UpdateOneAsync( filter: r => r.Id == project.Id.ToString(), update: updates ); @@ -71,7 +103,7 @@ namespace Marco.Pms.CacheHelper var projection = Builders.Projection.Exclude(p => p.Buildings); // Perform query - var project = await _projetCollection + var project = await _projectCollection .Find(filter) .Project(projection) .FirstOrDefaultAsync(); @@ -83,7 +115,7 @@ namespace Marco.Pms.CacheHelper List stringProjectIds = projectIds.Select(p => p.ToString()).ToList(); var filter = Builders.Filter.In(p => p.Id, stringProjectIds); var projection = Builders.Projection.Exclude(p => p.Buildings); - var projects = await _projetCollection + var projects = await _projectCollection .Find(filter) .Project(projection) .ToListAsync(); @@ -92,14 +124,14 @@ namespace Marco.Pms.CacheHelper public async Task DeleteProjectByIdFromCacheAsync(Guid projectId) { var filter = Builders.Filter.Eq(e => e.Id, projectId.ToString()); - var result = await _projetCollection.DeleteOneAsync(filter); + var result = await _projectCollection.DeleteOneAsync(filter); return result.DeletedCount > 0; } public async Task RemoveProjectsFromCacheAsync(List projectIds) { var stringIds = projectIds.Select(id => id.ToString()).ToList(); var filter = Builders.Filter.In(p => p.Id, stringIds); - var result = await _projetCollection.DeleteManyAsync(filter); + var result = await _projectCollection.DeleteManyAsync(filter); return result.DeletedCount > 0; } @@ -125,7 +157,7 @@ namespace Marco.Pms.CacheHelper var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); var update = Builders.Update.Push("Buildings", buildingMongo); - var result = await _projetCollection.UpdateOneAsync(filter, update); + var result = await _projectCollection.UpdateOneAsync(filter, update); if (result.MatchedCount == 0) { @@ -155,7 +187,7 @@ namespace Marco.Pms.CacheHelper ); var update = Builders.Update.Push("Buildings.$.Floors", floorMongo); - var result = await _projetCollection.UpdateOneAsync(filter, update); + var result = await _projectCollection.UpdateOneAsync(filter, update); if (result.MatchedCount == 0) { @@ -189,7 +221,7 @@ namespace Marco.Pms.CacheHelper var update = Builders.Update.Push("Buildings.$[b].Floors.$[f].WorkAreas", workAreaMongo); var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; - var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); + var result = await _projectCollection.UpdateOneAsync(filter, update, updateOptions); if (result.MatchedCount == 0) { @@ -221,7 +253,7 @@ namespace Marco.Pms.CacheHelper Builders.Update.Set("Buildings.$.Description", building.Description) ); - var result = await _projetCollection.UpdateOneAsync(filter, update); + var result = await _projectCollection.UpdateOneAsync(filter, update); if (result.MatchedCount == 0) { @@ -246,7 +278,7 @@ namespace Marco.Pms.CacheHelper var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); - var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); + var result = await _projectCollection.UpdateOneAsync(filter, update, updateOptions); if (result.MatchedCount == 0) { @@ -272,7 +304,7 @@ namespace Marco.Pms.CacheHelper var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); - var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); + var result = await _projectCollection.UpdateOneAsync(filter, update, updateOptions); if (result.MatchedCount == 0) { @@ -296,7 +328,7 @@ namespace Marco.Pms.CacheHelper var filter = Builders.Filter.Eq(p => p.Id, projectId.ToString()); // Project only the "Buildings" field from the document - var buildings = await _projetCollection + var buildings = await _projectCollection .Find(filter) .Project(p => p.Buildings) .FirstOrDefaultAsync(); @@ -315,7 +347,7 @@ namespace Marco.Pms.CacheHelper public async Task UpdatePlannedAndCompleteWorksInBuildingFromCache(Guid workAreaId, double plannedWork, double completedWork) { var filter = Builders.Filter.Eq("Buildings.Floors.WorkAreas._id", workAreaId.ToString()); - var project = await _projetCollection.Find(filter).FirstOrDefaultAsync(); + var project = await _projectCollection.Find(filter).FirstOrDefaultAsync(); string? selectedBuildingId = null; string? selectedFloorId = null; @@ -353,7 +385,7 @@ namespace Marco.Pms.CacheHelper .Inc("Buildings.$[b].CompletedWork", completedWork) .Inc("PlannedWork", plannedWork) .Inc("CompletedWork", completedWork); - var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions); + var result = await _projectCollection.UpdateOneAsync(filter, update, updateOptions); } public async Task GetBuildingAndFloorByWorkAreaIdFromCache(Guid workAreaId) @@ -393,7 +425,7 @@ namespace Marco.Pms.CacheHelper { "WorkArea", "$Buildings.Floors.WorkAreas" } }) }; - var result = await _projetCollection.Aggregate(pipeline).FirstOrDefaultAsync(); + var result = await _projectCollection.Aggregate(pipeline).FirstOrDefaultAsync(); if (result == null) return null; return result; diff --git a/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs b/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs index 49c514e..fab2b84 100644 --- a/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs @@ -9,5 +9,6 @@ namespace Marco.Pms.Model.MongoDBModels public List ApplicationRoleIds { get; set; } = new List(); public List PermissionIds { get; set; } = new List(); public List ProjectIds { get; set; } = new List(); + public DateTime ExpireAt { get; set; } = DateTime.UtcNow.Date.AddDays(1); } } diff --git a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs index 7f3a557..aac0e2c 100644 --- a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs @@ -14,5 +14,6 @@ public int TeamSize { get; set; } public double CompletedWork { get; set; } public double PlannedWork { get; set; } + public DateTime ExpireAt { get; set; } = DateTime.UtcNow.Date.AddDays(1); } } diff --git a/Marco.Pms.Services/Controllers/AttendanceController.cs b/Marco.Pms.Services/Controllers/AttendanceController.cs index 1a5e4e7..7339966 100644 --- a/Marco.Pms.Services/Controllers/AttendanceController.cs +++ b/Marco.Pms.Services/Controllers/AttendanceController.cs @@ -9,6 +9,7 @@ using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.AttendanceVM; using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Service; +using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; @@ -28,7 +29,7 @@ namespace MarcoBMS.Services.Controllers { private readonly ApplicationDbContext _context; private readonly EmployeeHelper _employeeHelper; - private readonly ProjectsHelper _projectsHelper; + private readonly IProjectServices _projectServices; private readonly UserHelper _userHelper; private readonly S3UploadService _s3Service; private readonly PermissionServices _permission; @@ -37,11 +38,11 @@ namespace MarcoBMS.Services.Controllers public AttendanceController( - ApplicationDbContext context, EmployeeHelper employeeHelper, ProjectsHelper projectsHelper, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permission, IHubContext signalR) + ApplicationDbContext context, EmployeeHelper employeeHelper, IProjectServices projectServices, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permission, IHubContext signalR) { _context = context; _employeeHelper = employeeHelper; - _projectsHelper = projectsHelper; + _projectServices = projectServices; _userHelper = userHelper; _s3Service = s3Service; _logger = logger; @@ -188,7 +189,7 @@ namespace MarcoBMS.Services.Controllers List lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date >= fromDate.Date && c.AttendanceDate.Date <= toDate.Date && c.TenantId == TenantId).ToListAsync(); - List projectteam = await _projectsHelper.GetTeamByProject(TenantId, projectId, true); + List projectteam = await _projectServices.GetTeamByProject(TenantId, projectId, true); var jobRole = await _context.JobRoles.ToListAsync(); foreach (Attendance? attendance in lstAttendance) { @@ -295,7 +296,7 @@ namespace MarcoBMS.Services.Controllers List lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date == forDate && c.TenantId == TenantId).ToListAsync(); - List projectteam = await _projectsHelper.GetTeamByProject(TenantId, projectId, IncludeInActive); + List projectteam = await _projectServices.GetTeamByProject(TenantId, projectId, IncludeInActive); var idList = projectteam.Select(p => p.EmployeeId).ToList(); //var emp = await _context.Employees.Where(e => idList.Contains(e.Id)).Include(e => e.JobRole).ToListAsync(); var jobRole = await _context.JobRoles.ToListAsync(); @@ -378,7 +379,7 @@ 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); + List projectteam = await _projectServices.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/EmployeeController.cs b/Marco.Pms.Services/Controllers/EmployeeController.cs index c9e19fa..d5d7f3d 100644 --- a/Marco.Pms.Services/Controllers/EmployeeController.cs +++ b/Marco.Pms.Services/Controllers/EmployeeController.cs @@ -9,6 +9,7 @@ using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Service; +using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; @@ -37,13 +38,13 @@ namespace MarcoBMS.Services.Controllers private readonly ILoggingService _logger; private readonly IHubContext _signalR; private readonly PermissionServices _permission; - private readonly ProjectsHelper _projectsHelper; + private readonly IProjectServices _projectServices; private readonly Guid tenantId; public EmployeeController(UserManager userManager, IEmailSender emailSender, ApplicationDbContext context, EmployeeHelper employeeHelper, UserHelper userHelper, IConfiguration configuration, ILoggingService logger, - IHubContext signalR, PermissionServices permission, ProjectsHelper projectsHelper) + IHubContext signalR, PermissionServices permission, IProjectServices projectServices) { _context = context; _userManager = userManager; @@ -54,7 +55,7 @@ namespace MarcoBMS.Services.Controllers _logger = logger; _signalR = signalR; _permission = permission; - _projectsHelper = projectsHelper; + _projectServices = projectServices; tenantId = _userHelper.GetTenantId(); } @@ -119,7 +120,7 @@ namespace MarcoBMS.Services.Controllers loggedInEmployee.Id, projectid ?? Guid.Empty, ShowInactive); // Step 3: Fetch project access and permissions - var projectIds = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); + var projectIds = await _projectServices.GetMyProjectIdsAsync(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/UserController.cs b/Marco.Pms.Services/Controllers/UserController.cs index 4bb4432..8269d3e 100644 --- a/Marco.Pms.Services/Controllers/UserController.cs +++ b/Marco.Pms.Services/Controllers/UserController.cs @@ -4,6 +4,7 @@ using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Employee; +using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -19,14 +20,14 @@ namespace MarcoBMS.Services.Controllers private readonly UserHelper _userHelper; private readonly EmployeeHelper _employeeHelper; - private readonly ProjectsHelper _projectsHelper; + private readonly IProjectServices _projectServices; private readonly RolesHelper _rolesHelper; - public UserController(EmployeeHelper employeeHelper, ProjectsHelper projectsHelper, UserHelper userHelper, RolesHelper rolesHelper) + public UserController(EmployeeHelper employeeHelper, IProjectServices projectServices, UserHelper userHelper, RolesHelper rolesHelper) { _userHelper = userHelper; _employeeHelper = employeeHelper; - _projectsHelper = projectsHelper; + _projectServices = projectServices; _rolesHelper = rolesHelper; } @@ -56,12 +57,12 @@ namespace MarcoBMS.Services.Controllers /* User with permission manage project can see all projects */ if (featurePermission != null && featurePermission.Exists(c => c.Id.ToString() == "172fc9b6-755b-4f62-ab26-55c34a330614")) { - List projects = await _projectsHelper.GetAllProjectByTanentID(emp.TenantId); + List projects = await _projectServices.GetAllProjectByTanentID(emp.TenantId); projectsId = projects.Select(c => c.Id.ToString()).ToArray(); } else { - List allocation = await _projectsHelper.GetProjectByEmployeeID(emp.Id); + List allocation = await _projectServices.GetProjectByEmployeeID(emp.Id); projectsId = allocation.Select(c => c.ProjectId.ToString()).ToArray(); } EmployeeProfile profile = new EmployeeProfile() { }; diff --git a/Marco.Pms.Services/Helpers/ProjectHelper.cs b/Marco.Pms.Services/Helpers/ProjectHelper.cs deleted file mode 100644 index f1b688e..0000000 --- a/Marco.Pms.Services/Helpers/ProjectHelper.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Marco.Pms.DataAccess.Data; -using Marco.Pms.Model.Projects; -using Microsoft.CodeAnalysis; -using Microsoft.EntityFrameworkCore; - - -namespace ModelServices.Helpers -{ - public class ProjectHelper - { - private readonly ApplicationDbContext _context; - public ProjectHelper(ApplicationDbContext context) - { - _context = context; - } - - public async Task> GetTeamByProject(Guid TenantId, Guid ProjectId, bool IncludeInactive) - { - if (IncludeInactive) - { - - var employees = await _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == ProjectId).Include(e => e.Employee).ToListAsync(); - - return employees; - } - else - { - var employees = await _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == ProjectId && c.IsActive == true).Include(e => e.Employee).ToListAsync(); - - return employees; - } - } - - - - } -} diff --git a/Marco.Pms.Services/Helpers/ProjectsHelper.cs b/Marco.Pms.Services/Helpers/ProjectsHelper.cs deleted file mode 100644 index e7e1dd6..0000000 --- a/Marco.Pms.Services/Helpers/ProjectsHelper.cs +++ /dev/null @@ -1,81 +0,0 @@ -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 Marco.Pms.Services.Service; -using Microsoft.EntityFrameworkCore; - -namespace MarcoBMS.Services.Helpers -{ - public class ProjectsHelper - { - private readonly ApplicationDbContext _context; - private readonly CacheUpdateHelper _cache; - private readonly PermissionServices _permission; - - public ProjectsHelper(ApplicationDbContext context, CacheUpdateHelper cache, PermissionServices permission) - { - _context = context; - _cache = cache; - _permission = permission; - } - - public async Task> GetAllProjectByTanentID(Guid tanentID) - { - List alloc = await _context.Projects.Where(c => c.TenantId == tanentID).ToListAsync(); - return alloc; - } - - public async Task> GetProjectByEmployeeID(Guid employeeID) - { - List alloc = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeID && c.IsActive == true).Include(c => c.Project).ToListAsync(); - return alloc; - } - - public async Task> GetTeamByProject(Guid TenantId, Guid ProjectId, bool IncludeInactive) - { - if (IncludeInactive) - { - - var employees = await _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == ProjectId).Include(e => e.Employee).ToListAsync(); - - return employees; - } - else - { - var employees = await _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == ProjectId && c.IsActive == true).Include(e => e.Employee).ToListAsync(); - - return employees; - } - } - - public async Task> GetMyProjects(Guid tenantId, Employee LoggedInEmployee) - { - var projectIds = await _cache.GetProjects(LoggedInEmployee.Id); - - if (projectIds == null) - { - var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageProject, LoggedInEmployee.Id); - if (hasPermission) - { - var projects = await _context.Projects.Where(c => c.TenantId == tenantId).ToListAsync(); - projectIds = projects.Select(p => p.Id).ToList(); - } - else - { - var allocation = await GetProjectByEmployeeID(LoggedInEmployee.Id); - if (!allocation.Any()) - { - return new List(); - } - projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); - } - await _cache.AddProjects(LoggedInEmployee.Id, projectIds); - } - - return projectIds; - } - - } -} \ No newline at end of file diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index 3c73416..3f012e2 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -167,7 +167,6 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs index d7ab2ac..9406ec9 100644 --- a/Marco.Pms.Services/Service/ProjectServices.cs +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -12,7 +12,6 @@ using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Model.ViewModels.Projects; using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Service.ServiceInterfaces; -using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.CodeAnalysis; using Microsoft.EntityFrameworkCore; @@ -25,7 +24,6 @@ namespace Marco.Pms.Services.Service private readonly IDbContextFactory _dbContextFactory; private readonly ApplicationDbContext _context; // Keeping this for direct scoped context use where appropriate private readonly ILoggingService _logger; - private readonly ProjectsHelper _projectsHelper; private readonly PermissionServices _permission; private readonly CacheUpdateHelper _cache; private readonly IMapper _mapper; @@ -34,7 +32,6 @@ namespace Marco.Pms.Services.Service IDbContextFactory dbContextFactory, ApplicationDbContext context, ILoggingService logger, - ProjectsHelper projectsHelper, PermissionServices permission, CacheUpdateHelper cache, IMapper mapper, @@ -43,7 +40,6 @@ namespace Marco.Pms.Services.Service _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); _context = context ?? throw new ArgumentNullException(nameof(context)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _projectsHelper = projectsHelper ?? throw new ArgumentNullException(nameof(projectsHelper)); _permission = permission ?? throw new ArgumentNullException(nameof(permission)); _cache = cache ?? throw new ArgumentNullException(nameof(cache)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); @@ -64,7 +60,7 @@ namespace Marco.Pms.Services.Service _logger.LogInfo("Basic project list requested by EmployeeId {EmployeeId}", loggedInEmployee.Id); // Step 2: Get the list of project IDs the user has access to - List accessibleProjectIds = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); + List accessibleProjectIds = await GetMyProjects(tenantId, loggedInEmployee); if (accessibleProjectIds == null || !accessibleProjectIds.Any()) { @@ -94,7 +90,7 @@ namespace Marco.Pms.Services.Service _logger.LogInfo("Starting GetAllProjects for TenantId: {TenantId}, User: {UserId}", tenantId, loggedInEmployee.Id); // --- Step 1: Get a list of project IDs the user can access --- - List projectIds = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); + List projectIds = await GetMyProjects(tenantId, loggedInEmployee); if (!projectIds.Any()) { _logger.LogInfo("User has no assigned projects. Returning empty list."); @@ -743,7 +739,7 @@ namespace Marco.Pms.Services.Service // This is a placeholder for your actual, more specific permission logic. // It should also handle the case where a user is requesting their own projects (employeeId == loggedInEmployee.Id). var hasPermission = await _permission.HasPermission(PermissionsMaster.ViewProject, loggedInEmployee.Id); - var projectIds = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); + var projectIds = await GetMyProjects(tenantId, loggedInEmployee); if (!hasPermission) { _logger.LogWarning("Access DENIED for user {UserId} trying to view projects for employee {TargetEmployeeId}.", loggedInEmployee.Id, employeeId); @@ -1329,6 +1325,110 @@ namespace Marco.Pms.Services.Service #region =================================================================== Helper Functions =================================================================== + public async Task> GetAllProjectByTanentID(Guid tanentId) + { + List alloc = await _context.Projects.Where(c => c.TenantId == tanentId).ToListAsync(); + return alloc; + } + + public async Task> GetProjectByEmployeeID(Guid employeeId) + { + List alloc = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.IsActive == true).Include(c => c.Project).ToListAsync(); + return alloc; + } + + public async Task> GetTeamByProject(Guid TenantId, Guid ProjectId, bool IncludeInactive) + { + if (IncludeInactive) + { + + var employees = await _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == ProjectId).Include(e => e.Employee).ToListAsync(); + + return employees; + } + else + { + var employees = await _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == ProjectId && c.IsActive == true).Include(e => e.Employee).ToListAsync(); + + return employees; + } + } + + public async Task> GetMyProjects(Guid tenantId, Employee LoggedInEmployee) + { + var projectIds = await _cache.GetProjects(LoggedInEmployee.Id); + + if (projectIds == null) + { + var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageProject, LoggedInEmployee.Id); + if (hasPermission) + { + var projects = await _context.Projects.Where(c => c.TenantId == tenantId).ToListAsync(); + projectIds = projects.Select(p => p.Id).ToList(); + } + else + { + var allocation = await GetProjectByEmployeeID(LoggedInEmployee.Id); + if (!allocation.Any()) + { + return new List(); + } + projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); + } + await _cache.AddProjects(LoggedInEmployee.Id, projectIds); + } + return projectIds; + } + + public async Task> GetMyProjectIdsAsync(Guid tenantId, Employee loggedInEmployee) + { + // 1. Attempt to retrieve the list of project IDs from the cache first. + // This is the "happy path" and should be as fast as possible. + List? projectIds = await _cache.GetProjects(loggedInEmployee.Id); + + if (projectIds != null) + { + // Cache Hit: Return the cached list immediately. + return projectIds; + } + + // 2. Cache Miss: The list was not in the cache, so we must fetch it from the database. + List newProjectIds; + + // Check for the specific permission. + var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageProject, loggedInEmployee.Id); + + if (hasPermission) + { + // 3a. OPTIMIZATION: User has permission to see all projects. + // Fetch *only* the Ids directly from the database. This is far more efficient + // than fetching full Project objects and then selecting the Ids in memory. + newProjectIds = await _context.Projects + .Where(p => p.TenantId == tenantId) + .Select(p => p.Id) // This translates to `SELECT Id FROM Projects...` in SQL. + .ToListAsync(); + } + else + { + // 3b. OPTIMIZATION: User can only see projects they are allocated to. + // We go directly to the source (ProjectAllocations) and ask the database + // for a distinct list of ProjectIds. This is much better than calling a + // helper function that might return full allocation objects. + newProjectIds = await _context.ProjectAllocations + .Where(a => a.EmployeeId == loggedInEmployee.Id && a.ProjectId != Guid.Empty) + .Select(a => a.ProjectId) + .Distinct() // Pushes the DISTINCT operation to the database. + .ToListAsync(); + } + + // 4. Populate the cache with the newly fetched list (even if it's empty). + // This prevents repeated database queries for employees with no projects. + await _cache.AddProjects(loggedInEmployee.Id, newProjectIds); + + return newProjectIds; + } + + /// /// 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 diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs index 0c7c964..b5acccc 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs @@ -1,5 +1,6 @@ using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Employees; +using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Projects; @@ -25,5 +26,10 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task>> CreateProjectTaskAsync(List workItemDtos, Guid tenantId, Employee loggedInEmployee); Task DeleteProjectTaskAsync(Guid id, Guid tenantId, Employee loggedInEmployee); + Task> GetAllProjectByTanentID(Guid tanentId); + Task> GetProjectByEmployeeID(Guid employeeId); + Task> GetTeamByProject(Guid TenantId, Guid ProjectId, bool IncludeInactive); + Task> GetMyProjectIdsAsync(Guid tenantId, Employee LoggedInEmployee); + } } From 6ac28de56abea7d7cebef06ae77789d46c670608 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 17 Jul 2025 12:42:02 +0530 Subject: [PATCH 112/124] Removed the reassgining of same object --- Marco.Pms.Services/Helpers/DirectoryHelper.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Marco.Pms.Services/Helpers/DirectoryHelper.cs b/Marco.Pms.Services/Helpers/DirectoryHelper.cs index 2963ff2..7af4b4d 100644 --- a/Marco.Pms.Services/Helpers/DirectoryHelper.cs +++ b/Marco.Pms.Services/Helpers/DirectoryHelper.cs @@ -1157,11 +1157,12 @@ namespace Marco.Pms.Services.Helpers List employeeBuckets = await _context.EmployeeBucketMappings.Where(b => b.EmployeeId == LoggedInEmployee.Id).ToListAsync(); var bucketIds = employeeBuckets.Select(b => b.BucketId).ToList(); - List employeeBucketVM = await _context.EmployeeBucketMappings.Where(b => bucketIds.Contains(b.BucketId)).ToListAsync(); + List bucketList = new List(); if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) { bucketList = await _context.Buckets.Include(b => b.CreatedBy).Where(b => b.TenantId == tenantId).ToListAsync(); + bucketIds = bucketList.Select(b => b.Id).ToList(); } else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) || permissionIds.Contains(PermissionsMaster.DirectoryUser)) { @@ -1173,6 +1174,8 @@ namespace Marco.Pms.Services.Helpers return ApiResponse.ErrorResponse("You don't have permission", "You don't have permission", 401); } + List employeeBucketVM = await _context.EmployeeBucketMappings.Where(b => bucketIds.Contains(b.BucketId)).ToListAsync(); + List bucketVMs = new List(); if (bucketList.Any()) { From 5deb97d73b61b2d9f6532e9204aa3eb04a4686fb Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 17 Jul 2025 12:53:01 +0530 Subject: [PATCH 113/124] Added one more condition to check if active is false while removing the employee from buckets --- Marco.Pms.Services/Helpers/DirectoryHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Marco.Pms.Services/Helpers/DirectoryHelper.cs b/Marco.Pms.Services/Helpers/DirectoryHelper.cs index 7af4b4d..3dd578e 100644 --- a/Marco.Pms.Services/Helpers/DirectoryHelper.cs +++ b/Marco.Pms.Services/Helpers/DirectoryHelper.cs @@ -1369,7 +1369,7 @@ namespace Marco.Pms.Services.Helpers _context.EmployeeBucketMappings.Add(employeeBucketMapping); assignedEmployee += 1; } - else + else if (!assignBucket.IsActive) { EmployeeBucketMapping? employeeBucketMapping = employeeBuckets.FirstOrDefault(eb => eb.BucketId == bucketId && eb.EmployeeId == assignBucket.EmployeeId); if (employeeBucketMapping != null) From 30d614fa11c26cfec2e8ec0121f2ee35b68e413c Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 17 Jul 2025 16:06:54 +0530 Subject: [PATCH 114/124] Added the logs setp in program.cs --- Marco.Pms.Services/Program.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index 3f012e2..5549702 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -23,9 +23,21 @@ var builder = WebApplication.CreateBuilder(args); #region ======================= Service Configuration (Dependency Injection) ======================= #region Logging + +// Add Serilog Configuration +string? mongoConn = builder.Configuration["MongoDB:SerilogDatabaseUrl"]; +string timeString = "00:00:30"; +TimeSpan.TryParse(timeString, out TimeSpan timeSpan); + builder.Host.UseSerilog((context, config) => { - config.ReadFrom.Configuration(context.Configuration); + config.ReadFrom.Configuration(context.Configuration) + .WriteTo.MongoDB( + databaseUrl: mongoConn ?? string.Empty, + collectionName: "api-logs", + batchPostingLimit: 100, + period: timeSpan + ); }); #endregion From 0ecf258661ae373406c744b57e175482c4487428 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 17 Jul 2025 17:01:51 +0530 Subject: [PATCH 115/124] Deleted the unused variable --- .../Controllers/ProjectController.cs | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 362c2af..796fd39 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -1,9 +1,6 @@ -using Marco.Pms.DataAccess.Data; -using Marco.Pms.Model.Dtos.Project; +using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Utilities; -using Marco.Pms.Services.Helpers; -using Marco.Pms.Services.Service; using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; @@ -20,30 +17,21 @@ namespace MarcoBMS.Services.Controllers public class ProjectController : ControllerBase { private readonly IProjectServices _projectServices; - private readonly ApplicationDbContext _context; private readonly UserHelper _userHelper; private readonly ILoggingService _logger; private readonly ISignalRService _signalR; - private readonly PermissionServices _permission; - private readonly CacheUpdateHelper _cache; private readonly Guid tenantId; public ProjectController( - ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, ISignalRService signalR, - CacheUpdateHelper cache, - PermissionServices permission, IProjectServices projectServices) { - _context = context; _userHelper = userHelper; _logger = logger; _signalR = signalR; - _cache = cache; - _permission = permission; _projectServices = projectServices; tenantId = userHelper.GetTenantId(); } From c8978ee9b1d24a9043174b4ddcfb680d9df9ed66 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 17 Jul 2025 16:38:39 +0530 Subject: [PATCH 116/124] added new function delete all employee entries from cache --- Marco.Pms.CacheHelper/EmployeeCache.cs | 21 +++++++++++-------- .../Helpers/CacheUpdateHelper.cs | 11 ++++++++++ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/Marco.Pms.CacheHelper/EmployeeCache.cs b/Marco.Pms.CacheHelper/EmployeeCache.cs index 0079106..7c7f4b4 100644 --- a/Marco.Pms.CacheHelper/EmployeeCache.cs +++ b/Marco.Pms.CacheHelper/EmployeeCache.cs @@ -122,16 +122,10 @@ namespace Marco.Pms.CacheHelper public async Task ClearAllProjectIdsByPermissionIdFromCache(Guid permissionId) { var filter = Builders.Filter.AnyEq(e => e.PermissionIds, permissionId.ToString()); + var update = Builders.Update.Set(e => e.ProjectIds, new List()); - var update = Builders.Update - .Set(e => e.ProjectIds, new List()); - - var result = await _collection.UpdateOneAsync(filter, update); - - if (result.MatchedCount == 0) - return false; - - return true; + var result = await _collection.UpdateManyAsync(filter, update).ConfigureAwait(false); + return result.IsAcknowledged && result.ModifiedCount > 0; } public async Task RemoveRoleIdFromCache(Guid employeeId, Guid roleId) { @@ -180,6 +174,15 @@ namespace Marco.Pms.CacheHelper return true; } + public async Task ClearAllEmployeesFromCache() + { + var result = await _collection.DeleteManyAsync(FilterDefinition.Empty); + + if (result.DeletedCount == 0) + return false; + + return true; + } // A private method to handle the one-time setup of the collection's indexes. private async Task InitializeCollectionAsync() diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index b0b1e06..9bb159b 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -811,6 +811,17 @@ namespace Marco.Pms.Services.Helpers _logger.LogWarning("Error occured while deleting Application role {RoleId} from Cache for employee {EmployeeId}: {Error}", roleId, employeeId, ex.Message); } } + public async Task ClearAllEmployees() + { + try + { + var response = await _employeeCache.ClearAllEmployeesFromCache(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occured while deleting all employees from Cache"); + } + } // ------------------------------------ Report Cache --------------------------------------- From b71935dd1f5fa41dd9416381ad44208bd52f90ee Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 17 Jul 2025 17:16:16 +0530 Subject: [PATCH 117/124] Removed commented code from project Cache --- Marco.Pms.CacheHelper/ProjectCache.cs | 46 ++++++--------------------- Marco.Pms.CacheHelper/ReportCache.cs | 5 +-- 2 files changed, 10 insertions(+), 41 deletions(-) diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index df95419..a9ae3af 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -23,6 +23,8 @@ namespace Marco.Pms.CacheHelper _taskCollection = mongoDB.GetCollection("WorkItemDetails"); } + #region=================================================================== Project Cache Helper =================================================================== + public async Task AddProjectDetailsToCache(ProjectMongoDB projectDetails) { await _projectCollection.InsertOneAsync(projectDetails); @@ -36,7 +38,6 @@ namespace Marco.Pms.CacheHelper await _projectCollection.Indexes.CreateOneAsync(indexModel); } - // The method should focus only on inserting data. public async Task AddProjectDetailsListToCache(List projectDetailsList) { // 1. Add a guard clause to avoid an unnecessary database call for an empty list. @@ -49,7 +50,6 @@ namespace Marco.Pms.CacheHelper await _projectCollection.InsertManyAsync(projectDetailsList); await InitializeCollectionAsync(); } - // A private method to handle the one-time setup of the collection's indexes. private async Task InitializeCollectionAsync() { // 1. Define the TTL (Time-To-Live) index on the 'ExpireAt' field. @@ -135,7 +135,9 @@ namespace Marco.Pms.CacheHelper return result.DeletedCount > 0; } - // ------------------------------------------------------- Project InfraStructure ------------------------------------------------------- + #endregion + + #region=================================================================== Project infrastructure Cache Helper =================================================================== public async Task AddBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId) { @@ -161,11 +163,8 @@ namespace Marco.Pms.CacheHelper if (result.MatchedCount == 0) { - //_logger.LogWarning("Project not found while adding building. ProjectId: {ProjectId}", projectId); return; } - - //_logger.LogInfo("Building {BuildingId} added to project {ProjectId}", building.Id, projectId); return; } @@ -191,11 +190,8 @@ namespace Marco.Pms.CacheHelper if (result.MatchedCount == 0) { - //_logger.LogWarning("Project or building not found while adding floor. ProjectId: {ProjectId}, BuildingId: {BuildingId}", projectId, floor.BuildingId); return; } - - //_logger.LogInfo("Floor {FloorId} added to building {BuildingId} in project {ProjectId}", floor.Id, floor.BuildingId, projectId); return; } @@ -225,16 +221,10 @@ namespace Marco.Pms.CacheHelper if (result.MatchedCount == 0) { - //_logger.LogWarning("Project or nested structure not found while adding work area. ProjectId: {ProjectId}, BuildingId: {BuildingId}, FloorId: {FloorId}", projectId, buildingId, workArea.FloorId); return; } - - //_logger.LogInfo("WorkArea {WorkAreaId} added to floor {FloorId} in building {BuildingId}, ProjectId: {ProjectId}", workArea.Id, workArea.FloorId, buildingId, projectId); return; } - - // Fallback case when no valid data was passed - //_logger.LogWarning("No valid infra data provided to add for ProjectId: {ProjectId}", projectId); } public async Task UpdateBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId) { @@ -257,11 +247,9 @@ namespace Marco.Pms.CacheHelper if (result.MatchedCount == 0) { - //_logger.LogWarning("Update failed: Project or Building not found. ProjectId: {ProjectId}, BuildingId: {BuildingId}", projectId, building.Id); return false; } - //_logger.LogInfo("Building {BuildingId} updated successfully in project {ProjectId}", building.Id, projectId); return true; } @@ -282,11 +270,8 @@ namespace Marco.Pms.CacheHelper if (result.MatchedCount == 0) { - //_logger.LogWarning("Update failed: Project or Floor not found. ProjectId: {ProjectId}, BuildingId: {BuildingId}, FloorId: {FloorId}", projectId, floor.BuildingId, floor.Id); return false; } - - //_logger.LogInfo("Floor {FloorId} updated successfully in Building {BuildingId}, ProjectId: {ProjectId}", floor.Id, floor.BuildingId, projectId); return true; } @@ -308,17 +293,10 @@ namespace Marco.Pms.CacheHelper if (result.MatchedCount == 0) { - //_logger.LogWarning("Update failed: Project or WorkArea not found. ProjectId: {ProjectId}, BuildingId: {BuildingId}, FloorId: {FloorId}, WorkAreaId: {WorkAreaId}", - //projectId, buildingId, workArea.FloorId, workArea.Id); return false; } - - //_logger.LogInfo("WorkArea {WorkAreaId} updated successfully in Floor {FloorId}, Building {BuildingId}, ProjectId: {ProjectId}", - //workArea.Id, workArea.FloorId, buildingId, projectId); return true; } - - //_logger.LogWarning("No update performed. Missing or invalid data for ProjectId: {ProjectId}", projectId); return false; } public async Task?> GetBuildingInfraFromCache(Guid projectId) @@ -333,15 +311,6 @@ namespace Marco.Pms.CacheHelper .Project(p => p.Buildings) .FirstOrDefaultAsync(); - //if (buildings == null) - //{ - // _logger.LogWarning("No building infrastructure found for ProjectId: {ProjectId}", projectId); - //} - //else - //{ - // _logger.LogInfo("Fetched {Count} buildings for ProjectId: {ProjectId}", buildings.Count, projectId); - //} - return buildings; } public async Task UpdatePlannedAndCompleteWorksInBuildingFromCache(Guid workAreaId, double plannedWork, double completedWork) @@ -431,8 +400,9 @@ namespace Marco.Pms.CacheHelper return result; } + #endregion - // ------------------------------------------------------- WorkItem ------------------------------------------------------- + #region=================================================================== WorkItem Cache Helper =================================================================== public async Task> GetWorkItemsByWorkAreaIdsFromCache(List workAreaIds) { @@ -517,5 +487,7 @@ namespace Marco.Pms.CacheHelper var result = await _taskCollection.DeleteOneAsync(filter); return result.DeletedCount > 0; } + + #endregion } } diff --git a/Marco.Pms.CacheHelper/ReportCache.cs b/Marco.Pms.CacheHelper/ReportCache.cs index 76009a4..66611a8 100644 --- a/Marco.Pms.CacheHelper/ReportCache.cs +++ b/Marco.Pms.CacheHelper/ReportCache.cs @@ -1,4 +1,3 @@ -using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.MongoDBModels; using Microsoft.Extensions.Configuration; using MongoDB.Driver; @@ -7,12 +6,10 @@ namespace Marco.Pms.CacheHelper { public class ReportCache { - private readonly ApplicationDbContext _context; private readonly IMongoCollection _projectReportCollection; - public ReportCache(ApplicationDbContext context, IConfiguration configuration) + public ReportCache(IConfiguration configuration) { var connectionString = configuration["MongoDB:ConnectionString"]; - _context = context; var mongoUrl = new MongoUrl(connectionString); var client = new MongoClient(mongoUrl); // Your MongoDB connection string var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name From 7b2a3887deb27a1cc8186a05451dd44454f5aa0e Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 17 Jul 2025 17:36:58 +0530 Subject: [PATCH 118/124] Solved the rebase issues --- .../Controllers/DashboardController.cs | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/Marco.Pms.Services/Controllers/DashboardController.cs b/Marco.Pms.Services/Controllers/DashboardController.cs index 0e01717..108a3ec 100644 --- a/Marco.Pms.Services/Controllers/DashboardController.cs +++ b/Marco.Pms.Services/Controllers/DashboardController.cs @@ -6,6 +6,7 @@ using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.DashBoard; using Marco.Pms.Services.Service; +using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; @@ -21,15 +22,15 @@ namespace Marco.Pms.Services.Controllers { private readonly ApplicationDbContext _context; private readonly UserHelper _userHelper; - private readonly ProjectsHelper _projectsHelper; + private readonly IProjectServices _projectServices; private readonly ILoggingService _logger; private readonly PermissionServices _permissionServices; public static readonly Guid ActiveId = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731"); - public DashboardController(ApplicationDbContext context, UserHelper userHelper, ProjectsHelper projectsHelper, ILoggingService logger, PermissionServices permissionServices) + public DashboardController(ApplicationDbContext context, UserHelper userHelper, IProjectServices projectServices, ILoggingService logger, PermissionServices permissionServices) { _context = context; _userHelper = userHelper; - _projectsHelper = projectsHelper; + _projectServices = projectServices; _logger = logger; _permissionServices = permissionServices; } @@ -182,11 +183,13 @@ namespace Marco.Pms.Services.Controllers // --- Step 1: Get the list of projects the user can access --- // This query is more efficient as it only selects the IDs needed. - var projects = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); - var accessibleActiveProjectIds = projects - .Where(p => p.ProjectStatusId == ActiveId) + var projects = await _projectServices.GetMyProjectIdsAsync(tenantId, loggedInEmployee); + + var accessibleActiveProjectIds = await _context.Projects + .Where(p => p.ProjectStatusId == ActiveId && projects.Contains(p.Id)) .Select(p => p.Id) - .ToList(); + .ToListAsync(); + if (!accessibleActiveProjectIds.Any()) { _logger.LogInfo("User {UserId} has no accessible active projects.", loggedInEmployee.Id); @@ -199,7 +202,7 @@ namespace Marco.Pms.Services.Controllers if (projectId.HasValue) { // Security Check: Ensure the requested project is in the user's accessible list. - var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value.ToString()); + var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value); if (!hasPermission) { _logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId} (not active or not accessible).", loggedInEmployee.Id, projectId.Value); @@ -250,7 +253,7 @@ namespace Marco.Pms.Services.Controllers } catch (Exception ex) { - _logger.LogError("An unexpected error occurred in GetTotalEmployees for projectId {ProjectId} \n {Error}", projectId ?? Guid.Empty, ex.Message); + _logger.LogError(ex, "An unexpected error occurred in GetTotalEmployees for projectId {ProjectId}", projectId ?? Guid.Empty); return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred.", null, 500)); } } @@ -281,7 +284,7 @@ namespace Marco.Pms.Services.Controllers // --- Logic for a SINGLE Project --- // 2a. Security Check: Verify permission for the specific project. - var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value.ToString()); + var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value); if (!hasPermission) { _logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, projectId.Value); @@ -301,8 +304,8 @@ namespace Marco.Pms.Services.Controllers // --- Logic for ALL Accessible Projects --- // 2c. Get a list of all projects the user is allowed to see. - var accessibleProject = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); - var accessibleProjectIds = accessibleProject.Select(p => p.Id).ToList(); + var accessibleProjectIds = await _projectServices.GetMyProjectIdsAsync(tenantId, loggedInEmployee); + if (!accessibleProjectIds.Any()) { _logger.LogInfo("User {UserId} has no accessible projects.", loggedInEmployee.Id); @@ -341,7 +344,7 @@ namespace Marco.Pms.Services.Controllers } catch (Exception ex) { - _logger.LogError("An unexpected error occurred in GetTotalTasks for projectId {ProjectId} \n {Error}", projectId ?? Guid.Empty, ex.Message); + _logger.LogError(ex, "An unexpected error occurred in GetTotalTasks for projectId {ProjectId}", projectId ?? Guid.Empty); return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred.", null, 500)); } } From 328c6ec4e3fbbd90fb976b25ca7296d6915815e0 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Fri, 18 Jul 2025 16:03:53 +0530 Subject: [PATCH 119/124] When sending the report before data has been taken from cache not from database change it to both --- Marco.Pms.CacheHelper/ProjectCache.cs | 13 ++++ .../Controllers/ReportController.cs | 78 ------------------- .../Helpers/CacheUpdateHelper.cs | 25 ++++-- Marco.Pms.Services/Helpers/ReportHelper.cs | 10 +-- 4 files changed, 37 insertions(+), 89 deletions(-) diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index a9ae3af..10eb623 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -110,6 +110,19 @@ namespace Marco.Pms.CacheHelper return project; } + public async Task GetProjectDetailsWithBuildingsFromCache(Guid projectId) + { + + // Build filter and projection to exclude large 'Buildings' list + var filter = Builders.Filter.Eq(p => p.Id, projectId.ToString()); + + // Perform query + var project = await _projectCollection + .Find(filter) + .FirstOrDefaultAsync(); + + return project; + } public async Task> GetProjectDetailsListFromCache(List projectIds) { List stringProjectIds = projectIds.Select(p => p.ToString()).ToList(); diff --git a/Marco.Pms.Services/Controllers/ReportController.cs b/Marco.Pms.Services/Controllers/ReportController.cs index 87382d7..a46c391 100644 --- a/Marco.Pms.Services/Controllers/ReportController.cs +++ b/Marco.Pms.Services/Controllers/ReportController.cs @@ -370,84 +370,6 @@ namespace Marco.Pms.Services.Controllers 200)); } - //[HttpPost("add-report-mail1")] - //public async Task StoreProjectStatistics1() - //{ - - // Guid tenantId = _userHelper.GetTenantId(); - - // // Use AsNoTracking() for read-only queries to improve performance - // List mailDetails = await _context.MailDetails - // .AsNoTracking() - // .Include(m => m.MailBody) - // .Where(m => m.TenantId == tenantId) - // .ToListAsync(); - - // var groupedMails = mailDetails - // .GroupBy(m => new { m.ProjectId, m.MailListId }) - // .Select(g => new - // { - // ProjectId = g.Key.ProjectId, - // MailListId = g.Key.MailListId, - // Recipients = g.Select(m => m.Recipient).Distinct().ToList(), - // MailBody = g.FirstOrDefault()?.MailBody?.Body ?? "", - // Subject = g.FirstOrDefault()?.MailBody?.Subject ?? string.Empty, - // }) - // .ToList(); - // foreach (var groupMail in groupedMails) - // { - // var projectId = groupMail.ProjectId; - // var body = groupMail.MailBody; - // var subject = groupMail.Subject; - // var receivers = groupMail.Recipients; - // if (projectId == Guid.Empty) - // { - // _logger.LogError("Provided empty project ID while fetching project report."); - // return NotFound(ApiResponse.ErrorResponse("Provided empty Project ID.", "Provided empty Project ID.", 400)); - // } - - - // var statisticReport = await _reportHelper.GetDailyProjectReport(projectId, tenantId); - - // if (statisticReport == null) - // { - // _logger.LogWarning("User attempted to fetch project progress for project ID {ProjectId} but not found.", projectId); - // return NotFound(ApiResponse.ErrorResponse("Project not found.", "Project not found.", 404)); - // } - // var date = statisticReport.Date.ToString("dd-MMM-yyyy", CultureInfo.InvariantCulture); - - // // Send Email - // var emailBody = await _emailSender.SendProjectStatisticsEmail(new List(), body, subject, statisticReport); - // var subjectReplacements = new Dictionary - // { - // {"DATE", date }, - // {"PROJECT_NAME", statisticReport.ProjectName} - // }; - // foreach (var item in subjectReplacements) - // { - // subject = subject.Replace($"{{{{{item.Key}}}}}", item.Value); - // } - // string env = _configuration["environment:Title"] ?? string.Empty; - // if (string.IsNullOrWhiteSpace(env)) - // { - // subject = $"{subject}"; - // } - // else - // { - // subject = $"({env}) {subject}"; - // } - // var mail = new ProjectReportEmailMongoDB - // { - // IsSent = false, - // Body = emailBody, - // Receivers = receivers, - // Subject = subject, - // }; - // await _cache.AddProjectReportMail(mail); - // } - // return Ok(ApiResponse.SuccessResponse("Project Report Mail is stored in MongoDB", "Project Report Mail is stored in MongoDB", 200)); - //} - [HttpPost("add-report-mail")] public async Task StoreProjectStatistics() { diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 9bb159b..d942ab1 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -200,7 +200,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occurred while adding project {ProjectId} to Cache: {Error}", project.Id, ex.Message); + _logger.LogError(ex, "Error occurred while adding project {ProjectId} to Cache", project.Id); } } public async Task AddProjectDetailsList(List projects) @@ -415,7 +415,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occurred while adding project list to Cache: {Error}", ex.Message); + _logger.LogError(ex, "Error occurred while adding project list to Cache"); } } public async Task UpdateProjectDetailsOnly(Project project) @@ -429,7 +429,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while updating project {ProjectId} to Cache: {Error}", project.Id, ex.Message); + _logger.LogError(ex, "Error occured while updating project {ProjectId} to Cache", project.Id); return false; } } @@ -442,7 +442,20 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while getting project {ProjectId} to Cache: {Error}", ex.Message); + _logger.LogError(ex, "Error occured while getting project {ProjectId} to Cache"); + return null; + } + } + public async Task GetProjectDetailsWithBuildings(Guid projectId) + { + try + { + var response = await _projectCache.GetProjectDetailsWithBuildingsFromCache(projectId); + return response; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occured while getting project {ProjectId} to Cache"); return null; } } @@ -462,7 +475,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while getting list of project details from to Cache: {Error}", ex.Message); + _logger.LogError(ex, "Error occured while getting list of project details from to Cache"); return null; } } @@ -474,7 +487,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _logger.LogWarning("Error occured while deleting project from to Cache: {Error}", ex.Message); + _logger.LogError(ex, "Error occured while deleting project from to Cache"); } } diff --git a/Marco.Pms.Services/Helpers/ReportHelper.cs b/Marco.Pms.Services/Helpers/ReportHelper.cs index 4ec9453..35dcf8b 100644 --- a/Marco.Pms.Services/Helpers/ReportHelper.cs +++ b/Marco.Pms.Services/Helpers/ReportHelper.cs @@ -28,7 +28,7 @@ namespace Marco.Pms.Services.Helpers { // await _cache.GetBuildingAndFloorByWorkAreaId(); DateTime reportDate = DateTime.UtcNow.AddDays(-1).Date; - var project = await _cache.GetProjectDetails(projectId); + var project = await _cache.GetProjectDetailsWithBuildings(projectId); if (project == null) { var projectSQL = await _context.Projects @@ -91,7 +91,7 @@ namespace Marco.Pms.Services.Helpers BuildingName = b.BuildingName, Description = b.Description }).ToList(); - if (buildings == null) + if (!buildings.Any()) { buildings = await _context.Buildings .Where(b => b.ProjectId == projectId) @@ -113,7 +113,7 @@ namespace Marco.Pms.Services.Helpers BuildingId = f.BuildingId, FloorName = f.FloorName })).ToList(); - if (floors == null) + if (!floors.Any()) { var buildingIds = buildings.Select(b => Guid.Parse(b.Id)).ToList(); floors = await _context.Floor @@ -131,7 +131,7 @@ namespace Marco.Pms.Services.Helpers areas = project.Buildings .SelectMany(b => b.Floors) .SelectMany(f => f.WorkAreas).ToList(); - if (areas == null) + if (!areas.Any()) { var floorIds = floors.Select(f => Guid.Parse(f.Id)).ToList(); areas = await _context.WorkAreas @@ -149,7 +149,7 @@ namespace Marco.Pms.Services.Helpers // fetch Work Items workItems = await _cache.GetWorkItemsByWorkAreaIds(areaIds); - if (workItems == null) + if (workItems == null || !workItems.Any()) { workItems = await _context.WorkItems .Include(w => w.ActivityMaster) From 468bfdf635af0ffcb89abc024813cb00df8cd770 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 23 Jul 2025 18:09:52 +0530 Subject: [PATCH 120/124] Made MPIN to be 4 digit --- Marco.Pms.Services/Controllers/AuthController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Marco.Pms.Services/Controllers/AuthController.cs b/Marco.Pms.Services/Controllers/AuthController.cs index 429a38b..67dd74a 100644 --- a/Marco.Pms.Services/Controllers/AuthController.cs +++ b/Marco.Pms.Services/Controllers/AuthController.cs @@ -750,7 +750,7 @@ namespace MarcoBMS.Services.Controllers .FirstOrDefaultAsync(e => e.Id == generateMPINDto.EmployeeId && e.TenantId == tenantId); // Validate employee and MPIN input - if (requestEmployee == null || string.IsNullOrWhiteSpace(generateMPINDto.MPIN) || generateMPINDto.MPIN.Length != 6 || !generateMPINDto.MPIN.All(char.IsDigit)) + if (requestEmployee == null || string.IsNullOrWhiteSpace(generateMPINDto.MPIN) || generateMPINDto.MPIN.Length != 4 || !generateMPINDto.MPIN.All(char.IsDigit)) { _logger.LogWarning("Employee {EmployeeId} provided invalid information to generate MPIN", loggedInEmployee.Id); return BadRequest(ApiResponse.ErrorResponse("Provided invalid information", "Provided invalid information", 400)); From 346f2cebcb34e2335ce87535c699a6e6ee2d8fe6 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 23 Jul 2025 18:11:10 +0530 Subject: [PATCH 121/124] Changed the function from GetProjectDetailsAsync to GetProjectDetailsOldAsync --- Marco.Pms.Services/Controllers/ProjectController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 796fd39..2c03d69 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -135,7 +135,7 @@ namespace MarcoBMS.Services.Controllers var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var response = await _projectServices.GetProjectDetailsAsync(id, tenantId, loggedInEmployee); + var response = await _projectServices.GetProjectDetailsOldAsync(id, tenantId, loggedInEmployee); return StatusCode(response.StatusCode, response); } From 5926ec6655633254f3d6656d4ac8133b45be631a Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 23 Jul 2025 12:47:22 +0000 Subject: [PATCH 122/124] will be pushed in another branch revert Changed the function from GetProjectDetailsAsync to GetProjectDetailsOldAsync --- Marco.Pms.Services/Controllers/ProjectController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 2c03d69..796fd39 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -135,7 +135,7 @@ namespace MarcoBMS.Services.Controllers var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var response = await _projectServices.GetProjectDetailsOldAsync(id, tenantId, loggedInEmployee); + var response = await _projectServices.GetProjectDetailsAsync(id, tenantId, loggedInEmployee); return StatusCode(response.StatusCode, response); } From 8cc6584e7cbd6975256f0cf93f818e31d319e8c5 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 23 Jul 2025 12:47:45 +0000 Subject: [PATCH 123/124] will be pushed in another branch revert Made MPIN to be 4 digit --- Marco.Pms.Services/Controllers/AuthController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Marco.Pms.Services/Controllers/AuthController.cs b/Marco.Pms.Services/Controllers/AuthController.cs index 67dd74a..429a38b 100644 --- a/Marco.Pms.Services/Controllers/AuthController.cs +++ b/Marco.Pms.Services/Controllers/AuthController.cs @@ -750,7 +750,7 @@ namespace MarcoBMS.Services.Controllers .FirstOrDefaultAsync(e => e.Id == generateMPINDto.EmployeeId && e.TenantId == tenantId); // Validate employee and MPIN input - if (requestEmployee == null || string.IsNullOrWhiteSpace(generateMPINDto.MPIN) || generateMPINDto.MPIN.Length != 4 || !generateMPINDto.MPIN.All(char.IsDigit)) + if (requestEmployee == null || string.IsNullOrWhiteSpace(generateMPINDto.MPIN) || generateMPINDto.MPIN.Length != 6 || !generateMPINDto.MPIN.All(char.IsDigit)) { _logger.LogWarning("Employee {EmployeeId} provided invalid information to generate MPIN", loggedInEmployee.Id); return BadRequest(ApiResponse.ErrorResponse("Provided invalid information", "Provided invalid information", 400)); From 62956d6b12e14e91e2261754f8e24a423960f06d Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Thu, 24 Jul 2025 17:28:27 +0530 Subject: [PATCH 124/124] solved the mearge error --- Marco.Pms.Services/Helpers/DirectoryHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Marco.Pms.Services/Helpers/DirectoryHelper.cs b/Marco.Pms.Services/Helpers/DirectoryHelper.cs index 172ded0..cb169a1 100644 --- a/Marco.Pms.Services/Helpers/DirectoryHelper.cs +++ b/Marco.Pms.Services/Helpers/DirectoryHelper.cs @@ -740,7 +740,7 @@ namespace Marco.Pms.Services.Helpers { Guid tenantId = _userHelper.GetTenantId(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var hasAdminPermission = await _permissionServices.HasPermission(directoryAdmin, LoggedInEmployee.Id); + var hasAdminPermission = await _permissionServices.HasPermission(PermissionsMaster.DirectoryAdmin, LoggedInEmployee.Id); if (id != Guid.Empty) { Contact? contact = await _context.Contacts.Include(c => c.ContactCategory).Include(c => c.CreatedBy).FirstOrDefaultAsync(c => c.Id == id && c.IsActive);