Compare commits

..

No commits in common. "8bb8b3643f72cdf60da3c1bdef59326e9e15f504" and "852b079428889ec1165de7e50d3e13e66353ff01" have entirely different histories.

19 changed files with 573 additions and 1481 deletions

View File

@ -24,14 +24,132 @@ namespace Marco.Pms.CacheHelper
_projetCollection = mongoDB.GetCollection<ProjectMongoDB>("ProjectDetails");
_taskCollection = mongoDB.GetCollection<WorkItemMongoDB>("WorkItemDetails");
}
public async Task AddProjectDetailsToCache(Project project)
{
//_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;
public async Task AddProjectDetailsToCache(ProjectMongoDB projectDetails)
{
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)
@ -100,7 +218,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);
@ -111,9 +229,6 @@ 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();

View File

@ -1,45 +0,0 @@
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);
}
}
}

View File

@ -1,16 +0,0 @@
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;
}
}

View File

@ -1,8 +1,8 @@
using Marco.Pms.DataAccess.Data;
using System.Globalization;
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,7 +16,6 @@ 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
@ -62,13 +61,7 @@ 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)
{
@ -146,9 +139,9 @@ namespace MarcoBMS.Services.Controllers
{
Guid TenantId = GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
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);
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());
if (!hasProjectPermission)
{
@ -262,9 +255,9 @@ namespace MarcoBMS.Services.Controllers
{
Guid TenantId = GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
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);
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());
if (!hasProjectPermission)
{
@ -368,7 +361,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);
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId.ToString());
if (!hasProjectPermission)
{
@ -378,6 +371,7 @@ 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();

View File

@ -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);
bool hasAssigned = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.ToString());
if (!hasAssigned)
{

View File

@ -1,4 +1,6 @@
using Marco.Pms.DataAccess.Data;
using System.Data;
using System.Net;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Dtos.Attendance;
using Marco.Pms.Model.Dtos.Employees;
using Marco.Pms.Model.Employees;
@ -16,8 +18,6 @@ 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,7 +119,8 @@ namespace MarcoBMS.Services.Controllers
loggedInEmployee.Id, projectid ?? Guid.Empty, ShowInactive);
// Step 3: Fetch project access and permissions
var projectIds = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee);
List<Project> projects = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee);
var projectIds = projects.Select(p => p.Id).ToList();
var hasViewAllEmployeesPermission = await _permission.HasPermission(PermissionsMaster.ViewAllEmployees, loggedInEmployee.Id);
var hasViewTeamMembersPermission = await _permission.HasPermission(PermissionsMaster.ViewTeamMembers, loggedInEmployee.Id);

View File

@ -1,4 +1,5 @@
using Marco.Pms.DataAccess.Data;
using System.Text.Json;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Activities;
using Marco.Pms.Model.Dtos.DocumentManager;
using Marco.Pms.Model.Employees;
@ -12,7 +13,6 @@ 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);
var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.ToString());
if (!hasPermission)
{
_logger.LogWarning("[GetImageList] Access denied for EmployeeId: {EmployeeId} on ProjectId: {ProjectId}", loggedInEmployee.Id, projectId);

View File

@ -1,10 +1,10 @@
using AutoMapper;
using Marco.Pms.DataAccess.Data;
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,12 +36,16 @@ namespace MarcoBMS.Services.Controllers
private readonly IHubContext<MarcoHub> _signalR;
private readonly PermissionServices _permission;
private readonly CacheUpdateHelper _cache;
private readonly IMapper _mapper;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly Guid ViewProjects;
private readonly Guid ManageProject;
private readonly Guid ViewInfra;
private readonly Guid ManageInfra;
private readonly Guid tenantId;
public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, ProjectsHelper projectHelper,
IHubContext<MarcoHub> signalR, PermissionServices permission, CacheUpdateHelper cache, IMapper mapper)
IHubContext<MarcoHub> signalR, PermissionServices permission, CacheUpdateHelper cache, IServiceScopeFactory serviceScopeFactory)
{
_context = context;
_userHelper = userHelper;
@ -51,12 +55,16 @@ namespace MarcoBMS.Services.Controllers
_signalR = signalR;
_cache = cache;
_permission = permission;
_mapper = mapper;
ViewProjects = Guid.Parse("6ea44136-987e-44ba-9e5d-1cf8f5837ebc");
ManageProject = Guid.Parse("172fc9b6-755b-4f62-ab26-55c34a330614");
ViewInfra = Guid.Parse("8d7cc6e3-9147-41f7-aaa7-fa507e450bd4");
ManageInfra = Guid.Parse("f2aee20a-b754-4537-8166-f9507b44585b");
tenantId = _userHelper.GetTenantId();
_serviceScopeFactory = serviceScopeFactory;
}
[HttpGet("list/basic1")]
public async Task<IActionResult> GetAllProjects1()
[HttpGet("list/basic")]
public async Task<IActionResult> GetAllProjects()
{
if (!ModelState.IsValid)
{
@ -76,113 +84,31 @@ 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<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>();
List<Project> projects = await _projectsHelper.GetMyProjects(tenantId, LoggedInEmployee);
//}
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();
}
// 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());
//}
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()
{
@ -213,63 +139,39 @@ 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 = new List<Project>();
///
List<Project> projects = await _projectsHelper.GetMyProjects(tenantId, LoggedInEmployee);
List<ProjectListVM> response = new List<ProjectListVM>();
List<Guid> projectIds = await _projectsHelper.GetMyProjects(tenantId, LoggedInEmployee);
var projectsDetails = await _cache.GetProjectDetailsList(projectIds);
if (projectsDetails == null)
foreach (var project in projects)
{
List<Project> projects = await _context.Projects.Where(p => projectIds.Contains(p.Id)).ToListAsync();
var result = project.ToProjectListVMFromProject();
var team = await _context.ProjectAllocations.Where(p => p.TenantId == tenantId && p.ProjectId == project.Id && p.IsActive == true).ToListAsync();
var teams = await _context.ProjectAllocations.Where(p => p.TenantId == tenantId && projectIds.Contains(p.ProjectId) && p.IsActive == true).ToListAsync();
result.TeamSize = team.Count();
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<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<Floor> floors = await _context.Floor.Where(f => idList.Contains(f.BuildingId) && f.TenantId == tenantId).ToListAsync();
idList = floors.Select(f => f.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<WorkArea> workAreas = await _context.WorkAreas.Where(a => idList.Contains(a.FloorId) && a.TenantId == tenantId).ToListAsync();
idList = workAreas.Select(a => a.Id).ToList();
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)
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)
{
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);
completedTask += workItem.CompletedWork;
plannedTask += workItem.PlannedWork;
}
}
else
{
response = projectsDetails.Select(p => _mapper.Map<ProjectListVM>(p)).ToList();
result.PlannedWork = plannedTask;
result.CompletedWork = completedTask;
response.Add(result);
}
return Ok(ApiResponse<object>.SuccessResponse(response, "Success.", 200));
@ -313,7 +215,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(PermissionsMaster.ViewProject, loggedInEmployee.Id);
var hasViewProjectPermission = await _permission.HasPermission(ViewProjects, loggedInEmployee.Id);
if (!hasViewProjectPermission)
{
_logger.LogWarning("ViewProjects permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id);
@ -321,7 +223,7 @@ namespace MarcoBMS.Services.Controllers
}
// Step 4: Check permission for this specific project
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, id);
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, id.ToString());
if (!hasProjectPermission)
{
_logger.LogWarning("Project-specific access denied. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployee.Id, id);
@ -336,9 +238,7 @@ namespace MarcoBMS.Services.Controllers
var project = await _context.Projects
.Include(c => c.ProjectStatus)
.FirstOrDefaultAsync(c => c.TenantId == tenantId && c.Id == id);
projectVM = _mapper.Map<ProjectVM>(project);
projectVM = GetProjectViewModel(project);
if (project != null)
{
await _cache.AddProjectDetails(project);
@ -346,28 +246,23 @@ namespace MarcoBMS.Services.Controllers
}
else
{
projectVM = _mapper.Map<ProjectVM>(projectDetails);
if (projectVM.ProjectStatus != null)
projectVM = new ProjectVM
{
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,
//};
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)
@ -382,6 +277,25 @@ 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)
{
@ -556,7 +470,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(PermissionsMaster.ManageProject);
Task cacheClearListTask = _cache.ClearAllProjectIdsByPermissionId(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)
@ -848,7 +762,7 @@ namespace MarcoBMS.Services.Controllers
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Step 2: Check project-specific permission
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId);
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.ToString());
if (!hasProjectPermission)
{
_logger.LogWarning("Project access denied for EmployeeId: {EmployeeId} on ProjectId: {ProjectId}", loggedInEmployee.Id, projectId);
@ -856,7 +770,7 @@ namespace MarcoBMS.Services.Controllers
}
// Step 3: Check 'ViewInfra' permission
var hasViewInfraPermission = await _permission.HasPermission(PermissionsMaster.ViewProjectInfra, loggedInEmployee.Id);
var hasViewInfraPermission = await _permission.HasPermission(ViewInfra, loggedInEmployee.Id);
if (!hasViewInfraPermission)
{
_logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id);
@ -969,7 +883,7 @@ namespace MarcoBMS.Services.Controllers
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Step 2: Check if the employee has ViewInfra permission
var hasViewInfraPermission = await _permission.HasPermission(PermissionsMaster.ViewProjectInfra, loggedInEmployee.Id);
var hasViewInfraPermission = await _permission.HasPermission(ViewInfra, loggedInEmployee.Id);
if (!hasViewInfraPermission)
{
_logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id);

View File

@ -1,19 +1,16 @@
using Marco.Pms.DataAccess.Data;
using System.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
{
@ -28,11 +25,7 @@ namespace Marco.Pms.Services.Controllers
private readonly UserHelper _userHelper;
private readonly IWebHostEnvironment _env;
private readonly 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)
public ReportController(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, UserHelper userHelper, IWebHostEnvironment env, ReportHelper reportHelper)
{
_context = context;
_emailSender = emailSender;
@ -40,122 +33,27 @@ namespace Marco.Pms.Services.Controllers
_userHelper = userHelper;
_env = env;
_reportHelper = reportHelper;
_configuration = configuration;
_cache = cache;
_serviceScopeFactory = serviceScopeFactory;
}
/// <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
[HttpPost("set-mail")]
public async Task<IActionResult> AddMailDetails([FromBody] MailDetailsDto mailDetailsDto)
{
// 1. Get Tenant ID and Basic Authorization Check
Guid tenantId = _userHelper.GetTenantId();
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
MailDetails mailDetails = new MailDetails
{
ProjectId = mailDetailsDto.ProjectId,
Recipient = mailDetailsDto.Recipient,
Schedule = mailDetailsDto.Schedule,
MailListId = mailDetailsDto.MailListId,
TenantId = tenantId,
TenantId = tenantId
};
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));
}
_context.MailDetails.Add(mailDetails);
await _context.SaveChangesAsync();
return Ok("Success");
}
[HttpPost("mail-template1")]
public async Task<IActionResult> AddMailTemplate1([FromBody] MailTemeplateDto mailTemeplateDto)
[HttpPost("mail-template")]
public async Task<IActionResult> AddMailTemplate([FromBody] MailTemeplateDto mailTemeplateDto)
{
Guid tenantId = _userHelper.GetTenantId();
if (string.IsNullOrWhiteSpace(mailTemeplateDto.Body) && string.IsNullOrWhiteSpace(mailTemeplateDto.Title))
@ -182,376 +80,116 @@ 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();
// 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
// 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)
.GroupBy(m => new { m.ProjectId, m.MailListId })
.Select(g => new
{
ProjectId = g.Key.ProjectId,
Recipients = g.Select(m => m.Recipient).Distinct().ToList(),
// 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()
})
.ToListAsync();
if (!projectMailGroups.Any())
{
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 groupedMails = mailDetails
.GroupBy(m => new { m.ProjectId, m.MailListId })
.Select(g => new
{
// 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>();
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();
try
{
// Ensure MailInfo and ProjectId are valid before proceeding
if (mailGroup.MailInfo == null || mailGroup.ProjectId == Guid.Empty)
{
Interlocked.Increment(ref invalidIdCount);
return;
}
var semaphore = new SemaphoreSlim(1);
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);
}
// Using Task.WhenAll to send reports concurrently for better performance
var sendTasks = groupedMails.Select(async mailDetail =>
{
await semaphore.WaitAsync();
try
{
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();
}
}).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(
"Project report sending complete for tenant {TenantId}. Success: {SuccessCount}, Not Found: {NotFoundCount}, Invalid ID: {InvalidIdCount}, Failures: {FailureCount}",
tenantId, successCount, notFoundCount, invalidIdCount, failureCount);
"Emails of project reports sent for tenant {TenantId}. Successfully sent: {SuccessCount}, Projects not found: {NotFoundCount}, Invalid IDs: {InvalidIdsCount}",
tenantId, successCount, notFoundCount, invalidIdCount);
return Ok(ApiResponse<object>.SuccessResponse(
new { successCount, notFoundCount, invalidIdCount, failureCount },
summaryMessage,
new { },
$"Reports sent successfully: {successCount}. Projects not found: {notFoundCount}. Invalid IDs: {invalidIdCount}.",
200));
}
//[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()
/// <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)
{
Guid tenantId = _userHelper.GetTenantId();
// 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())
if (projectId == Guid.Empty)
{
_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));
_logger.LogError("Provided empty project ID while fetching project report.");
return ApiResponse<object>.ErrorResponse("Provided empty Project ID.", "Provided empty Project ID.", 400);
}
string env = _configuration["environment:Title"] ?? string.Empty;
// 2. Process each group concurrently, but with isolated DBContexts.
var processingTasks = projectMailGroups.Select(async group =>
var statisticReport = await _reportHelper.GetDailyProjectReport(projectId, tenantId);
if (statisticReport == null)
{
// 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
// The rest of the logic is the same, but now it's thread-safe.
try
{
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));
_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);
}
return Ok(ApiResponse<object>.SuccessResponse(mailList, "Fetched list of mail body successfully", 200));
// 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
{
ProjectId = projectId,
EmailId = recipientEmail,
Body = emailBody,
EmployeeId = employee.Id,
TimeStamp = DateTime.UtcNow,
TenantId = tenantId
});
}
_context.MailLogs.AddRange(mailLogs);
await _context.SaveChangesAsync();
return ApiResponse<object>.SuccessResponse(statisticReport, "Email sent successfully", 200);
}
}
}

View File

@ -1,9 +1,7 @@
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
@ -12,407 +10,25 @@ 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, ReportCache reportCache, ILoggingService logger,
IDbContextFactory<ApplicationDbContext> dbContextFactory)
public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache, ILoggingService logger)
{
_projectCache = projectCache;
_employeeCache = employeeCache;
_reportCache = reportCache;
_logger = logger;
_dbContextFactory = dbContextFactory;
}
// ------------------------------------ Project Details Cache ---------------------------------------
// Assuming you have access to an IDbContextFactory<YourDbContext> as _dbContextFactory
// This is crucial for safe parallel database operations.
// ------------------------------------ Project Details and Infrastructure Cache ---------------------------------------
public async Task AddProjectDetails(Project project)
{
// --- Step 1: Fetch all required data from the database in parallel ---
// Each task uses its own DbContext instance to avoid concurrency issues.
var statusTask = Task.Run(async () =>
{
using var context = _dbContextFactory.CreateDbContext();
return await context.StatusMasters
.AsNoTracking()
.Where(s => s.Id == project.ProjectStatusId)
.Select(s => new { s.Id, s.Status }) // Projection
.FirstOrDefaultAsync();
});
var teamSizeTask = Task.Run(async () =>
{
using var context = _dbContextFactory.CreateDbContext();
return await context.ProjectAllocations
.AsNoTracking()
.CountAsync(pa => pa.ProjectId == project.Id && pa.IsActive); // Server-side count is efficient
});
// This task fetches the entire infrastructure hierarchy and performs aggregations in the database.
var infrastructureTask = Task.Run(async () =>
{
using var context = _dbContextFactory.CreateDbContext();
// 1. Fetch all hierarchical data using projections.
// This is still a chain, but it's inside one task and much faster due to projections.
var buildings = await context.Buildings.AsNoTracking()
.Where(b => b.ProjectId == project.Id)
.Select(b => new { b.Id, b.ProjectId, b.Name, b.Description })
.ToListAsync();
var buildingIds = buildings.Select(b => b.Id).ToList();
var floors = await context.Floor.AsNoTracking()
.Where(f => buildingIds.Contains(f.BuildingId))
.Select(f => new { f.Id, f.BuildingId, f.FloorName })
.ToListAsync();
var floorIds = floors.Select(f => f.Id).ToList();
var workAreas = await context.WorkAreas.AsNoTracking()
.Where(wa => floorIds.Contains(wa.FloorId))
.Select(wa => new { wa.Id, wa.FloorId, wa.AreaName })
.ToListAsync();
var workAreaIds = workAreas.Select(wa => wa.Id).ToList();
// 2. THE KEY OPTIMIZATION: Aggregate work items in the database.
var workSummaries = await context.WorkItems.AsNoTracking()
.Where(wi => workAreaIds.Contains(wi.WorkAreaId))
.GroupBy(wi => wi.WorkAreaId) // Group by parent on the DB server
.Select(g => new // Let the DB do the SUM
{
WorkAreaId = g.Key,
PlannedWork = g.Sum(i => i.PlannedWork),
CompletedWork = g.Sum(i => i.CompletedWork)
})
.ToDictionaryAsync(x => x.WorkAreaId); // Return a ready-to-use dictionary
return (buildings, floors, workAreas, workSummaries);
});
// Wait for all parallel database operations to complete.
await Task.WhenAll(statusTask, teamSizeTask, infrastructureTask);
// Get the results from the completed tasks.
var status = await statusTask;
var teamSize = await teamSizeTask;
var (allBuildings, allFloors, allWorkAreas, workSummariesByWorkAreaId) = await infrastructureTask;
// --- Step 2: Process the fetched data and build the MongoDB model ---
var projectDetails = new ProjectMongoDB
{
Id = project.Id.ToString(),
Name = project.Name,
ShortName = project.ShortName,
ProjectAddress = project.ProjectAddress,
StartDate = project.StartDate,
EndDate = project.EndDate,
ContactPerson = project.ContactPerson,
TeamSize = teamSize
};
projectDetails.ProjectStatus = new StatusMasterMongoDB
{
Id = status?.Id.ToString(),
Status = status?.Status
};
// Use fast in-memory lookups instead of .Where() in loops.
var floorsByBuildingId = allFloors.ToLookup(f => f.BuildingId);
var workAreasByFloorId = allWorkAreas.ToLookup(wa => wa.FloorId);
double totalPlannedWork = 0, totalCompletedWork = 0;
var buildingMongoList = new List<BuildingMongoDB>();
foreach (var building in allBuildings)
{
double buildingPlanned = 0, buildingCompleted = 0;
var floorMongoList = new List<FloorMongoDB>();
foreach (var floor in floorsByBuildingId[building.Id]) // Fast lookup
{
double floorPlanned = 0, floorCompleted = 0;
var workAreaMongoList = new List<WorkAreaMongoDB>();
foreach (var wa in workAreasByFloorId[floor.Id]) // Fast lookup
{
// Get the pre-calculated summary from the dictionary. O(1) operation.
workSummariesByWorkAreaId.TryGetValue(wa.Id, out var summary);
var waPlanned = summary?.PlannedWork ?? 0;
var waCompleted = summary?.CompletedWork ?? 0;
workAreaMongoList.Add(new WorkAreaMongoDB
{
Id = wa.Id.ToString(),
FloorId = wa.FloorId.ToString(),
AreaName = wa.AreaName,
PlannedWork = waPlanned,
CompletedWork = waCompleted
});
floorPlanned += waPlanned;
floorCompleted += waCompleted;
}
floorMongoList.Add(new FloorMongoDB
{
Id = floor.Id.ToString(),
BuildingId = floor.BuildingId.ToString(),
FloorName = floor.FloorName,
PlannedWork = floorPlanned,
CompletedWork = floorCompleted,
WorkAreas = workAreaMongoList
});
buildingPlanned += floorPlanned;
buildingCompleted += floorCompleted;
}
buildingMongoList.Add(new BuildingMongoDB
{
Id = building.Id.ToString(),
ProjectId = building.ProjectId.ToString(),
BuildingName = building.Name,
Description = building.Description,
PlannedWork = buildingPlanned,
CompletedWork = buildingCompleted,
Floors = floorMongoList
});
totalPlannedWork += buildingPlanned;
totalCompletedWork += buildingCompleted;
}
projectDetails.Buildings = buildingMongoList;
projectDetails.PlannedWork = totalPlannedWork;
projectDetails.CompletedWork = totalCompletedWork;
try
{
await _projectCache.AddProjectDetailsToCache(projectDetails);
await _projectCache.AddProjectDetailsToCache(project);
}
catch (Exception ex)
{
_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);
_logger.LogWarning("Error occured while adding project {ProjectId} to Cache : {Error}", project.Id, ex.Message);
}
}
public async Task<bool> UpdateProjectDetailsOnly(Project project)
@ -446,14 +62,7 @@ namespace Marco.Pms.Services.Helpers
try
{
var response = await _projectCache.GetProjectDetailsListFromCache(projectIds);
if (response.Any())
{
return response;
}
else
{
return null;
}
return response;
}
catch (Exception ex)
{
@ -461,9 +70,6 @@ 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
@ -736,33 +342,5 @@ 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);
}
}
}
}

View File

@ -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,14 +13,13 @@ 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, PermissionServices permission)
public ProjectsHelper(ApplicationDbContext context, RolesHelper rolesHelper, CacheUpdateHelper cache)
{
_context = context;
_rolesHelper = rolesHelper;
_cache = cache;
_permission = permission;
}
public async Task<List<Project>> GetAllProjectByTanentID(Guid tanentID)
@ -52,31 +51,79 @@ namespace MarcoBMS.Services.Helpers
}
}
public async Task<List<Guid>> GetMyProjects(Guid tenantId, Employee LoggedInEmployee)
public async Task<List<Project>> 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)
{
var hasPermission = await _permission.HasPermission(LoggedInEmployee.Id, PermissionsMaster.ManageProject);
if (hasPermission)
List<ProjectMongoDB> projectdetails = await _cache.GetProjectDetailsList(projectIds) ?? new List<ProjectMongoDB>();
projects = projectdetails.Select(p => new Project
{
var projects = await _context.Projects.Where(c => c.TenantId == tenantId).ToListAsync();
projectIds = projects.Select(p => p.Id).ToList();
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
}
else
{
// 3. Efficiently get project allocations and then filter projects
// Load allocations only once
var allocation = await GetProjectByEmployeeID(LoggedInEmployee.Id);
if (allocation.Any())
// If there are no allocations, return an empty list early
if (allocation == null || !allocation.Any())
{
projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList();
return new List<Project>();
}
return new List<Guid>();
// Use LINQ's Contains for efficient filtering by ProjectId
projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); // Get distinct Guids
// Filter projects based on the retrieved ProjectIds
projects = await projectQuery.Where(c => projectIds.Contains(c.Id)).ToListAsync();
}
projectIds = projects.Select(p => p.Id).ToList();
await _cache.AddProjects(LoggedInEmployee.Id, projectIds);
}
return projectIds;
return projects;
}
}

View File

@ -1,28 +1,20 @@
using Marco.Pms.DataAccess.Data;
using System.Globalization;
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(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, CacheUpdateHelper cache)
public ReportHelper(CacheUpdateHelper cache, ApplicationDbContext context)
{
_context = context;
_emailSender = emailSender;
_logger = logger;
_cache = cache;
_context = context;
}
public async Task<ProjectStatisticReport?> GetDailyProjectReport(Guid projectId, Guid tenantId)
{
@ -278,88 +270,5 @@ 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);
}
}
}
}

View File

@ -1,30 +0,0 @@
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>();
}
}
}

View File

@ -11,7 +11,6 @@
</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" />

View File

@ -1,3 +1,4 @@
using System.Text;
using Marco.Pms.CacheHelper;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Authentication;
@ -15,23 +16,47 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using Serilog;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
#region ======================= Service Configuration (Dependency Injection) =======================
// Add Serilog Configuration
string? mongoConn = builder.Configuration["MongoDB:SerilogDatabaseUrl"];
string timeString = "00:00:30";
TimeSpan.TryParse(timeString, out TimeSpan timeSpan);
#region Logging
// Add Serilog Configuration
builder.Host.UseSerilog((context, config) =>
{
config.ReadFrom.Configuration(context.Configuration);
});
#endregion
config.ReadFrom.Configuration(context.Configuration) // Taking all configuration from appsetting.json
.WriteTo.MongoDB(
databaseUrl: mongoConn ?? string.Empty,
collectionName: "api-logs",
batchPostingLimit: 100,
period: timeSpan
);
});
// 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 =>
{
// A more permissive policy for development
options.AddPolicy("Policy", policy =>
{
if (allowedOrigins != null && allowedMethods != null && allowedHeaders != null)
{
policy.WithOrigins(allowedOrigins)
.WithMethods(allowedMethods)
.WithHeaders(allowedHeaders);
}
});
}).AddCors(options =>
{
options.AddPolicy("DevCorsPolicy", policy =>
{
policy.AllowAnyOrigin()
@ -39,51 +64,93 @@ builder.Services.AddCors(options =>
.AllowAnyHeader()
.WithExposedHeaders("Authorization");
});
});
// 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 =>
// Add services to the container.
builder.Services.AddHostedService<StartupUserSeeder>();
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
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
{
policy.WithOrigins(allowedOrigins)
.AllowAnyMethod()
.AllowAnyHeader();
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"
}
},
new string[]{}
}
});
});
#endregion
#region Core Web & Framework Services
builder.Services.AddControllers();
builder.Services.AddSignalR();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddHttpContextAccessor();
builder.Services.AddMemoryCache();
builder.Services.AddAutoMapper(typeof(Program));
builder.Services.AddHostedService<StartupUserSeeder>();
#endregion
builder.Services.Configure<SmtpSettings>(builder.Configuration.GetSection("SmtpSettings"));
builder.Services.AddTransient<IEmailSender, EmailSender>();
#region Database & Identity
string? connString = builder.Configuration.GetConnectionString("DefaultConnectionString")
?? throw new InvalidOperationException("Database connection string 'DefaultConnectionString' not found.");
builder.Services.Configure<AWSSettings>(builder.Configuration.GetSection("AWS")); // For uploading images to aws s3
builder.Services.AddTransient<S3UploadService>();
// 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.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();
string? connString = builder.Configuration.GetConnectionString("DefaultConnectionString");
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
#region Authentication (JWT)
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();
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;
@ -101,129 +168,71 @@ 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"];
if (!string.IsNullOrEmpty(accessToken) && context.HttpContext.Request.Path.StartsWithSegments("/hubs/marco"))
var path = context.HttpContext.Request.Path;
// Match your hub route here
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs/marco"))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
builder.Services.AddSingleton(jwtSettings);
}
#endregion
#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.Services.AddSignalR();
builder.WebHost.ConfigureKestrel(options =>
{
options.AddServerHeader = false; // Disable the "Server" header for security
options.AddServerHeader = false; // Disable the "Server" header
});
#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
#region Development Environment Configuration
// These tools are only enabled in the Development environment for debugging and API testing.
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
// Use CORS in the pipeline
app.UseCors("DevCorsPolicy");
}
#endregion
else
{
//if (app.Environment.IsProduction())
//{
// app.UseCors("ProdCorsPolicy");
//}
#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.UseCors("AllowAll");
app.UseCors("DevCorsPolicy");
}
#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.UseStaticFiles(); // Enables serving static files
app.UseAuthentication(); // 1. Identifies who the user is.
app.UseAuthorization(); // 2. Determines what the identified user is allowed to do.
#endregion
//app.UseSerilogRequestLogging(); // This is Default Serilog Logging Middleware we are not using this because we're using custom logging middleware
#region Endpoint Routing (Run Last)
// These map incoming requests to the correct controller actions or SignalR hubs.
app.MapControllers();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapHub<MarcoHub>("/hubs/marco");
#endregion
#endregion
app.MapControllers();
app.Run();

View File

@ -150,24 +150,18 @@ 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));
if (!string.IsNullOrWhiteSpace(subject))
var subjectReplacements = new Dictionary<string, string>
{
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)
{"DATE", date },
{"PROJECT_NAME", report.ProjectName}
};
foreach (var item in subjectReplacements)
{
await SendEmailAsync(toEmails, subject, emailBody);
subject = subject.Replace($"{{{{{item.Key}}}}}", item.Value);
}
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)

View File

@ -1,9 +1,10 @@
namespace MarcoBMS.Services.Service
using Serilog.Context;
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);

View File

@ -18,11 +18,10 @@ namespace MarcoBMS.Services.Service
{
_logger.LogError(message, args);
}
else
{
else {
_logger.LogError(message);
}
}
}
public void LogInfo(string? message, params object[]? args)
{
@ -36,18 +35,6 @@ 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)
{
@ -62,5 +49,6 @@ namespace MarcoBMS.Services.Service
}
}
}
}

View File

@ -1,6 +1,7 @@
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;
@ -11,11 +12,13 @@ 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, CacheUpdateHelper cache)
public PermissionServices(ApplicationDbContext context, RolesHelper rolesHelper, ProjectsHelper projectsHelper, CacheUpdateHelper cache)
{
_context = context;
_rolesHelper = rolesHelper;
_projectsHelper = projectsHelper;
_cache = cache;
}
@ -30,31 +33,24 @@ namespace Marco.Pms.Services.Service
var hasPermission = featurePermissionIds.Contains(featurePermissionId);
return hasPermission;
}
public async Task<bool> HasProjectPermission(Employee LoggedInEmployee, Guid projectId)
public async Task<bool> HasProjectPermission(Employee emp, string projectId)
{
var employeeId = LoggedInEmployee.Id;
var projectIds = await _cache.GetProjects(employeeId);
List<FeaturePermission> featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(emp.Id);
string[] projectsId = [];
if (projectIds == null)
/* User with permission manage project can see all projects */
if (featurePermission != null && featurePermission.Exists(c => c.Id.ToString() == "172fc9b6-755b-4f62-ab26-55c34a330614"))
{
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);
List<Project> projects = await _projectsHelper.GetAllProjectByTanentID(emp.TenantId);
projectsId = projects.Select(c => c.Id.ToString()).ToArray();
}
return projectIds.Contains(projectId);
else
{
List<ProjectAllocation> allocation = await _projectsHelper.GetProjectByEmployeeID(emp.Id);
projectsId = allocation.Select(c => c.ProjectId.ToString()).ToArray();
}
bool response = projectsId.Contains(projectId);
return response;
}
}
}