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