Compare commits

..

No commits in common. "afe20b404adbbfe7f8b57e6fb5ac343b5ced4528" and "3a45bded0807fc7dba9e5737bd04f29f2031865d" have entirely different histories.

50 changed files with 2095 additions and 4110 deletions

3
.gitignore vendored
View File

@ -361,6 +361,3 @@ MigrationBackup/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# Sonar
/.sonarqube

View File

@ -1,4 +1,5 @@
using Marco.Pms.Model.MongoDBModels;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.MongoDBModels;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using MongoDB.Driver;
@ -7,21 +8,41 @@ namespace Marco.Pms.CacheHelper
{
public class EmployeeCache
{
private readonly ApplicationDbContext _context;
//private readonly IMongoDatabase _mongoDB;
private readonly IMongoCollection<EmployeePermissionMongoDB> _collection;
public EmployeeCache(IConfiguration configuration)
public EmployeeCache(ApplicationDbContext context, IConfiguration configuration)
{
var connectionString = configuration["MongoDB:ConnectionString"];
_context = context;
var mongoUrl = new MongoUrl(connectionString);
var client = new MongoClient(mongoUrl); // Your MongoDB connection string
var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name
_collection = mongoDB.GetCollection<EmployeePermissionMongoDB>("EmployeeProfile");
}
public async Task<bool> AddApplicationRoleToCache(Guid employeeId, List<string> newRoleIds, List<string> newPermissionIds)
public async Task<bool> AddApplicationRoleToCache(Guid employeeId, List<Guid> roleIds)
{
// 1. Guard Clause: Avoid unnecessary database work if there are no roles to add.
if (roleIds == null || !roleIds.Any())
{
return false; // Nothing to add, so the operation did not result in a change.
}
// 2. Perform database queries concurrently for better performance.
var employeeIdString = employeeId.ToString();
Task<List<string>> getPermissionIdsTask = _context.RolePermissionMappings
.Where(rp => roleIds.Contains(rp.ApplicationRoleId))
.Select(p => p.FeaturePermissionId.ToString())
.Distinct()
.ToListAsync();
// 3. Prepare role IDs in parallel with the database query.
var newRoleIds = roleIds.Select(r => r.ToString()).ToList();
// 4. Await the database query result.
var newPermissionIds = await getPermissionIdsTask;
// 5. Build a single, efficient update operation.
var filter = Builders<EmployeePermissionMongoDB>.Filter.Eq(e => e.Id, employeeIdString);
@ -33,8 +54,6 @@ namespace Marco.Pms.CacheHelper
var result = await _collection.UpdateOneAsync(filter, update, options);
await InitializeCollectionAsync();
// 6. Return a more accurate result indicating success for both updates and upserts.
// The operation is successful if an existing document was modified OR a new one was created.
return result.IsAcknowledged && (result.ModifiedCount > 0 || result.UpsertedId != null);
@ -53,7 +72,6 @@ namespace Marco.Pms.CacheHelper
{
return false;
}
await InitializeCollectionAsync();
return true;
}
public async Task<List<Guid>> GetProjectsFromCache(Guid employeeId)
@ -100,7 +118,7 @@ namespace Marco.Pms.CacheHelper
var result = await _collection.UpdateOneAsync(filter, update);
if (result.ModifiedCount == 0)
if (result.MatchedCount == 0)
return false;
return true;
@ -122,10 +140,16 @@ namespace Marco.Pms.CacheHelper
public async Task<bool> ClearAllProjectIdsByPermissionIdFromCache(Guid permissionId)
{
var filter = Builders<EmployeePermissionMongoDB>.Filter.AnyEq(e => e.PermissionIds, permissionId.ToString());
var update = Builders<EmployeePermissionMongoDB>.Update.Set(e => e.ProjectIds, new List<string>());
var result = await _collection.UpdateManyAsync(filter, update).ConfigureAwait(false);
return result.IsAcknowledged && result.ModifiedCount > 0;
var update = Builders<EmployeePermissionMongoDB>.Update
.Set(e => e.ProjectIds, new List<string>());
var result = await _collection.UpdateOneAsync(filter, update);
if (result.MatchedCount == 0)
return false;
return true;
}
public async Task<bool> RemoveRoleIdFromCache(Guid employeeId, Guid roleId)
{
@ -174,31 +198,5 @@ namespace Marco.Pms.CacheHelper
return true;
}
public async Task<bool> ClearAllEmployeesFromCache()
{
var result = await _collection.DeleteManyAsync(FilterDefinition<EmployeePermissionMongoDB>.Empty);
if (result.DeletedCount == 0)
return false;
return true;
}
// A private method to handle the one-time setup of the collection's indexes.
private async Task InitializeCollectionAsync()
{
// 1. Define the TTL (Time-To-Live) index on the 'ExpireAt' field.
var indexKeys = Builders<EmployeePermissionMongoDB>.IndexKeys.Ascending(x => x.ExpireAt);
var indexOptions = new CreateIndexOptions
{
// This tells MongoDB to automatically delete documents when their 'ExpireAt' time is reached.
ExpireAfter = TimeSpan.FromSeconds(0)
};
var indexModel = new CreateIndexModel<EmployeePermissionMongoDB>(indexKeys, indexOptions);
// 2. Create the index. This is an idempotent operation if the index already exists.
// Use CreateOneAsync since we are only creating a single index.
await _collection.Indexes.CreateOneAsync(indexModel);
}
}
}

View File

@ -11,62 +11,159 @@ namespace Marco.Pms.CacheHelper
{
public class ProjectCache
{
private readonly IMongoCollection<ProjectMongoDB> _projectCollection;
private readonly ApplicationDbContext _context;
private readonly IMongoCollection<ProjectMongoDB> _projetCollection;
private readonly IMongoCollection<WorkItemMongoDB> _taskCollection;
public ProjectCache(ApplicationDbContext context, IConfiguration configuration)
{
var connectionString = configuration["MongoDB:ConnectionString"];
_context = context;
var mongoUrl = new MongoUrl(connectionString);
var client = new MongoClient(mongoUrl); // Your MongoDB connection string
var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name
_projectCollection = mongoDB.GetCollection<ProjectMongoDB>("ProjectDetails");
_projetCollection = mongoDB.GetCollection<ProjectMongoDB>("ProjectDetails");
_taskCollection = mongoDB.GetCollection<WorkItemMongoDB>("WorkItemDetails");
}
#region=================================================================== Project Cache Helper ===================================================================
public async Task AddProjectDetailsToCache(ProjectMongoDB projectDetails)
public async Task AddProjectDetailsToCache(Project project)
{
await _projectCollection.InsertOneAsync(projectDetails);
//_logger.LogInfo("[AddProjectDetails] Initiated for ProjectId: {ProjectId}", project.Id);
var indexKeys = Builders<ProjectMongoDB>.IndexKeys.Ascending(x => x.ExpireAt);
var indexOptions = new CreateIndexOptions
var projectDetails = new ProjectMongoDB
{
ExpireAfter = TimeSpan.Zero // required for fixed expiration time
Id = project.Id.ToString(),
Name = project.Name,
ShortName = project.ShortName,
ProjectAddress = project.ProjectAddress,
StartDate = project.StartDate,
EndDate = project.EndDate,
ContactPerson = project.ContactPerson
};
var indexModel = new CreateIndexModel<ProjectMongoDB>(indexKeys, indexOptions);
await _projectCollection.Indexes.CreateOneAsync(indexModel);
}
public async Task AddProjectDetailsListToCache(List<ProjectMongoDB> projectDetailsList)
{
// 1. Add a guard clause to avoid an unnecessary database call for an empty list.
if (projectDetailsList == null || !projectDetailsList.Any())
// Get project status
var status = await _context.StatusMasters
.AsNoTracking()
.FirstOrDefaultAsync(s => s.Id == project.ProjectStatusId);
projectDetails.ProjectStatus = new StatusMasterMongoDB
{
return;
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;
}
// 2. Perform the insert operation. This is the only responsibility of this method.
await _projectCollection.InsertManyAsync(projectDetailsList);
await InitializeCollectionAsync();
}
private async Task InitializeCollectionAsync()
{
// 1. Define the TTL (Time-To-Live) index on the 'ExpireAt' field.
var indexKeys = Builders<ProjectMongoDB>.IndexKeys.Ascending(x => x.ExpireAt);
var indexOptions = new CreateIndexOptions
{
// This tells MongoDB to automatically delete documents when their 'ExpireAt' time is reached.
ExpireAfter = TimeSpan.FromSeconds(0)
};
var indexModel = new CreateIndexModel<ProjectMongoDB>(indexKeys, indexOptions);
projectDetails.Buildings = buildingMongoList;
projectDetails.PlannedWork = totalPlannedWork;
projectDetails.CompletedWork = totalCompletedWork;
// 2. Create the index. This is an idempotent operation if the index already exists.
// Use CreateOneAsync since we are only creating a single index.
await _projectCollection.Indexes.CreateOneAsync(indexModel);
await _projetCollection.InsertOneAsync(projectDetails);
//_logger.LogInfo("[AddProjectDetails] Project details inserted in MongoDB for ProjectId: {ProjectId}", project.Id);
}
public async Task<bool> UpdateProjectDetailsOnlyToCache(Project project, StatusMaster projectStatus)
public async Task<bool> UpdateProjectDetailsOnlyToCache(Project project)
{
//_logger.LogInfo("Starting update for project: {ProjectId}", project.Id);
var projectStatus = await _context.StatusMasters
.FirstOrDefaultAsync(s => s.Id == project.ProjectStatusId);
if (projectStatus == null)
{
//_logger.LogWarning("StatusMaster not found for ProjectStatusId: {StatusId}", project.ProjectStatusId);
}
// Build the update definition
var updates = Builders<ProjectMongoDB>.Update.Combine(
Builders<ProjectMongoDB>.Update.Set(r => r.Name, project.Name),
@ -74,8 +171,8 @@ namespace Marco.Pms.CacheHelper
Builders<ProjectMongoDB>.Update.Set(r => r.ShortName, project.ShortName),
Builders<ProjectMongoDB>.Update.Set(r => r.ProjectStatus, new StatusMasterMongoDB
{
Id = projectStatus.Id.ToString(),
Status = projectStatus.Status
Id = projectStatus?.Id.ToString(),
Status = projectStatus?.Status
}),
Builders<ProjectMongoDB>.Update.Set(r => r.StartDate, project.StartDate),
Builders<ProjectMongoDB>.Update.Set(r => r.EndDate, project.EndDate),
@ -83,16 +180,18 @@ namespace Marco.Pms.CacheHelper
);
// Perform the update
var result = await _projectCollection.UpdateOneAsync(
var result = await _projetCollection.UpdateOneAsync(
filter: r => r.Id == project.Id.ToString(),
update: updates
);
if (result.MatchedCount == 0)
{
//_logger.LogWarning("No project matched in MongoDB for update. ProjectId: {ProjectId}", project.Id);
return false;
}
//_logger.LogInfo("Project {ProjectId} successfully updated in MongoDB", project.Id);
return true;
}
public async Task<ProjectMongoDB?> GetProjectDetailsFromCache(Guid projectId)
@ -102,56 +201,34 @@ namespace Marco.Pms.CacheHelper
var filter = Builders<ProjectMongoDB>.Filter.Eq(p => p.Id, projectId.ToString());
var projection = Builders<ProjectMongoDB>.Projection.Exclude(p => p.Buildings);
//_logger.LogInfo("Fetching project details for ProjectId: {ProjectId} from MongoDB", projectId);
// Perform query
var project = await _projectCollection
var project = await _projetCollection
.Find(filter)
.Project<ProjectMongoDB>(projection)
.FirstOrDefaultAsync();
if (project == null)
{
//_logger.LogWarning("No project found in MongoDB for ProjectId: {ProjectId}", projectId);
return null;
}
//_logger.LogInfo("Successfully fetched project details (excluding Buildings) for ProjectId: {ProjectId}", projectId);
return project;
}
public async Task<ProjectMongoDB?> GetProjectDetailsWithBuildingsFromCache(Guid projectId)
{
// Build filter and projection to exclude large 'Buildings' list
var filter = Builders<ProjectMongoDB>.Filter.Eq(p => p.Id, projectId.ToString());
// Perform query
var project = await _projectCollection
.Find(filter)
.FirstOrDefaultAsync();
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);
var projection = Builders<ProjectMongoDB>.Projection.Exclude(p => p.Buildings);
var projects = await _projectCollection
var projects = await _projetCollection
.Find(filter)
.Project<ProjectMongoDB>(projection)
.ToListAsync();
return projects;
}
public async Task<bool> DeleteProjectByIdFromCacheAsync(Guid projectId)
{
var filter = Builders<ProjectMongoDB>.Filter.Eq(e => e.Id, projectId.ToString());
var result = await _projectCollection.DeleteOneAsync(filter);
return result.DeletedCount > 0;
}
public async Task<bool> RemoveProjectsFromCacheAsync(List<Guid> projectIds)
{
var stringIds = projectIds.Select(id => id.ToString()).ToList();
var filter = Builders<ProjectMongoDB>.Filter.In(p => p.Id, stringIds);
var result = await _projectCollection.DeleteManyAsync(filter);
return result.DeletedCount > 0;
}
#endregion
#region=================================================================== Project infrastructure Cache Helper ===================================================================
public async Task AddBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId)
{
var stringProjectId = projectId.ToString();
@ -172,12 +249,15 @@ namespace Marco.Pms.CacheHelper
var filter = Builders<ProjectMongoDB>.Filter.Eq(p => p.Id, stringProjectId);
var update = Builders<ProjectMongoDB>.Update.Push("Buildings", buildingMongo);
var result = await _projectCollection.UpdateOneAsync(filter, update);
var result = await _projetCollection.UpdateOneAsync(filter, update);
if (result.MatchedCount == 0)
{
//_logger.LogWarning("Project not found while adding building. ProjectId: {ProjectId}", projectId);
return;
}
//_logger.LogInfo("Building {BuildingId} added to project {ProjectId}", building.Id, projectId);
return;
}
@ -199,12 +279,15 @@ namespace Marco.Pms.CacheHelper
);
var update = Builders<ProjectMongoDB>.Update.Push("Buildings.$.Floors", floorMongo);
var result = await _projectCollection.UpdateOneAsync(filter, update);
var result = await _projetCollection.UpdateOneAsync(filter, update);
if (result.MatchedCount == 0)
{
//_logger.LogWarning("Project or building not found while adding floor. ProjectId: {ProjectId}, BuildingId: {BuildingId}", projectId, floor.BuildingId);
return;
}
//_logger.LogInfo("Floor {FloorId} added to building {BuildingId} in project {ProjectId}", floor.Id, floor.BuildingId, projectId);
return;
}
@ -230,14 +313,20 @@ namespace Marco.Pms.CacheHelper
var update = Builders<ProjectMongoDB>.Update.Push("Buildings.$[b].Floors.$[f].WorkAreas", workAreaMongo);
var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters };
var result = await _projectCollection.UpdateOneAsync(filter, update, updateOptions);
var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions);
if (result.MatchedCount == 0)
{
//_logger.LogWarning("Project or nested structure not found while adding work area. ProjectId: {ProjectId}, BuildingId: {BuildingId}, FloorId: {FloorId}", projectId, buildingId, workArea.FloorId);
return;
}
//_logger.LogInfo("WorkArea {WorkAreaId} added to floor {FloorId} in building {BuildingId}, ProjectId: {ProjectId}", workArea.Id, workArea.FloorId, buildingId, projectId);
return;
}
// Fallback case when no valid data was passed
//_logger.LogWarning("No valid infra data provided to add for ProjectId: {ProjectId}", projectId);
}
public async Task<bool> UpdateBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId)
{
@ -256,13 +345,15 @@ namespace Marco.Pms.CacheHelper
Builders<ProjectMongoDB>.Update.Set("Buildings.$.Description", building.Description)
);
var result = await _projectCollection.UpdateOneAsync(filter, update);
var result = await _projetCollection.UpdateOneAsync(filter, update);
if (result.MatchedCount == 0)
{
//_logger.LogWarning("Update failed: Project or Building not found. ProjectId: {ProjectId}, BuildingId: {BuildingId}", projectId, building.Id);
return false;
}
//_logger.LogInfo("Building {BuildingId} updated successfully in project {ProjectId}", building.Id, projectId);
return true;
}
@ -279,12 +370,15 @@ namespace Marco.Pms.CacheHelper
var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters };
var filter = Builders<ProjectMongoDB>.Filter.Eq(p => p.Id, stringProjectId);
var result = await _projectCollection.UpdateOneAsync(filter, update, updateOptions);
var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions);
if (result.MatchedCount == 0)
{
//_logger.LogWarning("Update failed: Project or Floor not found. ProjectId: {ProjectId}, BuildingId: {BuildingId}, FloorId: {FloorId}", projectId, floor.BuildingId, floor.Id);
return false;
}
//_logger.LogInfo("Floor {FloorId} updated successfully in Building {BuildingId}, ProjectId: {ProjectId}", floor.Id, floor.BuildingId, projectId);
return true;
}
@ -302,14 +396,21 @@ namespace Marco.Pms.CacheHelper
var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters };
var filter = Builders<ProjectMongoDB>.Filter.Eq(p => p.Id, stringProjectId);
var result = await _projectCollection.UpdateOneAsync(filter, update, updateOptions);
var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions);
if (result.MatchedCount == 0)
{
//_logger.LogWarning("Update failed: Project or WorkArea not found. ProjectId: {ProjectId}, BuildingId: {BuildingId}, FloorId: {FloorId}, WorkAreaId: {WorkAreaId}",
//projectId, buildingId, workArea.FloorId, workArea.Id);
return false;
}
//_logger.LogInfo("WorkArea {WorkAreaId} updated successfully in Floor {FloorId}, Building {BuildingId}, ProjectId: {ProjectId}",
//workArea.Id, workArea.FloorId, buildingId, projectId);
return true;
}
//_logger.LogWarning("No update performed. Missing or invalid data for ProjectId: {ProjectId}", projectId);
return false;
}
public async Task<List<BuildingMongoDB>?> GetBuildingInfraFromCache(Guid projectId)
@ -319,17 +420,26 @@ namespace Marco.Pms.CacheHelper
var filter = Builders<ProjectMongoDB>.Filter.Eq(p => p.Id, projectId.ToString());
// Project only the "Buildings" field from the document
var buildings = await _projectCollection
var buildings = await _projetCollection
.Find(filter)
.Project(p => p.Buildings)
.FirstOrDefaultAsync();
//if (buildings == null)
//{
// _logger.LogWarning("No building infrastructure found for ProjectId: {ProjectId}", projectId);
//}
//else
//{
// _logger.LogInfo("Fetched {Count} buildings for ProjectId: {ProjectId}", buildings.Count, projectId);
//}
return buildings;
}
public async Task UpdatePlannedAndCompleteWorksInBuildingFromCache(Guid workAreaId, double plannedWork, double completedWork)
{
var filter = Builders<ProjectMongoDB>.Filter.Eq("Buildings.Floors.WorkAreas._id", workAreaId.ToString());
var project = await _projectCollection.Find(filter).FirstOrDefaultAsync();
var project = await _projetCollection.Find(filter).FirstOrDefaultAsync();
string? selectedBuildingId = null;
string? selectedFloorId = null;
@ -367,7 +477,7 @@ namespace Marco.Pms.CacheHelper
.Inc("Buildings.$[b].CompletedWork", completedWork)
.Inc("PlannedWork", plannedWork)
.Inc("CompletedWork", completedWork);
var result = await _projectCollection.UpdateOneAsync(filter, update, updateOptions);
var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions);
}
public async Task<WorkAreaInfoMongoDB?> GetBuildingAndFloorByWorkAreaIdFromCache(Guid workAreaId)
@ -407,16 +517,11 @@ namespace Marco.Pms.CacheHelper
{ "WorkArea", "$Buildings.Floors.WorkAreas" }
})
};
var result = await _projectCollection.Aggregate<WorkAreaInfoMongoDB>(pipeline).FirstOrDefaultAsync();
var result = await _projetCollection.Aggregate<WorkAreaInfoMongoDB>(pipeline).FirstOrDefaultAsync();
if (result == null)
return null;
return result;
}
#endregion
#region=================================================================== WorkItem Cache Helper ===================================================================
public async Task<List<WorkItemMongoDB>> GetWorkItemsByWorkAreaIdsFromCache(List<Guid> workAreaIds)
{
var stringWorkAreaIds = workAreaIds.Select(wa => wa.ToString()).ToList();
@ -428,22 +533,48 @@ namespace Marco.Pms.CacheHelper
return workItems;
}
public async Task ManageWorkItemDetailsToCache(List<WorkItemMongoDB> workItems)
// ------------------------------------------------------- WorkItem -------------------------------------------------------
public async Task ManageWorkItemDetailsToCache(List<WorkItem> workItems)
{
foreach (WorkItemMongoDB workItem in workItems)
var activityIds = workItems.Select(wi => wi.ActivityId).ToList();
var workCategoryIds = workItems.Select(wi => wi.WorkCategoryId).ToList();
var workItemIds = workItems.Select(wi => wi.Id).ToList();
// fetching Activity master
var activities = await _context.ActivityMasters.Where(a => activityIds.Contains(a.Id)).ToListAsync() ?? new List<ActivityMaster>();
// Fetching Work Category
var workCategories = await _context.WorkCategoryMasters.Where(wc => workCategoryIds.Contains(wc.Id)).ToListAsync() ?? new List<WorkCategoryMaster>();
var task = await _context.TaskAllocations.Where(t => workItemIds.Contains(t.WorkItemId) && t.AssignmentDate == DateTime.UtcNow).ToListAsync();
var todaysAssign = task.Sum(t => t.PlannedTask);
foreach (WorkItem workItem in workItems)
{
var activity = activities.FirstOrDefault(a => a.Id == workItem.ActivityId) ?? new ActivityMaster();
var workCategory = workCategories.FirstOrDefault(a => a.Id == workItem.WorkCategoryId) ?? new WorkCategoryMaster();
var filter = Builders<WorkItemMongoDB>.Filter.Eq(p => p.Id, workItem.Id.ToString());
var updates = Builders<WorkItemMongoDB>.Update.Combine(
Builders<WorkItemMongoDB>.Update.Set(r => r.WorkAreaId, workItem.WorkAreaId.ToString()),
Builders<WorkItemMongoDB>.Update.Set(r => r.ParentTaskId, (workItem.ParentTaskId != null ? workItem.ParentTaskId.ToString() : null)),
Builders<WorkItemMongoDB>.Update.Set(r => r.PlannedWork, workItem.PlannedWork),
Builders<WorkItemMongoDB>.Update.Set(r => r.TodaysAssigned, workItem.TodaysAssigned),
Builders<WorkItemMongoDB>.Update.Set(r => r.TodaysAssigned, todaysAssign),
Builders<WorkItemMongoDB>.Update.Set(r => r.CompletedWork, workItem.CompletedWork),
Builders<WorkItemMongoDB>.Update.Set(r => r.Description, workItem.Description),
Builders<WorkItemMongoDB>.Update.Set(r => r.TaskDate, workItem.TaskDate),
Builders<WorkItemMongoDB>.Update.Set(r => r.ExpireAt, DateTime.UtcNow.Date.AddDays(1)),
Builders<WorkItemMongoDB>.Update.Set(r => r.ActivityMaster, workItem.ActivityMaster),
Builders<WorkItemMongoDB>.Update.Set(r => r.WorkCategoryMaster, workItem.WorkCategoryMaster)
Builders<WorkItemMongoDB>.Update.Set(r => r.ActivityMaster, new ActivityMasterMongoDB
{
Id = activity.Id.ToString(),
ActivityName = activity.ActivityName,
UnitOfMeasurement = activity.UnitOfMeasurement
}),
Builders<WorkItemMongoDB>.Update.Set(r => r.WorkCategoryMaster, new WorkCategoryMasterMongoDB
{
Id = workCategory.Id.ToString(),
Name = workCategory.Name,
Description = workCategory.Description,
})
);
var options = new UpdateOptions { IsUpsert = true };
var result = await _taskCollection.UpdateOneAsync(filter, updates, options);
@ -494,13 +625,5 @@ namespace Marco.Pms.CacheHelper
}
return false;
}
public async Task<bool> DeleteWorkItemByIdFromCacheAsync(Guid workItemId)
{
var filter = Builders<WorkItemMongoDB>.Filter.Eq(e => e.Id, workItemId.ToString());
var result = await _taskCollection.DeleteOneAsync(filter);
return result.DeletedCount > 0;
}
#endregion
}
}

View File

@ -1,42 +0,0 @@
using Marco.Pms.Model.MongoDBModels;
using Microsoft.Extensions.Configuration;
using MongoDB.Driver;
namespace Marco.Pms.CacheHelper
{
public class ReportCache
{
private readonly IMongoCollection<ProjectReportEmailMongoDB> _projectReportCollection;
public ReportCache(IConfiguration configuration)
{
var connectionString = configuration["MongoDB:ConnectionString"];
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

@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations;
namespace Marco.Pms.Model.Dtos.Project
{
public class BuildingDto
public class BuildingDot
{
[Key]
public Guid? Id { get; set; }

View File

@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations;
namespace Marco.Pms.Model.Dtos.Project
{
public class FloorDto
public class FloorDot
{
public Guid? Id { get; set; }

View File

@ -0,0 +1,9 @@
namespace Marco.Pms.Model.Dtos.Project
{
public class InfraDot
{
public BuildingDot? Building { get; set; }
public FloorDot? Floor { get; set; }
public WorkAreaDot? WorkArea { get; set; }
}
}

View File

@ -1,9 +0,0 @@
namespace Marco.Pms.Model.Dtos.Project
{
public class InfraDto
{
public BuildingDto? Building { get; set; }
public FloorDto? Floor { get; set; }
public WorkAreaDto? WorkArea { get; set; }
}
}

View File

@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations;
namespace Marco.Pms.Model.Dtos.Project
{
public class WorkAreaDto
public class WorkAreaDot
{
[Key]
public Guid? Id { get; set; }

View File

@ -2,7 +2,7 @@
namespace Marco.Pms.Model.Dtos.Project
{
public class WorkItemDto
public class WorkItemDot
{
[Key]
public Guid? Id { get; set; }

View File

@ -5,7 +5,7 @@ namespace Marco.Pms.Model.Mapper
{
public static class BuildingMapper
{
public static Building ToBuildingFromBuildingDto(this BuildingDto model, Guid tenantId)
public static Building ToBuildingFromBuildingDto(this BuildingDot model, Guid tenantId)
{
return new Building
{
@ -20,7 +20,7 @@ namespace Marco.Pms.Model.Mapper
public static class FloorMapper
{
public static Floor ToFloorFromFloorDto(this FloorDto model, Guid tenantId)
public static Floor ToFloorFromFloorDto(this FloorDot model, Guid tenantId)
{
return new Floor
{
@ -34,7 +34,7 @@ namespace Marco.Pms.Model.Mapper
public static class WorAreaMapper
{
public static WorkArea ToWorkAreaFromWorkAreaDto(this WorkAreaDto model, Guid tenantId)
public static WorkArea ToWorkAreaFromWorkAreaDto(this WorkAreaDot model, Guid tenantId)
{
return new WorkArea
{
@ -48,7 +48,7 @@ namespace Marco.Pms.Model.Mapper
}
public static class WorkItemMapper
{
public static WorkItem ToWorkItemFromWorkItemDto(this WorkItemDto model, Guid tenantId)
public static WorkItem ToWorkItemFromWorkItemDto(this WorkItemDot model, Guid tenantId)
{
return new WorkItem
{

View File

@ -9,6 +9,5 @@ namespace Marco.Pms.Model.MongoDBModels
public List<string> ApplicationRoleIds { get; set; } = new List<string>();
public List<string> PermissionIds { get; set; } = new List<string>();
public List<string> ProjectIds { get; set; } = new List<string>();
public DateTime ExpireAt { get; set; } = DateTime.UtcNow.Date.AddDays(1);
}
}

View File

@ -14,6 +14,5 @@
public int TeamSize { get; set; }
public double CompletedWork { get; set; }
public double PlannedWork { get; set; }
public DateTime ExpireAt { get; set; } = DateTime.UtcNow.Date.AddDays(1);
}
}

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

@ -2,7 +2,7 @@
{
public class StatusMasterMongoDB
{
public string Id { get; set; } = string.Empty;
public string? Id { get; set; }
public string? Status { get; set; }
}
}

View File

@ -1,8 +0,0 @@
namespace Marco.Pms.Model.Utilities
{
public class ServiceResponse
{
public object? Notification { get; set; }
public ApiResponse<object> Response { get; set; } = ApiResponse<object>.ErrorResponse("");
}
}

View File

@ -1,13 +0,0 @@
namespace Marco.Pms.Model.ViewModels.Projects
{
public class ProjectAllocationVM
{
public Guid Id { get; set; }
public Guid EmployeeId { get; set; }
public Guid? JobRoleId { get; set; }
public bool IsActive { get; set; } = true;
public Guid ProjectId { get; set; }
public DateTime AllocationDate { get; set; }
public DateTime? ReAllocationDate { get; set; }
}
}

View File

@ -1,15 +1,14 @@
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;
using Marco.Pms.Model.ViewModels.AttendanceVM;
using Marco.Pms.Services.Hubs;
using Marco.Pms.Services.Service;
using Marco.Pms.Services.Service.ServiceInterfaces;
using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization;
@ -17,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
@ -29,7 +27,7 @@ namespace MarcoBMS.Services.Controllers
{
private readonly ApplicationDbContext _context;
private readonly EmployeeHelper _employeeHelper;
private readonly IProjectServices _projectServices;
private readonly ProjectsHelper _projectsHelper;
private readonly UserHelper _userHelper;
private readonly S3UploadService _s3Service;
private readonly PermissionServices _permission;
@ -38,11 +36,11 @@ namespace MarcoBMS.Services.Controllers
public AttendanceController(
ApplicationDbContext context, EmployeeHelper employeeHelper, IProjectServices projectServices, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permission, IHubContext<MarcoHub> signalR)
ApplicationDbContext context, EmployeeHelper employeeHelper, ProjectsHelper projectsHelper, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permission, IHubContext<MarcoHub> signalR)
{
_context = context;
_employeeHelper = employeeHelper;
_projectServices = projectServices;
_projectsHelper = projectsHelper;
_userHelper = userHelper;
_s3Service = s3Service;
_logger = logger;
@ -63,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)
{
@ -91,18 +83,18 @@ namespace MarcoBMS.Services.Controllers
if (dateFrom != null && DateTime.TryParse(dateFrom, out fromDate) == false)
{
_logger.LogWarning("User sent Invalid from Date while featching attendance logs");
_logger.LogError("User sent Invalid from Date while featching attendance logs");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
}
if (dateTo != null && DateTime.TryParse(dateTo, out toDate) == false)
{
_logger.LogWarning("User sent Invalid to Date while featching attendance logs");
_logger.LogError("User sent Invalid to Date while featching attendance logs");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
}
if (employeeId == Guid.Empty)
{
_logger.LogWarning("The employee Id sent by user is empty");
_logger.LogError("The employee Id sent by user is empty");
return BadRequest(ApiResponse<object>.ErrorResponse("Employee ID is required and must not be Empty.", "Employee ID is required and must not be empty.", 400));
}
List<Attendance> attendances = await _context.Attendes.Where(c => c.EmployeeID == employeeId && c.TenantId == TenantId && c.AttendanceDate.Date >= fromDate && c.AttendanceDate.Date <= toDate).ToListAsync();
@ -147,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)
{
@ -162,18 +154,18 @@ namespace MarcoBMS.Services.Controllers
if (dateFrom != null && DateTime.TryParse(dateFrom, out fromDate) == false)
{
_logger.LogWarning("User sent Invalid fromDate while featching attendance logs");
_logger.LogError("User sent Invalid fromDate while featching attendance logs");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
}
if (dateTo != null && DateTime.TryParse(dateTo, out toDate) == false)
{
_logger.LogWarning("User sent Invalid toDate while featching attendance logs");
_logger.LogError("User sent Invalid toDate while featching attendance logs");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
}
if (projectId == Guid.Empty)
{
_logger.LogWarning("The project Id sent by user is less than or equal to zero");
_logger.LogError("The project Id sent by user is less than or equal to zero");
return BadRequest(ApiResponse<object>.ErrorResponse("Project ID is required and must be greater than zero.", "Project ID is required and must be greater than zero.", 400));
}
@ -189,7 +181,7 @@ namespace MarcoBMS.Services.Controllers
List<Attendance> lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date >= fromDate.Date && c.AttendanceDate.Date <= toDate.Date && c.TenantId == TenantId).ToListAsync();
List<ProjectAllocation> projectteam = await _projectServices.GetTeamByProject(TenantId, projectId, true);
List<ProjectAllocation> projectteam = await _projectsHelper.GetTeamByProject(TenantId, projectId, true);
var jobRole = await _context.JobRoles.ToListAsync();
foreach (Attendance? attendance in lstAttendance)
{
@ -263,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)
{
@ -277,13 +269,13 @@ namespace MarcoBMS.Services.Controllers
if (date != null && DateTime.TryParse(date, out forDate) == false)
{
_logger.LogWarning("User sent Invalid Date while featching attendance logs");
_logger.LogError("User sent Invalid Date while featching attendance logs");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
}
if (projectId == Guid.Empty)
{
_logger.LogWarning("The project Id sent by user is less than or equal to zero");
_logger.LogError("The project Id sent by user is less than or equal to zero");
return BadRequest(ApiResponse<object>.ErrorResponse("Project ID is required and must be greater than zero.", "Project ID is required and must be greater than zero.", 400));
}
@ -296,7 +288,7 @@ namespace MarcoBMS.Services.Controllers
List<Attendance> lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date == forDate && c.TenantId == TenantId).ToListAsync();
List<ProjectAllocation> projectteam = await _projectServices.GetTeamByProject(TenantId, projectId, IncludeInActive);
List<ProjectAllocation> projectteam = await _projectsHelper.GetTeamByProject(TenantId, projectId, IncludeInActive);
var idList = projectteam.Select(p => p.EmployeeId).ToList();
//var emp = await _context.Employees.Where(e => idList.Contains(e.Id)).Include(e => e.JobRole).ToListAsync();
var jobRole = await _context.JobRoles.ToListAsync();
@ -369,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)
{
@ -379,7 +371,8 @@ 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 _projectServices.GetTeamByProject(TenantId, projectId, true);
List<ProjectAllocation> projectteam = await _projectsHelper.GetTeamByProject(TenantId, projectId, true);
var idList = projectteam.Select(p => p.EmployeeId).ToList();
var jobRole = await _context.JobRoles.ToListAsync();
@ -426,7 +419,7 @@ namespace MarcoBMS.Services.Controllers
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
_logger.LogWarning("User sent Invalid Date while marking attendance \n {Error}", string.Join(",", errors));
_logger.LogError("User sent Invalid Date while marking attendance");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
@ -440,14 +433,14 @@ namespace MarcoBMS.Services.Controllers
if (recordAttendanceDot.MarkTime == null)
{
_logger.LogWarning("User sent Invalid Mark Time while marking attendance");
_logger.LogError("User sent Invalid Mark Time while marking attendance");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Mark Time", "Invalid Mark Time", 400));
}
DateTime finalDateTime = GetDateFromTimeStamp(recordAttendanceDot.Date, recordAttendanceDot.MarkTime);
if (recordAttendanceDot.Comment == null)
{
_logger.LogWarning("User sent Invalid comment while marking attendance");
_logger.LogError("User sent Invalid comment while marking attendance");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Comment", "Invalid Comment", 400));
}
@ -481,7 +474,7 @@ namespace MarcoBMS.Services.Controllers
}
else
{
_logger.LogWarning("Employee {EmployeeId} sent regularization request but it check-out time is earlier than check-out");
_logger.LogError("Employee {EmployeeId} sent regularization request but it check-out time is earlier than check-out");
return BadRequest(ApiResponse<object>.ErrorResponse("Check-out time must be later than check-in time", "Check-out time must be later than check-in time", 400));
}
// do nothing
@ -586,7 +579,7 @@ namespace MarcoBMS.Services.Controllers
catch (Exception ex)
{
await transaction.RollbackAsync(); // Rollback on failure
_logger.LogError(ex, "An Error occured while marking attendance");
_logger.LogError("{Error} while marking attendance", ex.Message);
var response = new
{
message = ex.Message,
@ -605,7 +598,7 @@ namespace MarcoBMS.Services.Controllers
if (!ModelState.IsValid)
{
var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList();
_logger.LogWarning("Invalid attendance model received. \n {Error}", string.Join(",", errors));
_logger.LogError("Invalid attendance model received.");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
@ -781,7 +774,7 @@ namespace MarcoBMS.Services.Controllers
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "Error while recording attendance");
_logger.LogError("Error while recording attendance : {Error}", ex.Message);
return BadRequest(ApiResponse<object>.ErrorResponse("Something went wrong", ex.Message, 500));
}
}

View File

@ -1,4 +1,8 @@
using Marco.Pms.DataAccess.Data;
using System.Net;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Authentication;
using Marco.Pms.Model.Dtos.Authentication;
using Marco.Pms.Model.Dtos.Util;
@ -11,10 +15,6 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Net;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
namespace MarcoBMS.Services.Controllers
{
@ -110,7 +110,7 @@ namespace MarcoBMS.Services.Controllers
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error during login");
_logger.LogError("Unexpected error during login : {Error}", ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Unexpected error", ex.Message, 500));
}
}
@ -270,7 +270,7 @@ namespace MarcoBMS.Services.Controllers
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error occurred while verifying MPIN");
_logger.LogError("Unexpected error occurred while verifying MPIN : {Error}", ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Unexpected error", ex.Message, 500));
}
}
@ -307,7 +307,7 @@ namespace MarcoBMS.Services.Controllers
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error during logout");
_logger.LogError("Unexpected error during logout : {Error}", ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Unexpected error occurred", ex.Message, 500));
}
}
@ -351,7 +351,7 @@ namespace MarcoBMS.Services.Controllers
if (string.IsNullOrWhiteSpace(user.UserName))
{
_logger.LogWarning("Username missing for user ID: {UserId}", user.Id);
_logger.LogError("Username missing for user ID: {UserId}", user.Id);
return NotFound(ApiResponse<object>.ErrorResponse("Username not found.", "Username not found.", 404));
}
@ -370,7 +370,7 @@ namespace MarcoBMS.Services.Controllers
}
catch (Exception ex)
{
_logger.LogError(ex, "An unexpected error occurred during token refresh.");
_logger.LogError("An unexpected error occurred during token refresh. : {Error}", ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Unexpected error occurred.", ex.Message, 500));
}
}
@ -406,7 +406,7 @@ namespace MarcoBMS.Services.Controllers
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while sending password reset email to");
_logger.LogError("Error while sending password reset email to: {Error}", ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Error sending password reset email.", ex.Message, 500));
}
}
@ -480,7 +480,7 @@ namespace MarcoBMS.Services.Controllers
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while sending reset password success email to user");
_logger.LogError("Error while sending reset password success email to user: {Error}", ex.Message);
// Continue, do not fail because of email issue
}
@ -547,7 +547,7 @@ namespace MarcoBMS.Services.Controllers
}
catch (Exception ex)
{
_logger.LogError(ex, "An unexpected error occurred while sending OTP to {Email}", generateOTP.Email ?? "");
_logger.LogError("An unexpected error occurred while sending OTP to {Email} : {Error}", generateOTP.Email ?? "", ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("An unexpected error occurred.", ex.Message, 500));
}
}
@ -638,7 +638,7 @@ namespace MarcoBMS.Services.Controllers
}
catch (Exception ex)
{
_logger.LogError(ex, "An unexpected error occurred during OTP login for email {Email}", verifyOTP.Email ?? string.Empty);
_logger.LogError("An unexpected error occurred during OTP login for email {Email} : {Error}", verifyOTP.Email ?? string.Empty, ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Unexpected error", ex.Message, 500));
}
}
@ -719,7 +719,7 @@ namespace MarcoBMS.Services.Controllers
if (!result.Succeeded)
{
var errors = result.Errors.Select(e => e.Description).ToList();
_logger.LogWarning("Password reset failed for user {Email}. Errors: {Errors}", changePassword.Email, string.Join("; ", errors));
_logger.LogError("Password reset failed for user {Email}. Errors: {Errors}", changePassword.Email, string.Join("; ", errors));
return BadRequest(ApiResponse<object>.ErrorResponse("Failed to change password", errors, 400));
}
@ -732,7 +732,7 @@ namespace MarcoBMS.Services.Controllers
}
catch (Exception exp)
{
_logger.LogError(exp, "An unexpected error occurred while changing password");
_logger.LogError("An unexpected error occurred while changing password : {Error}", exp.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("An unexpected error occurred.", exp.Message, 500));
}
}
@ -752,7 +752,7 @@ namespace MarcoBMS.Services.Controllers
// Validate employee and MPIN input
if (requestEmployee == null || string.IsNullOrWhiteSpace(generateMPINDto.MPIN) || generateMPINDto.MPIN.Length != 6 || !generateMPINDto.MPIN.All(char.IsDigit))
{
_logger.LogWarning("Employee {EmployeeId} provided invalid information to generate MPIN", loggedInEmployee.Id);
_logger.LogError("Employee {EmployeeId} provided invalid information to generate MPIN", loggedInEmployee.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("Provided invalid information", "Provided invalid information", 400));
}

View File

@ -6,7 +6,6 @@ using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.DashBoard;
using Marco.Pms.Services.Service;
using Marco.Pms.Services.Service.ServiceInterfaces;
using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization;
@ -22,15 +21,15 @@ namespace Marco.Pms.Services.Controllers
{
private readonly ApplicationDbContext _context;
private readonly UserHelper _userHelper;
private readonly IProjectServices _projectServices;
private readonly ProjectsHelper _projectsHelper;
private readonly ILoggingService _logger;
private readonly PermissionServices _permissionServices;
public static readonly Guid ActiveId = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731");
public DashboardController(ApplicationDbContext context, UserHelper userHelper, IProjectServices projectServices, ILoggingService logger, PermissionServices permissionServices)
public DashboardController(ApplicationDbContext context, UserHelper userHelper, ProjectsHelper projectsHelper, ILoggingService logger, PermissionServices permissionServices)
{
_context = context;
_userHelper = userHelper;
_projectServices = projectServices;
_projectsHelper = projectsHelper;
_logger = logger;
_permissionServices = permissionServices;
}
@ -183,13 +182,11 @@ namespace Marco.Pms.Services.Controllers
// --- Step 1: Get the list of projects the user can access ---
// This query is more efficient as it only selects the IDs needed.
var projects = await _projectServices.GetMyProjectIdsAsync(tenantId, loggedInEmployee);
var accessibleActiveProjectIds = await _context.Projects
.Where(p => p.ProjectStatusId == ActiveId && projects.Contains(p.Id))
var projects = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee);
var accessibleActiveProjectIds = projects
.Where(p => p.ProjectStatusId == ActiveId)
.Select(p => p.Id)
.ToListAsync();
.ToList();
if (!accessibleActiveProjectIds.Any())
{
_logger.LogInfo("User {UserId} has no accessible active projects.", loggedInEmployee.Id);
@ -202,7 +199,7 @@ namespace Marco.Pms.Services.Controllers
if (projectId.HasValue)
{
// Security Check: Ensure the requested project is in the user's accessible list.
var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value);
var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value.ToString());
if (!hasPermission)
{
_logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId} (not active or not accessible).", loggedInEmployee.Id, projectId.Value);
@ -253,7 +250,7 @@ namespace Marco.Pms.Services.Controllers
}
catch (Exception ex)
{
_logger.LogError(ex, "An unexpected error occurred in GetTotalEmployees for projectId {ProjectId}", projectId ?? Guid.Empty);
_logger.LogError("An unexpected error occurred in GetTotalEmployees for projectId {ProjectId} \n {Error}", projectId ?? Guid.Empty, ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred.", null, 500));
}
}
@ -284,7 +281,7 @@ namespace Marco.Pms.Services.Controllers
// --- Logic for a SINGLE Project ---
// 2a. Security Check: Verify permission for the specific project.
var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value);
var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value.ToString());
if (!hasPermission)
{
_logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, projectId.Value);
@ -304,8 +301,8 @@ namespace Marco.Pms.Services.Controllers
// --- Logic for ALL Accessible Projects ---
// 2c. Get a list of all projects the user is allowed to see.
var accessibleProjectIds = await _projectServices.GetMyProjectIdsAsync(tenantId, loggedInEmployee);
var accessibleProject = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee);
var accessibleProjectIds = accessibleProject.Select(p => p.Id).ToList();
if (!accessibleProjectIds.Any())
{
_logger.LogInfo("User {UserId} has no accessible projects.", loggedInEmployee.Id);
@ -344,7 +341,7 @@ namespace Marco.Pms.Services.Controllers
}
catch (Exception ex)
{
_logger.LogError(ex, "An unexpected error occurred in GetTotalTasks for projectId {ProjectId}", projectId ?? Guid.Empty);
_logger.LogError("An unexpected error occurred in GetTotalTasks for projectId {ProjectId} \n {Error}", projectId ?? Guid.Empty, ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred.", null, 500));
}
}
@ -367,7 +364,7 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Number of pending regularization and pending check-out are fetched successfully for employee {EmployeeId}", LoggedInEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(response, "Pending regularization and pending check-out are fetched successfully", 200));
}
_logger.LogWarning("No attendance entry was found for employee {EmployeeId}", LoggedInEmployee.Id);
_logger.LogError("No attendance entry was found for employee {EmployeeId}", LoggedInEmployee.Id);
return NotFound(ApiResponse<object>.ErrorResponse("No attendance entry was found for this employee", "No attendance entry was found for this employee", 404));
}
@ -381,14 +378,14 @@ namespace Marco.Pms.Services.Controllers
List<ProjectProgressionVM>? projectProgressionVMs = new List<ProjectProgressionVM>();
if (date != null && DateTime.TryParse(date, out currentDate) == false)
{
_logger.LogWarning($"user send invalid date");
_logger.LogError($"user send invalid date");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid date.", "Invalid date.", 400));
}
Project? project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == projectId);
if (project == null)
{
_logger.LogWarning("Employee {EmployeeId} was attempted to get project attendance for date {Date}, but project not found in database", LoggedInEmployee.Id, currentDate);
_logger.LogError("Employee {EmployeeId} was attempted to get project attendance for date {Date}, but project not found in database", LoggedInEmployee.Id, currentDate);
return NotFound(ApiResponse<object>.ErrorResponse("Project not found", "Project not found", 404));
}
List<ProjectAllocation>? projectAllocation = await _context.ProjectAllocations.Where(p => p.ProjectId == projectId && p.IsActive && p.TenantId == tenantId).ToListAsync();
@ -434,14 +431,14 @@ namespace Marco.Pms.Services.Controllers
DateTime currentDate = DateTime.UtcNow;
if (date != null && DateTime.TryParse(date, out currentDate) == false)
{
_logger.LogWarning($"user send invalid date");
_logger.LogError($"user send invalid date");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid date.", "Invalid date.", 400));
}
Project? project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == projectId);
if (project == null)
{
_logger.LogWarning("Employee {EmployeeId} was attempted to get activities performed for date {Date}, but project not found in database", LoggedInEmployee.Id, currentDate);
_logger.LogError("Employee {EmployeeId} was attempted to get activities performed for date {Date}, but project not found in database", LoggedInEmployee.Id, currentDate);
return NotFound(ApiResponse<object>.ErrorResponse("Project not found", "Project not found", 404));
}
@ -519,7 +516,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

@ -77,7 +77,7 @@ namespace Marco.Pms.Services.Controllers
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
_logger.LogWarning("User sent Invalid Date while marking attendance");
_logger.LogError("User sent Invalid Date while marking attendance");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
var response = await _directoryHelper.CreateContact(createContact);
@ -256,7 +256,7 @@ namespace Marco.Pms.Services.Controllers
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
_logger.LogWarning("User sent Invalid Date while marking attendance");
_logger.LogError("User sent Invalid Date while marking attendance");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
var response = await _directoryHelper.CreateBucket(bucketDto);

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;
@ -9,7 +11,6 @@ using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Employee;
using Marco.Pms.Services.Hubs;
using Marco.Pms.Services.Service;
using Marco.Pms.Services.Service.ServiceInterfaces;
using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization;
@ -17,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
{
@ -38,13 +37,13 @@ namespace MarcoBMS.Services.Controllers
private readonly ILoggingService _logger;
private readonly IHubContext<MarcoHub> _signalR;
private readonly PermissionServices _permission;
private readonly IProjectServices _projectServices;
private readonly ProjectsHelper _projectsHelper;
private readonly Guid tenantId;
public EmployeeController(UserManager<ApplicationUser> userManager, IEmailSender emailSender,
ApplicationDbContext context, EmployeeHelper employeeHelper, UserHelper userHelper, IConfiguration configuration, ILoggingService logger,
IHubContext<MarcoHub> signalR, PermissionServices permission, IProjectServices projectServices)
IHubContext<MarcoHub> signalR, PermissionServices permission, ProjectsHelper projectsHelper)
{
_context = context;
_userManager = userManager;
@ -55,7 +54,7 @@ namespace MarcoBMS.Services.Controllers
_logger = logger;
_signalR = signalR;
_permission = permission;
_projectServices = projectServices;
_projectsHelper = projectsHelper;
tenantId = _userHelper.GetTenantId();
}
@ -120,7 +119,8 @@ namespace MarcoBMS.Services.Controllers
loggedInEmployee.Id, projectid ?? Guid.Empty, ShowInactive);
// Step 3: Fetch project access and permissions
var projectIds = await _projectServices.GetMyProjectIdsAsync(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);
@ -383,7 +383,7 @@ namespace MarcoBMS.Services.Controllers
Employee? existingEmployee = await _context.Employees.FirstOrDefaultAsync(e => e.Id == model.Id.Value);
if (existingEmployee == null)
{
_logger.LogWarning("User tries to update employee {EmployeeId} but not found in database", model.Id);
_logger.LogError("User tries to update employee {EmployeeId} but not found in database", model.Id);
return NotFound(ApiResponse<object>.ErrorResponse("Employee not found", "Employee not found", 404));
}
byte[]? imageBytes = null;
@ -496,7 +496,7 @@ namespace MarcoBMS.Services.Controllers
}
else
{
_logger.LogWarning("Employee with ID {EmploueeId} not found in database", id);
_logger.LogError("Employee with ID {EmploueeId} not found in database", id);
}
return Ok(ApiResponse<object>.SuccessResponse(new { }, "Employee Suspended successfully", 200));
}

View File

@ -44,7 +44,7 @@ namespace Marco.Pms.Services.Controllers
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
_logger.LogWarning("{error}", errors);
_logger.LogError("{error}", errors);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
Guid tenantId = _userHelper.GetTenantId();
@ -66,7 +66,7 @@ namespace Marco.Pms.Services.Controllers
var Image = attachmentDto;
if (string.IsNullOrEmpty(Image.Base64Data))
{
_logger.LogWarning("Base64 data is missing");
_logger.LogError("Base64 data is missing");
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
}
@ -160,7 +160,7 @@ namespace Marco.Pms.Services.Controllers
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
_logger.LogWarning("{error}", errors);
_logger.LogError("{error}", errors);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
Guid tenantId = _userHelper.GetTenantId();
@ -197,7 +197,7 @@ namespace Marco.Pms.Services.Controllers
var Image = attachmentDto;
if (string.IsNullOrEmpty(Image.Base64Data))
{
_logger.LogWarning("Base64 data is missing");
_logger.LogError("Base64 data is missing");
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
}
@ -336,7 +336,7 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Ticket {TicketId} updated", updateTicketDto.Id);
return Ok(ApiResponse<object>.SuccessResponse(ticketVM, "Ticket Updated Successfully", 200));
}
_logger.LogWarning("Ticket {TicketId} not Found in database", updateTicketDto.Id);
_logger.LogError("Ticket {TicketId} not Found in database", updateTicketDto.Id);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket not Found", "Ticket not Found", 404));
}
@ -349,7 +349,7 @@ namespace Marco.Pms.Services.Controllers
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
_logger.LogWarning("{error}", errors);
_logger.LogError("{error}", errors);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
@ -364,7 +364,7 @@ namespace Marco.Pms.Services.Controllers
if (ticket == null)
{
_logger.LogWarning("Ticket {TicketId} not Found in database", addCommentDto.TicketId);
_logger.LogError("Ticket {TicketId} not Found in database", addCommentDto.TicketId);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket not Found", "Ticket not Found", 404));
}
@ -379,7 +379,7 @@ namespace Marco.Pms.Services.Controllers
var Image = attachmentDto;
if (string.IsNullOrEmpty(Image.Base64Data))
{
_logger.LogWarning("Base64 data is missing");
_logger.LogError("Base64 data is missing");
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
}
@ -437,7 +437,7 @@ namespace Marco.Pms.Services.Controllers
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
_logger.LogWarning("{error}", errors);
_logger.LogError("{error}", errors);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
@ -451,7 +451,7 @@ namespace Marco.Pms.Services.Controllers
if (ticket == null)
{
_logger.LogWarning("Ticket {TicketId} not Found in database", updateCommentDto.TicketId);
_logger.LogError("Ticket {TicketId} not Found in database", updateCommentDto.TicketId);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket not Found", "Ticket not Found", 404));
}
@ -474,7 +474,7 @@ namespace Marco.Pms.Services.Controllers
var Image = attachmentDto;
if (string.IsNullOrEmpty(Image.Base64Data))
{
_logger.LogWarning("Base64 data is missing");
_logger.LogError("Base64 data is missing");
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
}
@ -552,7 +552,7 @@ namespace Marco.Pms.Services.Controllers
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
_logger.LogWarning("{error}", errors);
_logger.LogError("{error}", errors);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
@ -568,7 +568,7 @@ namespace Marco.Pms.Services.Controllers
if (tickets == null || tickets.Count > 0)
{
_logger.LogWarning("Tickets not Found in database");
_logger.LogError("Tickets not Found in database");
return NotFound(ApiResponse<object>.ErrorResponse("Ticket not Found", "Ticket not Found", 404));
}
@ -578,12 +578,12 @@ namespace Marco.Pms.Services.Controllers
{
if (string.IsNullOrEmpty(forumAttachmentDto.Base64Data))
{
_logger.LogWarning("Base64 data is missing");
_logger.LogError("Base64 data is missing");
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
}
if (forumAttachmentDto.TicketId == null)
{
_logger.LogWarning("ticket ID is missing");
_logger.LogError("ticket ID is missing");
return BadRequest(ApiResponse<object>.ErrorResponse("ticket ID is missing", "ticket ID is missing", 400));
}
var ticket = tickets.FirstOrDefault(t => t.Id == forumAttachmentDto.TicketId);

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

@ -168,7 +168,7 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("activity updated successfully from tenant {tenantId}", tenantId);
return Ok(ApiResponse<object>.SuccessResponse(activityVM, "activity updated successfully", 200));
}
_logger.LogWarning("Activity {ActivityId} not found", id);
_logger.LogError("Activity {ActivityId} not found", id);
return NotFound(ApiResponse<object>.ErrorResponse("Activity not found", "Activity not found", 404));
}
@ -230,7 +230,7 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Ticket Status master {TicketStatusId} added successfully from tenant {tenantId}", statusMaster.Id, tenantId);
return Ok(ApiResponse<object>.SuccessResponse(statusVM, "Ticket Status master added successfully", 200));
}
_logger.LogWarning("User sent empyt payload");
_logger.LogError("User sent empyt payload");
return BadRequest(ApiResponse<object>.ErrorResponse("Sent Empty payload", "Sent Empty payload", 400));
}
@ -251,10 +251,10 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Ticket Status master {TicketStatusId} updated successfully from tenant {tenantId}", statusMaster.Id, tenantId);
return Ok(ApiResponse<object>.SuccessResponse(statusVM, "Ticket Status master updated successfully", 200));
}
_logger.LogWarning("Ticket Status master {TicketStatusId} not found in database", statusMasterDto.Id != null ? statusMasterDto.Id.Value : Guid.Empty);
_logger.LogError("Ticket Status master {TicketStatusId} not found in database", statusMasterDto.Id != null ? statusMasterDto.Id.Value : Guid.Empty);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket Status master not found", "Ticket Status master not found", 404));
}
_logger.LogWarning("User sent empyt payload");
_logger.LogError("User sent empyt payload");
return BadRequest(ApiResponse<object>.ErrorResponse("Sent Empty payload", "Sent Empty payload", 400));
}
@ -281,7 +281,7 @@ namespace Marco.Pms.Services.Controllers
}
else
{
_logger.LogWarning("Ticket Status {TickeStatusId} not found in database", id);
_logger.LogError("Ticket Status {TickeStatusId} not found in database", id);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket Status not found", "Ticket Status not found", 404));
}
}
@ -318,7 +318,7 @@ namespace Marco.Pms.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(typeVM, "Ticket type master added successfully", 200));
}
_logger.LogWarning("User sent empyt payload");
_logger.LogError("User sent empyt payload");
return BadRequest(ApiResponse<object>.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400));
}
@ -339,10 +339,10 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Ticket Type master {TicketTypeId} updated successfully from tenant {tenantId}", typeMaster.Id, tenantId);
return Ok(ApiResponse<object>.SuccessResponse(typeVM, "Ticket type master updated successfully", 200));
}
_logger.LogWarning("Ticket type master {TicketTypeId} not found in database", typeMasterDto.Id != null ? typeMasterDto.Id.Value : Guid.Empty);
_logger.LogError("Ticket type master {TicketTypeId} not found in database", typeMasterDto.Id != null ? typeMasterDto.Id.Value : Guid.Empty);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket type master not found", "Ticket type master not found", 404));
}
_logger.LogWarning("User sent empyt payload");
_logger.LogError("User sent empyt payload");
return BadRequest(ApiResponse<object>.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400));
}
@ -369,7 +369,7 @@ namespace Marco.Pms.Services.Controllers
}
else
{
_logger.LogWarning("Ticket Type {TickeTypeId} not found in database", id);
_logger.LogError("Ticket Type {TickeTypeId} not found in database", id);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket Type not found", "Ticket Type not found", 404));
}
}
@ -407,7 +407,7 @@ namespace Marco.Pms.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(typeVM, "Ticket Priority master added successfully", 200));
}
_logger.LogWarning("User sent empyt payload");
_logger.LogError("User sent empyt payload");
return BadRequest(ApiResponse<object>.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400));
}
[HttpPost("ticket-priorities/edit/{id}")]
@ -427,10 +427,10 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Ticket Priority master {TicketPriorityId} updated successfully from tenant {tenantId}", typeMaster.Id, tenantId);
return Ok(ApiResponse<object>.SuccessResponse(typeVM, "Ticket Priority master updated successfully", 200));
}
_logger.LogWarning("Ticket Priority master {TicketPriorityId} not found in database", priorityMasterDto.Id != null ? priorityMasterDto.Id.Value : Guid.Empty);
_logger.LogError("Ticket Priority master {TicketPriorityId} not found in database", priorityMasterDto.Id != null ? priorityMasterDto.Id.Value : Guid.Empty);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket Priority master not found", "Ticket Priority master not found", 404));
}
_logger.LogWarning("User sent empyt payload");
_logger.LogError("User sent empyt payload");
return BadRequest(ApiResponse<object>.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400));
}
@ -457,7 +457,7 @@ namespace Marco.Pms.Services.Controllers
}
else
{
_logger.LogWarning("Ticket Priority {TickePriorityId} not found in database", id);
_logger.LogError("Ticket Priority {TickePriorityId} not found in database", id);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket Priority not found", "Ticket Priority not found", 404));
}
}
@ -494,7 +494,7 @@ namespace Marco.Pms.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(typeVM, "Ticket tag master added successfully", 200));
}
_logger.LogWarning("User sent empyt payload");
_logger.LogError("User sent empyt payload");
return BadRequest(ApiResponse<object>.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400));
}
@ -515,10 +515,10 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Ticket Tag master {TicketTypeId} updated successfully from tenant {tenantId}", tagMaster.Id, tenantId);
return Ok(ApiResponse<object>.SuccessResponse(typeVM, "Ticket tag master updated successfully", 200));
}
_logger.LogWarning("Ticket tag master {TicketTypeId} not found in database", tagMasterDto.Id != null ? tagMasterDto.Id.Value : Guid.Empty);
_logger.LogError("Ticket tag master {TicketTypeId} not found in database", tagMasterDto.Id != null ? tagMasterDto.Id.Value : Guid.Empty);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket tag master not found", "Ticket tag master not found", 404));
}
_logger.LogWarning("User sent empyt payload");
_logger.LogError("User sent empyt payload");
return BadRequest(ApiResponse<object>.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400));
}
@ -545,7 +545,7 @@ namespace Marco.Pms.Services.Controllers
}
else
{
_logger.LogWarning("Ticket Tag {TickeTagId} not found in database", id);
_logger.LogError("Ticket Tag {TickeTagId} not found in database", id);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket tag not found", "Ticket tag not found", 404));
}
}
@ -609,7 +609,7 @@ namespace Marco.Pms.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(workCategoryMasterVM, "Work category master added successfully", 200));
}
_logger.LogWarning("User sent empyt payload");
_logger.LogError("User sent empyt payload");
return BadRequest(ApiResponse<object>.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400));
}
@ -624,7 +624,7 @@ namespace Marco.Pms.Services.Controllers
{
if (workCategory.IsSystem)
{
_logger.LogWarning("User tries to update system-defined work category");
_logger.LogError("User tries to update system-defined work category");
return BadRequest(ApiResponse<object>.ErrorResponse("Cannot update system-defined work", "Cannot update system-defined work", 400));
}
workCategory = workCategoryMasterDto.ToWorkCategoryMasterFromWorkCategoryMasterDto(tenantId);
@ -635,10 +635,10 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Work category master {WorkCategoryId} updated successfully from tenant {tenantId}", workCategory.Id, tenantId);
return Ok(ApiResponse<object>.SuccessResponse(workCategoryMasterVM, "Work category master updated successfully", 200));
}
_logger.LogWarning("Work category master {WorkCategoryId} not found in database", workCategoryMasterDto.Id ?? Guid.Empty);
_logger.LogError("Work category master {WorkCategoryId} not found in database", workCategoryMasterDto.Id ?? Guid.Empty);
return NotFound(ApiResponse<object>.ErrorResponse("Work category master not found", "Work category master not found", 404));
}
_logger.LogWarning("User sent empyt payload");
_logger.LogError("User sent empyt payload");
return BadRequest(ApiResponse<object>.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400));
}
@ -666,7 +666,7 @@ namespace Marco.Pms.Services.Controllers
}
else
{
_logger.LogWarning("Work category {WorkCategoryId} not found in database", id);
_logger.LogError("Work category {WorkCategoryId} not found in database", id);
return NotFound(ApiResponse<object>.ErrorResponse("Work category not found", "Work category not found", 404));
}
}
@ -689,7 +689,7 @@ namespace Marco.Pms.Services.Controllers
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
_logger.LogWarning("User sent Invalid Date while marking attendance");
_logger.LogError("User sent Invalid Date while marking attendance");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
var response = await _masterHelper.CreateWorkStatus(createWorkStatusDto);
@ -803,7 +803,7 @@ namespace Marco.Pms.Services.Controllers
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
_logger.LogWarning("User sent Invalid Date while marking attendance");
_logger.LogError("User sent Invalid Date while marking attendance");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
var response = await _masterHelper.CreateContactTag(contactTagDto);

File diff suppressed because it is too large Load Diff

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(ex, "Database Error: Failed to check existence of MailListId '{MailListId}' for TenantId: {TenantId}.", mailDetailsDto.MailListId, tenantId);
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(dbEx, "Database Error: Failed to save new mail details for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}.", newMailDetails.ProjectId, newMailDetails.Recipient, tenantId);
// Check for specific constraint violations if applicable (e.g., duplicate recipient for a project)
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while saving the mail details.", 500));
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected Error: An unhandled exception occurred while adding mail details for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}.", newMailDetails.ProjectId, newMailDetails.Recipient, tenantId);
return StatusCode(500, ApiResponse<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,298 +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(ex, "Database Error: Failed to check for existing mail template with title '{Title}' for TenantId: {TenantId}.", mailTemplateDto.Title, tenantId);
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(dbEx, "Database Error: Failed to save new mail template '{Title}' for TenantId: {TenantId}. : {Error}", mailTemplateDto.Title, tenantId);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while saving the mail template.", 500));
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected Error: An unhandled exception occurred while adding mail template '{Title}' for TenantId: {TenantId}.", mailTemplateDto.Title, tenantId);
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(ex, "Failed to send report for project {ProjectId}", mailGroup.ProjectId);
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-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(ex, "Failed to process project report for ProjectId {ProjectId}", group.ProjectId);
}
}
});
// 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

@ -4,7 +4,6 @@ using Marco.Pms.Model.Mapper;
using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Employee;
using Marco.Pms.Services.Service.ServiceInterfaces;
using MarcoBMS.Services.Helpers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@ -20,14 +19,14 @@ namespace MarcoBMS.Services.Controllers
private readonly UserHelper _userHelper;
private readonly EmployeeHelper _employeeHelper;
private readonly IProjectServices _projectServices;
private readonly ProjectsHelper _projectsHelper;
private readonly RolesHelper _rolesHelper;
public UserController(EmployeeHelper employeeHelper, IProjectServices projectServices, UserHelper userHelper, RolesHelper rolesHelper)
public UserController(EmployeeHelper employeeHelper, ProjectsHelper projectsHelper, UserHelper userHelper, RolesHelper rolesHelper)
{
_userHelper = userHelper;
_employeeHelper = employeeHelper;
_projectServices = projectServices;
_projectsHelper = projectsHelper;
_rolesHelper = rolesHelper;
}
@ -51,18 +50,18 @@ namespace MarcoBMS.Services.Controllers
emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id);
}
List<FeaturePermission> featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeId(emp.Id);
List<FeaturePermission> featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(emp.Id);
string[] projectsId = [];
/* User with permission manage project can see all projects */
if (featurePermission != null && featurePermission.Exists(c => c.Id.ToString() == "172fc9b6-755b-4f62-ab26-55c34a330614"))
{
List<Project> projects = await _projectServices.GetAllProjectByTanentID(emp.TenantId);
List<Project> projects = await _projectsHelper.GetAllProjectByTanentID(emp.TenantId);
projectsId = projects.Select(c => c.Id.ToString()).ToArray();
}
else
{
List<ProjectAllocation> allocation = await _projectServices.GetProjectByEmployeeID(emp.Id);
List<ProjectAllocation> allocation = await _projectsHelper.GetProjectByEmployeeID(emp.Id);
projectsId = allocation.Select(c => c.ProjectId.ToString()).ToArray();
}
EmployeeProfile profile = new EmployeeProfile() { };

View File

@ -1,10 +1,7 @@
using Marco.Pms.CacheHelper;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Master;
using Marco.Pms.Model.MongoDBModels;
using Marco.Pms.Model.Projects;
using MarcoBMS.Services.Service;
using Microsoft.EntityFrameworkCore;
using Project = Marco.Pms.Model.Projects.Project;
namespace Marco.Pms.Services.Helpers
@ -13,423 +10,37 @@ 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;
private readonly ApplicationDbContext _context;
private readonly GeneralHelper _generalHelper;
public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache, ReportCache reportCache, ILoggingService logger,
IDbContextFactory<ApplicationDbContext> dbContextFactory, ApplicationDbContext context, GeneralHelper generalHelper)
public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache, ILoggingService logger)
{
_projectCache = projectCache;
_employeeCache = employeeCache;
_reportCache = reportCache;
_logger = logger;
_dbContextFactory = dbContextFactory;
_context = context;
_generalHelper = generalHelper;
}
// ------------------------------------ Project Details Cache ---------------------------------------
// ------------------------------------ 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.LogError(ex, "Error occurred while adding project {ProjectId} to Cache", project.Id);
}
}
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.LogError(ex, "Error occurred while adding project list to Cache");
_logger.LogWarning("Error occured while adding project {ProjectId} to Cache : {Error}", project.Id, ex.Message);
}
}
public async Task<bool> UpdateProjectDetailsOnly(Project project)
{
StatusMaster projectStatus = await _context.StatusMasters
.FirstOrDefaultAsync(s => s.Id == project.ProjectStatusId) ?? new StatusMaster();
try
{
bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project, projectStatus);
bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while updating project {ProjectId} to Cache", project.Id);
_logger.LogWarning("Error occured while updating project {ProjectId} to Cache: {Error}", project.Id, ex.Message);
return false;
}
}
@ -442,20 +53,7 @@ namespace Marco.Pms.Services.Helpers
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while getting project {ProjectId} to Cache");
return null;
}
}
public async Task<ProjectMongoDB?> GetProjectDetailsWithBuildings(Guid projectId)
{
try
{
var response = await _projectCache.GetProjectDetailsWithBuildingsFromCache(projectId);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while getting project {ProjectId} to Cache");
_logger.LogWarning("Error occured while getting project {ProjectId} to Cache: {Error}", ex.Message);
return null;
}
}
@ -464,48 +62,14 @@ 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)
{
_logger.LogError(ex, "Error occured while getting list of project details from to Cache");
_logger.LogWarning("Error occured while getting list od project details from to Cache: {Error}", ex.Message);
return null;
}
}
public async Task DeleteProjectByIdAsync(Guid projectId)
{
try
{
var response = await _projectCache.DeleteProjectByIdFromCacheAsync(projectId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while deleting project from to Cache");
}
}
public async Task RemoveProjectsAsync(List<Guid> projectIds)
{
try
{
var response = await _projectCache.RemoveProjectsFromCacheAsync(projectIds);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while deleting project list from to Cache");
}
}
// ------------------------------------ Project Infrastructure Cache ---------------------------------------
public async Task AddBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null)
{
try
@ -569,9 +133,6 @@ namespace Marco.Pms.Services.Helpers
return null;
}
}
// ------------------------------------------------------- WorkItem -------------------------------------------------------
public async Task<List<WorkItemMongoDB>?> GetWorkItemsByWorkAreaIds(List<Guid> workAreaIds)
{
try
@ -589,20 +150,10 @@ namespace Marco.Pms.Services.Helpers
return null;
}
}
// ------------------------------------------------------- WorkItem -------------------------------------------------------
public async Task ManageWorkItemDetails(List<WorkItem> workItems)
{
try
{
var workAreaId = workItems.First().WorkAreaId;
var workItemDB = await _generalHelper.GetWorkItemsListFromDB(workAreaId);
await _projectCache.ManageWorkItemDetailsToCache(workItemDB);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while saving workItems form Cache: {Error}", ex.Message);
}
}
public async Task ManageWorkItemDetailsByVM(List<WorkItemMongoDB> workItems)
{
try
{
@ -664,47 +215,14 @@ namespace Marco.Pms.Services.Helpers
_logger.LogWarning("Error occured while updating planned work, completed work, and today's assigned work in workItems in Cache: {Error}", ex.Message);
}
}
public async Task DeleteWorkItemByIdAsync(Guid workItemId)
{
try
{
var response = await _projectCache.DeleteWorkItemByIdFromCacheAsync(workItemId);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while deleting work item from to Cache: {Error}", ex.Message);
}
}
// ------------------------------------ Employee Profile Cache ---------------------------------------
public async Task AddApplicationRole(Guid employeeId, List<Guid> roleIds)
{
// 1. Guard Clause: Avoid unnecessary database work if there are no roles to add.
if (roleIds == null || !roleIds.Any())
{
return; // Nothing to add, so the operation did not result in a change.
}
Task<List<string>> getPermissionIdsTask = Task.Run(async () =>
{
using var context = _dbContextFactory.CreateDbContext();
return await context.RolePermissionMappings
.Where(rp => roleIds.Contains(rp.ApplicationRoleId))
.Select(p => p.FeaturePermissionId.ToString())
.Distinct()
.ToListAsync();
});
// 3. Prepare role IDs in parallel with the database query.
var newRoleIds = roleIds.Select(r => r.ToString()).ToList();
// 4. Await the database query result.
var newPermissionIds = await getPermissionIdsTask;
try
{
var response = await _employeeCache.AddApplicationRoleToCache(employeeId, newRoleIds, newPermissionIds);
var response = await _employeeCache.AddApplicationRoleToCache(employeeId, roleIds);
}
catch (Exception ex)
{
@ -824,44 +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);
}
}
public async Task ClearAllEmployees()
{
try
{
var response = await _employeeCache.ClearAllEmployeesFromCache();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while deleting all employees from Cache");
}
}
// ------------------------------------ Report Cache ---------------------------------------
public async Task<List<ProjectReportEmailMongoDB>?> GetProjectReportMail(bool IsSend)
{
try
{
var response = await _reportCache.GetProjectReportMailFromCache(IsSend);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while fetching project report mail bodys");
return null;
}
}
public async Task AddProjectReportMail(ProjectReportEmailMongoDB report)
{
try
{
await _reportCache.AddProjectReportMailToCache(report);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while adding project report mail bodys");
}
}
}
}

View File

@ -52,7 +52,7 @@ namespace Marco.Pms.Services.Helpers
}
else
{
_logger.LogWarning("Employee {EmployeeId} attemped to access a contacts, but do not have permission", LoggedInEmployee.Id);
_logger.LogError("Employee {EmployeeId} attemped to access a contacts, but do not have permission", LoggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("You don't have permission", "You don't have permission", 401);
}
@ -202,7 +202,7 @@ namespace Marco.Pms.Services.Helpers
}
else
{
_logger.LogWarning("Employee {EmployeeId} attemped to access a contacts with in bucket {BucketId}, but do not have permission", LoggedInEmployee.Id, id);
_logger.LogError("Employee {EmployeeId} attemped to access a contacts with in bucket {BucketId}, but do not have permission", LoggedInEmployee.Id, id);
return ApiResponse<object>.ErrorResponse("You don't have permission", "You don't have permission", 401);
}
@ -490,7 +490,7 @@ namespace Marco.Pms.Services.Helpers
}
else
{
_logger.LogWarning("Employee {EmployeeId} attemped to update a contact, but do not have permission", LoggedInEmployee.Id);
_logger.LogError("Employee {EmployeeId} attemped to update a contact, but do not have permission", LoggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("You don't have permission", "You don't have permission", 401);
}
@ -1157,12 +1157,11 @@ namespace Marco.Pms.Services.Helpers
List<EmployeeBucketMapping> employeeBuckets = await _context.EmployeeBucketMappings.Where(b => b.EmployeeId == LoggedInEmployee.Id).ToListAsync();
var bucketIds = employeeBuckets.Select(b => b.BucketId).ToList();
List<EmployeeBucketMapping> employeeBucketVM = await _context.EmployeeBucketMappings.Where(b => bucketIds.Contains(b.BucketId)).ToListAsync();
List<Bucket> bucketList = new List<Bucket>();
if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin))
{
bucketList = await _context.Buckets.Include(b => b.CreatedBy).Where(b => b.TenantId == tenantId).ToListAsync();
bucketIds = bucketList.Select(b => b.Id).ToList();
}
else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) || permissionIds.Contains(PermissionsMaster.DirectoryUser))
{
@ -1170,12 +1169,10 @@ namespace Marco.Pms.Services.Helpers
}
else
{
_logger.LogWarning("Employee {EmployeeId} attemped to access a buckets list, but do not have permission", LoggedInEmployee.Id);
_logger.LogError("Employee {EmployeeId} attemped to access a buckets list, but do not have permission", LoggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("You don't have permission", "You don't have permission", 401);
}
List<EmployeeBucketMapping> employeeBucketVM = await _context.EmployeeBucketMappings.Where(b => bucketIds.Contains(b.BucketId)).ToListAsync();
List<AssignBucketVM> bucketVMs = new List<AssignBucketVM>();
if (bucketList.Any())
{
@ -1187,11 +1184,7 @@ namespace Marco.Pms.Services.Helpers
var emplyeeIds = employeeBucketMappings.Select(eb => eb.EmployeeId).ToList();
List<ContactBucketMapping>? contactBuckets = contactBucketMappings.Where(cb => cb.BucketId == bucket.Id).ToList();
AssignBucketVM bucketVM = bucket.ToAssignBucketVMFromBucket();
if (bucketVM.CreatedBy != null)
{
emplyeeIds.Add(bucketVM.CreatedBy.Id);
}
bucketVM.EmployeeIds = emplyeeIds.Distinct().ToList();
bucketVM.EmployeeIds = emplyeeIds;
bucketVM.NumberOfContacts = contactBuckets.Count;
bucketVMs.Add(bucketVM);
}
@ -1211,7 +1204,7 @@ namespace Marco.Pms.Services.Helpers
var demo = !permissionIds.Contains(PermissionsMaster.DirectoryUser);
if (!permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && !permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && !permissionIds.Contains(PermissionsMaster.DirectoryUser))
{
_logger.LogWarning("Employee {EmployeeId} attemped to create a bucket, but do not have permission", LoggedInEmployee.Id);
_logger.LogError("Employee {EmployeeId} attemped to create a bucket, but do not have permission", LoggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("You don't have permission", "You don't have permission", 401);
}
@ -1283,7 +1276,7 @@ namespace Marco.Pms.Services.Helpers
}
if (accessableBucket == null)
{
_logger.LogWarning("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id);
_logger.LogError("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id);
return ApiResponse<object>.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 401);
}
@ -1349,7 +1342,7 @@ namespace Marco.Pms.Services.Helpers
}
if (accessableBucket == null)
{
_logger.LogWarning("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id);
_logger.LogError("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id);
return ApiResponse<object>.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 401);
}
var employeeIds = await _context.Employees.Where(e => e.TenantId == tenantId && e.IsActive).Select(e => e.Id).ToListAsync();
@ -1369,7 +1362,7 @@ namespace Marco.Pms.Services.Helpers
_context.EmployeeBucketMappings.Add(employeeBucketMapping);
assignedEmployee += 1;
}
else if (!assignBucket.IsActive)
else
{
EmployeeBucketMapping? employeeBucketMapping = employeeBuckets.FirstOrDefault(eb => eb.BucketId == bucketId && eb.EmployeeId == assignBucket.EmployeeId);
if (employeeBucketMapping != null)
@ -1403,7 +1396,7 @@ namespace Marco.Pms.Services.Helpers
}
if (removededEmployee > 0)
{
_logger.LogWarning("Employee {EmployeeId} removed {conut} number of employees from bucket {BucketId}", LoggedInEmployee.Id, removededEmployee, bucketId);
_logger.LogError("Employee {EmployeeId} removed {conut} number of employees from bucket {BucketId}", LoggedInEmployee.Id, removededEmployee, bucketId);
}
return ApiResponse<object>.SuccessResponse(bucketVM, "Details updated successfully", 200);
}
@ -1450,7 +1443,7 @@ namespace Marco.Pms.Services.Helpers
}
if (accessableBucket == null)
{
_logger.LogWarning("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id);
_logger.LogError("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id);
return ApiResponse<object>.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 401);
}

View File

@ -33,7 +33,7 @@ namespace MarcoBMS.Services.Helpers
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while fetching employee by application user ID {ApplicationUserId}", ApplicationUserID);
_logger.LogError("{Error}", ex.Message);
return new Employee();
}
}
@ -66,7 +66,7 @@ namespace MarcoBMS.Services.Helpers
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occoured while filtering employees by string {SearchString} or project {ProjectId}", searchString, ProjectId ?? Guid.Empty);
_logger.LogError("{Error}", ex.Message);
return new List<EmployeeVM>();
}
}
@ -102,7 +102,7 @@ namespace MarcoBMS.Services.Helpers
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while featching list of employee by project ID {ProjectId}", ProjectId ?? Guid.Empty);
_logger.LogError("{Error}", ex.Message);
return new List<EmployeeVM>();
}
}

View File

@ -1,214 +0,0 @@
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.MongoDBModels;
using MarcoBMS.Services.Service;
using Microsoft.EntityFrameworkCore;
namespace Marco.Pms.Services.Helpers
{
public class GeneralHelper
{
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
private readonly ApplicationDbContext _context; // Keeping this for direct scoped context use where appropriate
private readonly ILoggingService _logger;
public GeneralHelper(IDbContextFactory<ApplicationDbContext> dbContextFactory,
ApplicationDbContext context,
ILoggingService logger)
{
_dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory));
_context = context ?? throw new ArgumentNullException(nameof(context));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<List<BuildingMongoDB>> GetProjectInfraFromDB(Guid projectId)
{
// Each task uses its own DbContext instance for thread safety. Projections are used for efficiency.
// Task to fetch Buildings, Floors, and WorkAreas using projections
var hierarchyTask = Task.Run(async () =>
{
using var context = _dbContextFactory.CreateDbContext();
var buildings = await context.Buildings.AsNoTracking().Where(b => b.ProjectId == projectId).Select(b => new { b.Id, b.Name, b.Description }).ToListAsync();
var buildingIds = buildings.Select(b => b.Id).ToList();
var floors = await context.Floor.AsNoTracking().Where(f => buildingIds.Contains(f.BuildingId)).Select(f => new { f.Id, f.BuildingId, f.FloorName }).ToListAsync();
var floorIds = floors.Select(f => f.Id).ToList();
var workAreas = await context.WorkAreas.AsNoTracking().Where(wa => floorIds.Contains(wa.FloorId)).Select(wa => new { wa.Id, wa.FloorId, wa.AreaName }).ToListAsync();
return (buildings, floors, workAreas);
});
// Task to get work summaries, AGGREGATED ON THE DATABASE SERVER
var workSummaryTask = Task.Run(async () =>
{
using var context = _dbContextFactory.CreateDbContext();
// This is the most powerful optimization. It avoids pulling all WorkItem rows.
return await context.WorkItems.AsNoTracking()
.Where(wi => wi.WorkArea != null && wi.WorkArea.Floor != null && wi.WorkArea.Floor.Building != null && wi.WorkArea.Floor.Building.ProjectId == projectId)
.GroupBy(wi => wi.WorkAreaId) // Group by the parent WorkArea
.Select(g => new
{
WorkAreaId = g.Key,
PlannedWork = g.Sum(i => i.PlannedWork),
CompletedWork = g.Sum(i => i.CompletedWork)
})
.ToDictionaryAsync(x => x.WorkAreaId); // Return a ready-to-use dictionary for fast lookups
});
await Task.WhenAll(hierarchyTask, workSummaryTask);
var (buildings, floors, workAreas) = await hierarchyTask;
var workSummariesByWorkAreaId = await workSummaryTask;
// --- Step 4: Build the hierarchy efficiently using Lookups ---
// Using lookups is much faster (O(1)) than repeated .Where() calls (O(n)).
var floorsByBuildingId = floors.ToLookup(f => f.BuildingId);
var workAreasByFloorId = workAreas.ToLookup(wa => wa.FloorId);
var buildingMongoList = new List<BuildingMongoDB>();
foreach (var building in buildings)
{
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 workArea in workAreasByFloorId[floor.Id]) // Fast lookup
{
// Get the pre-calculated summary from the dictionary. O(1) operation.
workSummariesByWorkAreaId.TryGetValue(workArea.Id, out var summary);
var waPlanned = summary?.PlannedWork ?? 0;
var waCompleted = summary?.CompletedWork ?? 0;
workAreaMongoList.Add(new WorkAreaMongoDB
{
Id = workArea.Id.ToString(),
AreaName = workArea.AreaName,
PlannedWork = waPlanned,
CompletedWork = waCompleted
});
floorPlanned += waPlanned;
floorCompleted += waCompleted;
}
floorMongoList.Add(new FloorMongoDB
{
Id = floor.Id.ToString(),
FloorName = floor.FloorName,
PlannedWork = floorPlanned,
CompletedWork = floorCompleted,
WorkAreas = workAreaMongoList
});
buildingPlanned += floorPlanned;
buildingCompleted += floorCompleted;
}
buildingMongoList.Add(new BuildingMongoDB
{
Id = building.Id.ToString(),
BuildingName = building.Name,
Description = building.Description,
PlannedWork = buildingPlanned,
CompletedWork = buildingCompleted,
Floors = floorMongoList
});
}
return buildingMongoList;
}
/// <summary>
/// Retrieves a list of work items for a specific work area, including a summary of tasks assigned for the current day.
/// This method is highly optimized to run database operations in parallel and perform aggregations on the server.
/// </summary>
/// <param name="workAreaId">The ID of the work area.</param>
/// <returns>A list of WorkItemMongoDB objects with calculated daily assignments.</returns>
public async Task<List<WorkItemMongoDB>> GetWorkItemsListFromDB(Guid workAreaId)
{
_logger.LogInfo("Fetching DB work items for WorkAreaId: {WorkAreaId}", workAreaId);
try
{
// --- Step 1: Run independent database queries in PARALLEL ---
// We can fetch the WorkItems and the aggregated TaskAllocations at the same time.
// Task 1: Fetch the WorkItem entities and their related data.
var workItemsTask = _context.WorkItems
.Include(wi => wi.ActivityMaster)
.Include(wi => wi.WorkCategoryMaster)
.Where(wi => wi.WorkAreaId == workAreaId)
.AsNoTracking()
.ToListAsync();
// Task 2: Fetch and AGGREGATE today's task allocations ON THE DATABASE SERVER.
var todaysAssignmentsTask = Task.Run(async () =>
{
// Correctly define "today's" date range to avoid precision issues.
var today = DateTime.UtcNow.Date;
var tomorrow = today.AddDays(1);
using var context = _dbContextFactory.CreateDbContext(); // Use a factory for thread safety
// This is the most powerful optimization:
// 1. It filters by WorkAreaId directly, making it independent of the first query.
// 2. It filters by a correct date range.
// 3. It groups and sums on the DB server, returning only a small summary.
return await context.TaskAllocations
.Where(t => t.WorkItem != null && t.WorkItem.WorkAreaId == workAreaId &&
t.AssignmentDate >= today && t.AssignmentDate < tomorrow)
.GroupBy(t => t.WorkItemId)
.Select(g => new
{
WorkItemId = g.Key,
TodaysAssigned = g.Sum(x => x.PlannedTask)
})
// Return a dictionary for instant O(1) lookups later.
.ToDictionaryAsync(x => x.WorkItemId, x => x.TodaysAssigned);
});
// Await both parallel database operations to complete.
await Task.WhenAll(workItemsTask, todaysAssignmentsTask);
// Retrieve the results from the completed tasks.
var workItemsFromDb = await workItemsTask;
var todaysAssignments = await todaysAssignmentsTask;
// --- Step 2: Map to the ViewModel/MongoDB model efficiently ---
var workItemVMs = workItemsFromDb.Select(wi => new WorkItemMongoDB
{
Id = wi.Id.ToString(),
WorkAreaId = wi.WorkAreaId.ToString(),
ParentTaskId = wi.ParentTaskId.ToString(),
ActivityMaster = wi.ActivityMaster != null ? new ActivityMasterMongoDB
{
Id = wi.ActivityMaster.Id.ToString(),
ActivityName = wi.ActivityMaster.ActivityName,
UnitOfMeasurement = wi.ActivityMaster.UnitOfMeasurement
} : null,
WorkCategoryMaster = wi.WorkCategoryMaster != null ? new WorkCategoryMasterMongoDB
{
Id = wi.WorkCategoryMaster.Id.ToString(),
Name = wi.WorkCategoryMaster.Name,
Description = wi.WorkCategoryMaster.Description
} : null,
PlannedWork = wi.PlannedWork,
CompletedWork = wi.CompletedWork,
Description = wi.Description,
TaskDate = wi.TaskDate,
// Use the fast dictionary lookup instead of the slow in-memory Where/Sum.
TodaysAssigned = todaysAssignments.GetValueOrDefault(wi.Id, 0)
}).ToList();
_logger.LogInfo("Successfully processed {WorkItemCount} work items for WorkAreaId: {WorkAreaId}", workItemVMs.Count, workAreaId);
return workItemVMs;
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while fetching DB work items for WorkAreaId: {WorkAreaId}", workAreaId);
// Return an empty list or re-throw, depending on your application's error handling strategy.
return new List<WorkItemMongoDB>();
}
}
}
}

View File

@ -218,7 +218,7 @@ namespace Marco.Pms.Services.Helpers
_logger.LogInfo("Contact tag master {ConatctTagId} updated successfully by employee {EmployeeId}", contactTagVm.Id, LoggedInEmployee.Id);
return ApiResponse<object>.SuccessResponse(contactTagVm, "Contact Tag master updated successfully", 200);
}
_logger.LogWarning("Contact Tag master {ContactTagId} not found in database", id);
_logger.LogError("Contact Tag master {ContactTagId} not found in database", id);
return ApiResponse<object>.ErrorResponse("Contact Tag master not found", "Contact tag master not found", 404);
}
_logger.LogWarning("Employee with ID {LoggedInEmployeeId} sended empty payload", LoggedInEmployee.Id);
@ -294,7 +294,7 @@ namespace Marco.Pms.Services.Helpers
}
catch (Exception ex)
{
_logger.LogWarning("Error occurred while fetching work status list : {Error}", ex.Message);
_logger.LogError("Error occurred while fetching work status list : {Error}", ex.Message);
return ApiResponse<object>.ErrorResponse("An error occurred", "Unable to fetch work status list", 500);
}
}
@ -343,7 +343,7 @@ namespace Marco.Pms.Services.Helpers
}
catch (Exception ex)
{
_logger.LogWarning("Error occurred while creating work status : {Error}", ex.Message);
_logger.LogError("Error occurred while creating work status : {Error}", ex.Message);
return ApiResponse<object>.ErrorResponse("An error occurred", "Unable to create work status", 500);
}
}
@ -403,7 +403,7 @@ namespace Marco.Pms.Services.Helpers
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while updating work status ID: {Id}", id);
_logger.LogError("Error occurred while updating work status ID: {Id} : {Error}", id, ex.Message);
return ApiResponse<object>.ErrorResponse("An error occurred", "Unable to update the work status at this time", 500);
}
}
@ -458,7 +458,7 @@ namespace Marco.Pms.Services.Helpers
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while deleting WorkStatus Id: {Id}", id);
_logger.LogError("Error occurred while deleting WorkStatus Id: {Id} : {Error}", id, ex.Message);
return ApiResponse<object>.ErrorResponse("An error occurred", "Unable to delete work status", 500);
}
}

View File

@ -0,0 +1,37 @@
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Projects;
using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore;
namespace ModelServices.Helpers
{
public class ProjectHelper
{
private readonly ApplicationDbContext _context;
public ProjectHelper(ApplicationDbContext context)
{
_context = context;
}
public async Task<List<ProjectAllocation>> GetTeamByProject(Guid TenantId, Guid ProjectId, bool IncludeInactive)
{
if (IncludeInactive)
{
var employees = await _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == ProjectId).Include(e => e.Employee).ToListAsync();
return employees;
}
else
{
var employees = await _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == ProjectId && c.IsActive == true).Include(e => e.Employee).ToListAsync();
return employees;
}
}
}
}

View File

@ -0,0 +1,130 @@
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.MongoDBModels;
using Marco.Pms.Model.Projects;
using Marco.Pms.Services.Helpers;
using Microsoft.EntityFrameworkCore;
namespace MarcoBMS.Services.Helpers
{
public class ProjectsHelper
{
private readonly ApplicationDbContext _context;
private readonly RolesHelper _rolesHelper;
private readonly CacheUpdateHelper _cache;
public ProjectsHelper(ApplicationDbContext context, RolesHelper rolesHelper, CacheUpdateHelper cache)
{
_context = context;
_rolesHelper = rolesHelper;
_cache = cache;
}
public async Task<List<Project>> GetAllProjectByTanentID(Guid tanentID)
{
List<Project> alloc = await _context.Projects.Where(c => c.TenantId == tanentID).ToListAsync();
return alloc;
}
public async Task<List<ProjectAllocation>> GetProjectByEmployeeID(Guid employeeID)
{
List<ProjectAllocation> alloc = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeID && c.IsActive == true).Include(c => c.Project).ToListAsync();
return alloc;
}
public async Task<List<ProjectAllocation>> GetTeamByProject(Guid TenantId, Guid ProjectId, bool IncludeInactive)
{
if (IncludeInactive)
{
var employees = await _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == ProjectId).Include(e => e.Employee).ToListAsync();
return employees;
}
else
{
var employees = await _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == ProjectId && c.IsActive == true).Include(e => e.Employee).ToListAsync();
return employees;
}
}
public async Task<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)
{
List<ProjectMongoDB> projectdetails = await _cache.GetProjectDetailsList(projectIds) ?? new List<ProjectMongoDB>();
projects = projectdetails.Select(p => new Project
{
Id = Guid.Parse(p.Id),
Name = p.Name,
ShortName = p.ShortName,
ProjectAddress = p.ProjectAddress,
ProjectStatusId = Guid.Parse(p.ProjectStatus?.Id ?? ""),
ContactPerson = p.ContactPerson,
StartDate = p.StartDate,
EndDate = p.EndDate,
TenantId = tenantId
}).ToList();
if (projects.Count != projectIds.Count)
{
projects = await _context.Projects.Where(p => projectIds.Contains(p.Id)).ToListAsync();
}
}
else
{
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 there are no allocations, return an empty list early
if (allocation == null || !allocation.Any())
{
return new List<Project>();
}
// Use LINQ's Contains for efficient filtering by ProjectId
projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); // Get distinct Guids
// Filter projects based on the retrieved ProjectIds
projects = await projectQuery.Where(c => projectIds.Contains(c.Id)).ToListAsync();
}
projectIds = projects.Select(p => p.Id).ToList();
await _cache.AddProjects(LoggedInEmployee.Id, projectIds);
}
return projects;
}
}
}

View File

@ -1,34 +1,26 @@
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)
{
// await _cache.GetBuildingAndFloorByWorkAreaId();
DateTime reportDate = DateTime.UtcNow.AddDays(-1).Date;
var project = await _cache.GetProjectDetailsWithBuildings(projectId);
var project = await _cache.GetProjectDetails(projectId);
if (project == null)
{
var projectSQL = await _context.Projects
@ -91,7 +83,7 @@ namespace Marco.Pms.Services.Helpers
BuildingName = b.BuildingName,
Description = b.Description
}).ToList();
if (!buildings.Any())
if (buildings == null)
{
buildings = await _context.Buildings
.Where(b => b.ProjectId == projectId)
@ -113,7 +105,7 @@ namespace Marco.Pms.Services.Helpers
BuildingId = f.BuildingId,
FloorName = f.FloorName
})).ToList();
if (!floors.Any())
if (floors == null)
{
var buildingIds = buildings.Select(b => Guid.Parse(b.Id)).ToList();
floors = await _context.Floor
@ -131,7 +123,7 @@ namespace Marco.Pms.Services.Helpers
areas = project.Buildings
.SelectMany(b => b.Floors)
.SelectMany(f => f.WorkAreas).ToList();
if (!areas.Any())
if (areas == null)
{
var floorIds = floors.Select(f => Guid.Parse(f.Id)).ToList();
areas = await _context.WorkAreas
@ -149,7 +141,7 @@ namespace Marco.Pms.Services.Helpers
// fetch Work Items
workItems = await _cache.GetWorkItemsByWorkAreaIds(areaIds);
if (workItems == null || !workItems.Any())
if (workItems == null)
{
workItems = await _context.WorkItems
.Include(w => w.ActivityMaster)
@ -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.LogWarning("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.LogWarning("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(ex, "Email Sending Error: Failed to send project statistics email for project ID {ProjectId}.", projectId);
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(dbEx, "Database Error: Failed to save mail logs for project ID {ProjectId}.", projectId);
// Depending on your requirements, you might still return success here as the email was sent.
// Or return an error indicating the logging failed.
return ApiResponse<object>.ErrorResponse("Email sent, but failed to log activity.", "Email sent, but an error occurred while logging.", 500);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected Error: An unhandled exception occurred while processing project statistics for project ID {ProjectId}.", projectId);
return ApiResponse<object>.ErrorResponse("An unexpected error occurred.", "An unexpected error occurred.", 500);
}
}
}
}

View File

@ -3,93 +3,41 @@
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.EntityFrameworkCore;
namespace MarcoBMS.Services.Helpers
{
public class RolesHelper
{
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
private readonly ApplicationDbContext _context;
private readonly CacheUpdateHelper _cache;
private readonly ILoggingService _logger;
public RolesHelper(ApplicationDbContext context, CacheUpdateHelper cache, ILoggingService logger, IDbContextFactory<ApplicationDbContext> dbContextFactory)
public RolesHelper(ApplicationDbContext context, CacheUpdateHelper cache)
{
_context = context;
_cache = cache;
_logger = logger;
_dbContextFactory = dbContextFactory;
}
/// <summary>
/// Retrieves a unique list of enabled feature permissions for a given employee.
/// This method is optimized to use a single, composed database query.
/// </summary>
/// <param name="EmployeeId">The ID of the employee.</param>
/// <returns>A distinct list of FeaturePermission objects the employee is granted.</returns>
public async Task<List<FeaturePermission>> GetFeaturePermissionByEmployeeId(Guid EmployeeId)
public async Task<List<FeaturePermission>> GetFeaturePermissionByEmployeeID(Guid EmployeeID)
{
_logger.LogInfo("Fetching feature permissions for EmployeeId: {EmployeeId}", EmployeeId);
List<Guid> roleMappings = await _context.EmployeeRoleMappings.Where(c => c.EmployeeId == EmployeeID && c.IsEnabled == true).Select(c => c.RoleId).ToListAsync();
try
{
// --- Step 1: Define the subquery using the main thread's context ---
// This is safe because the query is not executed yet.
var employeeRoleIdsQuery = _context.EmployeeRoleMappings
.Where(erm => erm.EmployeeId == EmployeeId && erm.IsEnabled)
.Select(erm => erm.RoleId);
await _cache.AddApplicationRole(EmployeeID, roleMappings);
// --- Step 2: Asynchronously update the cache using the DbContextFactory ---
_ = Task.Run(async () =>
{
try
{
// Create a NEW, short-lived DbContext instance for this background task.
await using var contextForCache = await _dbContextFactory.CreateDbContextAsync();
// _context.RolePermissionMappings
// Now, re-create and execute the query using this new, isolated context.
var roleIds = await contextForCache.EmployeeRoleMappings
.Where(erm => erm.EmployeeId == EmployeeId && erm.IsEnabled)
.Select(erm => erm.RoleId)
.ToListAsync();
var result = await (from rpm in _context.RolePermissionMappings
join fp in _context.FeaturePermissions.Where(c => c.IsEnabled == true).Include(fp => fp.Feature) // Include Feature
on rpm.FeaturePermissionId equals fp.Id
where roleMappings.Contains(rpm.ApplicationRoleId)
select fp)
.ToListAsync();
if (roleIds.Any())
{
// The cache service might also need its own context, or you can pass the data directly.
// Assuming AddApplicationRole takes the data, not a context.
await _cache.AddApplicationRole(EmployeeId, roleIds);
_logger.LogInfo("Successfully queued cache update for EmployeeId: {EmployeeId}", EmployeeId);
}
}
catch (Exception ex)
{
_logger.LogWarning("Background cache update failed for EmployeeId {EmployeeId} : {Error}", EmployeeId, ex.Message);
}
});
return result;
// --- Step 3: Execute the main query on the main thread using its original context ---
// This is now safe because the background task is using a different DbContext instance.
var permissions = await (
from rpm in _context.RolePermissionMappings
join fp in _context.FeaturePermissions.Include(f => f.Feature)
on rpm.FeaturePermissionId equals fp.Id
where employeeRoleIdsQuery.Contains(rpm.ApplicationRoleId) && fp.IsEnabled == true
select fp)
.Distinct()
.ToListAsync();
_logger.LogInfo("Successfully retrieved {PermissionCount} unique permissions for EmployeeId: {EmployeeId}", permissions.Count, EmployeeId);
return permissions;
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while fetching permissions for EmployeeId {EmployeeId}", EmployeeId);
return new List<FeaturePermission>();
}
// return null;
}
public async Task<List<FeaturePermission>> GetFeaturePermissionByRoleID1(Guid roleId)
public async Task<List<FeaturePermission>> GetFeaturePermissionByRoleID(Guid roleId)
{
List<Guid> roleMappings = await _context.RolePermissionMappings.Where(c => c.ApplicationRoleId == roleId).Select(c => c.ApplicationRoleId).ToListAsync();
@ -106,49 +54,5 @@ namespace MarcoBMS.Services.Helpers
// return null;
}
/// <summary>
/// Retrieves a unique list of enabled feature permissions for a given role.
/// This method is optimized to fetch all data in a single, efficient database query.
/// </summary>
/// <param name="roleId">The ID of the role.</param>
/// <returns>A distinct list of FeaturePermission objects granted to the role.</returns>
public async Task<List<FeaturePermission>> GetFeaturePermissionByRoleID(Guid roleId)
{
_logger.LogInfo("Fetching feature permissions for RoleID: {RoleId}", roleId);
try
{
// This single, efficient query gets all the required data at once.
// It joins the mapping table to the permissions table and filters by the given roleId.
var permissions = await (
// 1. Start with the linking table.
from rpm in _context.RolePermissionMappings
// 2. Join to the FeaturePermissions table on the foreign key.
join fp in _context.FeaturePermissions on rpm.FeaturePermissionId equals fp.Id
// 3. Apply all filters in one 'where' clause for clarity and efficiency.
where
rpm.ApplicationRoleId == roleId // Filter by the specific role
&& fp.IsEnabled == true // And only get enabled permissions
// 4. Select the final FeaturePermission object.
select fp)
.Include(fp => fp.Feature)
.Distinct()
.ToListAsync();
_logger.LogInfo("Successfully retrieved {PermissionCount} unique permissions for RoleID: {RoleId}", permissions.Count, roleId);
return permissions;
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while fetching permissions for RoleId {RoleId}", roleId);
// Return an empty list as a safe default to prevent downstream failures.
return new List<FeaturePermission>();
}
}
}
}

View File

@ -1,68 +0,0 @@
using AutoMapper;
using Marco.Pms.Model.Dtos.Project;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Master;
using Marco.Pms.Model.MongoDBModels;
using Marco.Pms.Model.Projects;
using Marco.Pms.Model.ViewModels.Employee;
using Marco.Pms.Model.ViewModels.Projects;
namespace Marco.Pms.Services.MappingProfiles
{
public class MappingProfile : Profile
{
public MappingProfile()
{
#region ======================================================= Projects =======================================================
// Your mappings
CreateMap<Project, ProjectVM>();
CreateMap<Project, ProjectInfoVM>();
CreateMap<ProjectMongoDB, ProjectInfoVM>();
CreateMap<UpdateProjectDto, Project>();
CreateMap<Project, ProjectListVM>();
CreateMap<Project, ProjectDto>();
CreateMap<ProjectMongoDB, ProjectListVM>();
CreateMap<ProjectMongoDB, ProjectVM>()
.ForMember(
dest => dest.Id,
// Explicitly and safely convert string Id to Guid Id
opt => opt.MapFrom(src => new Guid(src.Id))
);
CreateMap<ProjectMongoDB, Project>()
.ForMember(
dest => dest.Id,
// Explicitly and safely convert string Id to Guid Id
opt => opt.MapFrom(src => new Guid(src.Id))
).ForMember(
dest => dest.ProjectStatusId,
// Explicitly and safely convert string ProjectStatusId to Guid ProjectStatusId
opt => opt.MapFrom(src => src.ProjectStatus == null ? Guid.Empty : new Guid(src.ProjectStatus.Id))
);
CreateMap<StatusMasterMongoDB, StatusMaster>();
CreateMap<ProjectVM, Project>();
CreateMap<CreateProjectDto, Project>();
CreateMap<ProjectAllocationDot, ProjectAllocation>()
.ForMember(
dest => dest.EmployeeId,
// Explicitly and safely convert string ProjectStatusId to Guid ProjectStatusId
opt => opt.MapFrom(src => src.EmpID));
CreateMap<ProjectsAllocationDto, ProjectAllocation>();
CreateMap<ProjectAllocation, ProjectAllocationVM>();
CreateMap<BuildingDto, Building>();
CreateMap<FloorDto, Floor>();
CreateMap<WorkAreaDto, WorkArea>();
CreateMap<WorkItemDto, WorkItem>()
.ForMember(
dest => dest.Description,
opt => opt.MapFrom(src => src.Comment));
#endregion
#region ======================================================= Projects =======================================================
CreateMap<Employee, EmployeeVM>();
#endregion
}
}
}

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;
@ -6,7 +7,6 @@ using Marco.Pms.Model.Utilities;
using Marco.Pms.Services.Helpers;
using Marco.Pms.Services.Hubs;
using Marco.Pms.Services.Service;
using Marco.Pms.Services.Service.ServiceInterfaces;
using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Middleware;
using MarcoBMS.Services.Service;
@ -16,35 +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) =======================
#region Logging
// Add Serilog Configuration
string? mongoConn = builder.Configuration["MongoDB:SerilogDatabaseUrl"];
string timeString = "00:00:30";
TimeSpan.TryParse(timeString, out TimeSpan timeSpan);
// Add Serilog Configuration
builder.Host.UseSerilog((context, config) =>
{
config.ReadFrom.Configuration(context.Configuration)
.WriteTo.MongoDB(
config.ReadFrom.Configuration(context.Configuration) // Taking all configuration from appsetting.json
.WriteTo.MongoDB(
databaseUrl: mongoConn ?? string.Empty,
collectionName: "api-logs",
batchPostingLimit: 100,
period: timeSpan
);
});
#endregion
#region CORS (Cross-Origin Resource Sharing)
});
// 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(',');
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()
@ -52,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;
@ -114,139 +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)
#region Customs Services
builder.Services.AddScoped<RefreshTokenService>();
builder.Services.AddScoped<PermissionServices>();
builder.Services.AddScoped<ISignalRService, SignalRService>();
builder.Services.AddScoped<IProjectServices, ProjectServices>();
#endregion
#region Helpers
builder.Services.AddScoped<GeneralHelper>();
builder.Services.AddScoped<UserHelper>();
builder.Services.AddScoped<RolesHelper>();
builder.Services.AddScoped<EmployeeHelper>();
builder.Services.AddScoped<DirectoryHelper>();
builder.Services.AddScoped<MasterHelper>();
builder.Services.AddScoped<ReportHelper>();
builder.Services.AddScoped<CacheUpdateHelper>();
#endregion
#region Cache Services
builder.Services.AddScoped<ProjectCache>();
builder.Services.AddScoped<EmployeeCache>();
builder.Services.AddScoped<ReportCache>();
#endregion
// 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,11 +1,12 @@
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(Exception? ex, string? message, params object[]? args);
void LogError(string? message, params object[]? args);
}
}

View File

@ -11,18 +11,17 @@ namespace MarcoBMS.Services.Service
_logger = logger;
}
public void LogError(Exception? ex, string? message, params object[]? args)
public void LogError(string? message, params object[]? args)
{
using (LogContext.PushProperty("LogLevel", "Error"))
if (args != null)
{
_logger.LogError(ex, message, args);
_logger.LogError(message, args);
}
else
{
_logger.LogError(ex, message);
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;
}
@ -24,37 +27,30 @@ namespace Marco.Pms.Services.Service
var featurePermissionIds = await _cache.GetPermissions(employeeId);
if (featurePermissionIds == null)
{
List<FeaturePermission> featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeId(employeeId);
List<FeaturePermission> featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(employeeId);
featurePermissionIds = featurePermission.Select(fp => fp.Id).ToList();
}
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(PermissionsMaster.ManageProject, employeeId);
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).ToListAsync();
if (!allocation.Any())
{
return false;
}
projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList();
}
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;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,11 @@
using Marco.Pms.DataAccess.Data;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Authentication;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
#nullable disable
namespace MarcoBMS.Services.Service
@ -94,7 +94,7 @@ namespace MarcoBMS.Services.Service
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while creating new JWT token for user {UserId}", userId);
_logger.LogError("{Error}", ex.Message);
throw;
}
}
@ -132,7 +132,7 @@ namespace MarcoBMS.Services.Service
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating MPIN token for userId: {UserId}, tenantId: {TenantId}", userId, tenantId);
_logger.LogError("Error creating MPIN token for userId: {UserId}, tenantId: {TenantId}, error : {Error}", userId, tenantId, ex.Message);
throw;
}
}
@ -218,7 +218,7 @@ namespace MarcoBMS.Services.Service
catch (Exception ex)
{
// Token is invalid
_logger.LogError(ex, "Token validation failed");
_logger.LogError($"Token validation failed: {ex.Message}");
return null;
}
}

View File

@ -64,7 +64,7 @@ namespace Marco.Pms.Services.Service
}
catch (Exception ex)
{
_logger.LogError(ex, "error occured while uploading file to S3");
_logger.LogError("{error} while uploading file to S3", ex.Message);
}
@ -87,7 +87,7 @@ namespace Marco.Pms.Services.Service
}
catch (Exception ex)
{
_logger.LogError(ex, "error occured while requesting presigned url from Amazon S3", ex.Message);
_logger.LogError("{error} while requesting presigned url from Amazon S3", ex.Message);
return string.Empty;
}
}
@ -107,7 +107,7 @@ namespace Marco.Pms.Services.Service
}
catch (Exception ex)
{
_logger.LogError(ex, "error ocured while deleting from Amazon S3");
_logger.LogError("{error} while deleting from Amazon S3", ex.Message);
return false;
}
}
@ -202,7 +202,7 @@ namespace Marco.Pms.Services.Service
}
else
{
_logger.LogWarning("Warning: Could not find MimeType, Type, or ContentType property in Definition.");
_logger.LogError("Warning: Could not find MimeType, Type, or ContentType property in Definition.");
return "application/octet-stream";
}
}
@ -211,16 +211,16 @@ namespace Marco.Pms.Services.Service
return "application/octet-stream"; // Default if type cannot be determined
}
}
catch (FormatException fEx)
catch (FormatException)
{
// Handle cases where the input string is not valid Base64
_logger.LogError(fEx, "Invalid Base64 string.");
_logger.LogError("Invalid Base64 string.");
return string.Empty;
}
catch (Exception ex)
{
// Handle other potential errors during decoding or inspection
_logger.LogError(ex, "errors during decoding or inspection");
_logger.LogError($"An error occurred: {ex.Message}");
return string.Empty;
}
}

View File

@ -1,35 +0,0 @@
using Marco.Pms.Model.Dtos.Project;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Projects;
namespace Marco.Pms.Services.Service.ServiceInterfaces
{
public interface IProjectServices
{
Task<ApiResponse<object>> GetAllProjectsBasicAsync(Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetAllProjectsAsync(Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetProjectAsync(Guid id, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetProjectDetailsAsync(Guid id, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetProjectDetailsOldAsync(Guid id, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> CreateProjectAsync(CreateProjectDto projectDto, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> UpdateProjectAsync(Guid id, UpdateProjectDto updateProjectDto, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetEmployeeByProjectIdAsync(Guid? projectId, bool includeInactive, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetProjectAllocationAsync(Guid? projectId, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<List<ProjectAllocationVM>>> ManageAllocationAsync(List<ProjectAllocationDot> projectAllocationDots, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetProjectsByEmployeeAsync(Guid employeeId, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<List<ProjectAllocationVM>>> AssigneProjectsToEmployeeAsync(List<ProjectsAllocationDto> projectAllocationDtos, Guid employeeId, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetInfraDetailsAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetWorkItemsAsync(Guid workAreaId, Guid tenantId, Employee loggedInEmployee);
Task<ServiceResponse> ManageProjectInfraAsync(List<InfraDto> infraDtos, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<List<WorkItemVM>>> CreateProjectTaskAsync(List<WorkItemDto> workItemDtos, Guid tenantId, Employee loggedInEmployee);
Task<ServiceResponse> DeleteProjectTaskAsync(Guid id, Guid tenantId, Employee loggedInEmployee);
Task<List<Project>> GetAllProjectByTanentID(Guid tanentId);
Task<List<ProjectAllocation>> GetProjectByEmployeeID(Guid employeeId);
Task<List<ProjectAllocation>> GetTeamByProject(Guid TenantId, Guid ProjectId, bool IncludeInactive);
Task<List<Guid>> GetMyProjectIdsAsync(Guid tenantId, Employee LoggedInEmployee);
}
}

View File

@ -1,7 +0,0 @@
namespace Marco.Pms.Services.Service.ServiceInterfaces
{
public interface ISignalRService
{
Task SendNotificationAsync(object notification);
}
}

View File

@ -1,29 +0,0 @@
using Marco.Pms.Services.Hubs;
using Marco.Pms.Services.Service.ServiceInterfaces;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.SignalR;
namespace Marco.Pms.Services.Service
{
public class SignalRService : ISignalRService
{
private readonly IHubContext<MarcoHub> _signalR;
private readonly ILoggingService _logger;
public SignalRService(IHubContext<MarcoHub> signalR, ILoggingService logger)
{
_signalR = signalR ?? throw new ArgumentNullException(nameof(signalR));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task SendNotificationAsync(object notification)
{
try
{
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception occured during sending notification through signalR");
}
}
}
}