Activity_Hierarchy #94

Open
ashutosh.nehete wants to merge 15 commits from Activity_Hierarchy into main
84 changed files with 13370 additions and 1765 deletions
Showing only changes of commit eb75e2f723 - Show all commits

3
.gitignore vendored
View File

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

View File

@ -0,0 +1,204 @@
using Marco.Pms.Model.MongoDBModels;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using MongoDB.Driver;
namespace Marco.Pms.CacheHelper
{
public class EmployeeCache
{
private readonly IMongoCollection<EmployeePermissionMongoDB> _collection;
public EmployeeCache(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
_collection = mongoDB.GetCollection<EmployeePermissionMongoDB>("EmployeeProfile");
}
public async Task<bool> AddApplicationRoleToCache(Guid employeeId, List<string> newRoleIds, List<string> newPermissionIds)
{
// 2. Perform database queries concurrently for better performance.
var employeeIdString = employeeId.ToString();
// 5. Build a single, efficient update operation.
var filter = Builders<EmployeePermissionMongoDB>.Filter.Eq(e => e.Id, employeeIdString);
var update = Builders<EmployeePermissionMongoDB>.Update
.AddToSetEach(e => e.ApplicationRoleIds, newRoleIds)
.AddToSetEach(e => e.PermissionIds, newPermissionIds);
var options = new UpdateOptions { IsUpsert = true };
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);
}
public async Task<bool> AddProjectsToCache(Guid employeeId, List<Guid> projectIds)
{
var newprojectIds = projectIds.Select(p => p.ToString()).ToList();
var filter = Builders<EmployeePermissionMongoDB>.Filter.Eq(e => e.Id, employeeId.ToString());
var update = Builders<EmployeePermissionMongoDB>.Update
.AddToSetEach(e => e.ProjectIds, newprojectIds);
var result = await _collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true });
if (result.MatchedCount == 0)
{
return false;
}
await InitializeCollectionAsync();
return true;
}
public async Task<List<Guid>> GetProjectsFromCache(Guid employeeId)
{
var filter = Builders<EmployeePermissionMongoDB>.Filter.Eq(e => e.Id, employeeId.ToString());
var result = await _collection
.Find(filter)
.FirstOrDefaultAsync();
var projectIds = new List<Guid>();
if (result != null)
{
projectIds = result.ProjectIds.Select(Guid.Parse).ToList();
}
return projectIds;
}
public async Task<List<Guid>> GetPermissionsFromCache(Guid employeeId)
{
var filter = Builders<EmployeePermissionMongoDB>.Filter.Eq(e => e.Id, employeeId.ToString());
var result = await _collection
.Find(filter)
.FirstOrDefaultAsync();
var permissionIds = new List<Guid>();
if (result != null)
{
permissionIds = result.PermissionIds.Select(Guid.Parse).ToList();
}
return permissionIds;
}
public async Task<bool> ClearAllProjectIdsFromCache(Guid employeeId)
{
var filter = Builders<EmployeePermissionMongoDB>.Filter
.Eq(e => e.Id, employeeId.ToString());
var update = Builders<EmployeePermissionMongoDB>.Update
.Set(e => e.ProjectIds, new List<string>());
var result = await _collection.UpdateOneAsync(filter, update);
if (result.ModifiedCount == 0)
return false;
return true;
}
public async Task<bool> ClearAllProjectIdsByRoleIdFromCache(Guid roleId)
{
var filter = Builders<EmployeePermissionMongoDB>.Filter.AnyEq(e => e.ApplicationRoleIds, roleId.ToString());
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> 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;
}
public async Task<bool> RemoveRoleIdFromCache(Guid employeeId, Guid roleId)
{
var filter = Builders<EmployeePermissionMongoDB>.Filter
.Eq(e => e.Id, employeeId.ToString());
var update = Builders<EmployeePermissionMongoDB>.Update
.Pull(e => e.ApplicationRoleIds, roleId.ToString());
var result = await _collection.UpdateOneAsync(filter, update);
if (result.MatchedCount == 0)
return false;
if (result.ModifiedCount == 0)
return false;
return true;
}
public async Task<bool> ClearAllPermissionIdsByEmployeeIDFromCache(Guid employeeId)
{
var filter = Builders<EmployeePermissionMongoDB>.Filter
.Eq(e => e.Id, employeeId.ToString());
var update = Builders<EmployeePermissionMongoDB>.Update
.Set(e => e.PermissionIds, new List<string>());
var result = await _collection.UpdateOneAsync(filter, update);
if (result.MatchedCount == 0)
return false;
return true;
}
public async Task<bool> ClearAllPermissionIdsByRoleIdFromCache(Guid roleId)
{
var filter = Builders<EmployeePermissionMongoDB>.Filter.AnyEq(e => e.ApplicationRoleIds, roleId.ToString());
var update = Builders<EmployeePermissionMongoDB>.Update
.Set(e => e.PermissionIds, new List<string>());
var result = await _collection.UpdateOneAsync(filter, update);
if (result.MatchedCount == 0)
return false;
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

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MongoDB.Driver" Version="3.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Marco.Pms.DataAccess\Marco.Pms.DataAccess.csproj" />
<ProjectReference Include="..\Marco.Pms.Model\Marco.Pms.Model.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,506 @@
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Master;
using Marco.Pms.Model.MongoDBModels;
using Marco.Pms.Model.Projects;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using MongoDB.Bson;
using MongoDB.Driver;
namespace Marco.Pms.CacheHelper
{
public class ProjectCache
{
private readonly IMongoCollection<ProjectMongoDB> _projectCollection;
private readonly IMongoCollection<WorkItemMongoDB> _taskCollection;
public ProjectCache(ApplicationDbContext context, 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
_projectCollection = mongoDB.GetCollection<ProjectMongoDB>("ProjectDetails");
_taskCollection = mongoDB.GetCollection<WorkItemMongoDB>("WorkItemDetails");
}
#region=================================================================== Project Cache Helper ===================================================================
public async Task AddProjectDetailsToCache(ProjectMongoDB projectDetails)
{
await _projectCollection.InsertOneAsync(projectDetails);
var indexKeys = Builders<ProjectMongoDB>.IndexKeys.Ascending(x => x.ExpireAt);
var indexOptions = new CreateIndexOptions
{
ExpireAfter = TimeSpan.Zero // required for fixed expiration time
};
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())
{
return;
}
// 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);
// 2. Create the index. This is an idempotent operation if the index already exists.
// Use CreateOneAsync since we are only creating a single index.
await _projectCollection.Indexes.CreateOneAsync(indexModel);
}
public async Task<bool> UpdateProjectDetailsOnlyToCache(Project project, StatusMaster projectStatus)
{
// Build the update definition
var updates = Builders<ProjectMongoDB>.Update.Combine(
Builders<ProjectMongoDB>.Update.Set(r => r.Name, project.Name),
Builders<ProjectMongoDB>.Update.Set(r => r.ProjectAddress, project.ProjectAddress),
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
}),
Builders<ProjectMongoDB>.Update.Set(r => r.StartDate, project.StartDate),
Builders<ProjectMongoDB>.Update.Set(r => r.EndDate, project.EndDate),
Builders<ProjectMongoDB>.Update.Set(r => r.ContactPerson, project.ContactPerson)
);
// Perform the update
var result = await _projectCollection.UpdateOneAsync(
filter: r => r.Id == project.Id.ToString(),
update: updates
);
if (result.MatchedCount == 0)
{
return false;
}
return true;
}
public async Task<ProjectMongoDB?> GetProjectDetailsFromCache(Guid projectId)
{
// Build filter and projection to exclude large 'Buildings' list
var filter = Builders<ProjectMongoDB>.Filter.Eq(p => p.Id, projectId.ToString());
var projection = Builders<ProjectMongoDB>.Projection.Exclude(p => p.Buildings);
// Perform query
var project = await _projectCollection
.Find(filter)
.Project<ProjectMongoDB>(projection)
.FirstOrDefaultAsync();
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)
{
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
.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();
// Add Building
if (building != null)
{
var buildingMongo = new BuildingMongoDB
{
Id = building.Id.ToString(),
BuildingName = building.Name,
Description = building.Description,
PlannedWork = 0,
CompletedWork = 0,
Floors = new List<FloorMongoDB>()
};
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);
if (result.MatchedCount == 0)
{
return;
}
return;
}
// Add Floor
if (floor != null)
{
var floorMongo = new FloorMongoDB
{
Id = floor.Id.ToString(),
FloorName = floor.FloorName,
PlannedWork = 0,
CompletedWork = 0,
WorkAreas = new List<WorkAreaMongoDB>()
};
var filter = Builders<ProjectMongoDB>.Filter.And(
Builders<ProjectMongoDB>.Filter.Eq(p => p.Id, stringProjectId),
Builders<ProjectMongoDB>.Filter.Eq("Buildings._id", floor.BuildingId.ToString())
);
var update = Builders<ProjectMongoDB>.Update.Push("Buildings.$.Floors", floorMongo);
var result = await _projectCollection.UpdateOneAsync(filter, update);
if (result.MatchedCount == 0)
{
return;
}
return;
}
// Add WorkArea
if (workArea != null && buildingId != null)
{
var workAreaMongo = new WorkAreaMongoDB
{
Id = workArea.Id.ToString(),
AreaName = workArea.AreaName,
PlannedWork = 0,
CompletedWork = 0
};
var filter = Builders<ProjectMongoDB>.Filter.Eq(p => p.Id, stringProjectId);
var arrayFilters = new List<ArrayFilterDefinition>
{
new JsonArrayFilterDefinition<BsonDocument>("{ 'b._id': '" + buildingId + "' }"),
new JsonArrayFilterDefinition<BsonDocument>("{ 'f._id': '" + workArea.FloorId + "' }")
};
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);
if (result.MatchedCount == 0)
{
return;
}
return;
}
}
public async Task<bool> UpdateBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId)
{
var stringProjectId = projectId.ToString();
// Update Building
if (building != null)
{
var filter = Builders<ProjectMongoDB>.Filter.And(
Builders<ProjectMongoDB>.Filter.Eq(p => p.Id, stringProjectId),
Builders<ProjectMongoDB>.Filter.Eq("Buildings._id", building.Id.ToString())
);
var update = Builders<ProjectMongoDB>.Update.Combine(
Builders<ProjectMongoDB>.Update.Set("Buildings.$.BuildingName", building.Name),
Builders<ProjectMongoDB>.Update.Set("Buildings.$.Description", building.Description)
);
var result = await _projectCollection.UpdateOneAsync(filter, update);
if (result.MatchedCount == 0)
{
return false;
}
return true;
}
// Update Floor
if (floor != null)
{
var arrayFilters = new List<ArrayFilterDefinition>
{
new JsonArrayFilterDefinition<BsonDocument>("{ 'b._id': '" + floor.BuildingId + "' }"),
new JsonArrayFilterDefinition<BsonDocument>("{ 'f._id': '" + floor.Id + "' }")
};
var update = Builders<ProjectMongoDB>.Update.Set("Buildings.$[b].Floors.$[f].FloorName", floor.FloorName);
var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters };
var filter = Builders<ProjectMongoDB>.Filter.Eq(p => p.Id, stringProjectId);
var result = await _projectCollection.UpdateOneAsync(filter, update, updateOptions);
if (result.MatchedCount == 0)
{
return false;
}
return true;
}
// Update WorkArea
if (workArea != null && buildingId != null)
{
var arrayFilters = new List<ArrayFilterDefinition>
{
new JsonArrayFilterDefinition<BsonDocument>("{ 'b._id': '" + buildingId + "' }"),
new JsonArrayFilterDefinition<BsonDocument>("{ 'f._id': '" + workArea.FloorId + "' }"),
new JsonArrayFilterDefinition<BsonDocument>("{ 'a._id': '" + workArea.Id + "' }")
};
var update = Builders<ProjectMongoDB>.Update.Set("Buildings.$[b].Floors.$[f].WorkAreas.$[a].AreaName", workArea.AreaName);
var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters };
var filter = Builders<ProjectMongoDB>.Filter.Eq(p => p.Id, stringProjectId);
var result = await _projectCollection.UpdateOneAsync(filter, update, updateOptions);
if (result.MatchedCount == 0)
{
return false;
}
return true;
}
return false;
}
public async Task<List<BuildingMongoDB>?> GetBuildingInfraFromCache(Guid projectId)
{
// Filter by project ID
var filter = Builders<ProjectMongoDB>.Filter.Eq(p => p.Id, projectId.ToString());
// Project only the "Buildings" field from the document
var buildings = await _projectCollection
.Find(filter)
.Project(p => p.Buildings)
.FirstOrDefaultAsync();
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();
string? selectedBuildingId = null;
string? selectedFloorId = null;
string? selectedWorkAreaId = null;
foreach (var building in project.Buildings)
{
foreach (var floor in building.Floors)
{
foreach (var area in floor.WorkAreas)
{
if (area.Id == workAreaId.ToString())
{
selectedWorkAreaId = area.Id;
selectedFloorId = floor.Id;
selectedBuildingId = building.Id;
}
}
}
}
var arrayFilters = new List<ArrayFilterDefinition>
{
new JsonArrayFilterDefinition<BsonDocument>("{ 'b._id': '" + selectedBuildingId + "' }"),
new JsonArrayFilterDefinition<BsonDocument>("{ 'f._id': '" + selectedFloorId + "' }"),
new JsonArrayFilterDefinition<BsonDocument>("{ 'a._id': '" + selectedWorkAreaId + "' }")
};
var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters };
var update = Builders<ProjectMongoDB>.Update
.Inc("Buildings.$[b].Floors.$[f].WorkAreas.$[a].PlannedWork", plannedWork)
.Inc("Buildings.$[b].Floors.$[f].WorkAreas.$[a].CompletedWork", completedWork)
.Inc("Buildings.$[b].Floors.$[f].PlannedWork", plannedWork)
.Inc("Buildings.$[b].Floors.$[f].CompletedWork", completedWork)
.Inc("Buildings.$[b].PlannedWork", plannedWork)
.Inc("Buildings.$[b].CompletedWork", completedWork)
.Inc("PlannedWork", plannedWork)
.Inc("CompletedWork", completedWork);
var result = await _projectCollection.UpdateOneAsync(filter, update, updateOptions);
}
public async Task<WorkAreaInfoMongoDB?> GetBuildingAndFloorByWorkAreaIdFromCache(Guid workAreaId)
{
var pipeline = new[]
{
new BsonDocument("$unwind", "$Buildings"),
new BsonDocument("$unwind", "$Buildings.Floors"),
new BsonDocument("$unwind", "$Buildings.Floors.WorkAreas"),
new BsonDocument("$match", new BsonDocument("Buildings.Floors.WorkAreas._id", workAreaId.ToString())),
new BsonDocument("$project", new BsonDocument
{
{ "_id", 0 },
{ "ProjectId", "$_id" },
{ "ProjectName", "$Name" },
{ "PlannedWork", "$PlannedWork" },
{ "CompletedWork", "$CompletedWork" },
{
"Building", new BsonDocument
{
{ "_id", "$Buildings._id" },
{ "BuildingName", "$Buildings.BuildingName" },
{ "Description", "$Buildings.Description" },
{ "PlannedWork", "$Buildings.PlannedWork" },
{ "CompletedWork", "$Buildings.CompletedWork" }
}
},
{
"Floor", new BsonDocument
{
{ "_id", "$Buildings.Floors._id" },
{ "FloorName", "$Buildings.Floors.FloorName" },
{ "PlannedWork", "$Buildings.Floors.PlannedWork" },
{ "CompletedWork", "$Buildings.Floors.CompletedWork" }
}
},
{ "WorkArea", "$Buildings.Floors.WorkAreas" }
})
};
var result = await _projectCollection.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();
var filter = Builders<WorkItemMongoDB>.Filter.In(w => w.WorkAreaId, stringWorkAreaIds);
var workItems = await _taskCollection // replace with your actual collection name
.Find(filter)
.ToListAsync();
return workItems;
}
public async Task ManageWorkItemDetailsToCache(List<WorkItemMongoDB> workItems)
{
foreach (WorkItemMongoDB workItem in workItems)
{
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.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)
);
var options = new UpdateOptions { IsUpsert = true };
var result = await _taskCollection.UpdateOneAsync(filter, updates, options);
if (result.UpsertedId != null)
{
var indexKeys = Builders<WorkItemMongoDB>.IndexKeys.Ascending(x => x.ExpireAt);
var indexOptions = new CreateIndexOptions
{
ExpireAfter = TimeSpan.Zero // required for fixed expiration time
};
var indexModel = new CreateIndexModel<WorkItemMongoDB>(indexKeys, indexOptions);
await _taskCollection.Indexes.CreateOneAsync(indexModel);
}
}
}
public async Task<List<WorkItemMongoDB>> GetWorkItemDetailsByWorkAreaFromCache(Guid workAreaId)
{
var filter = Builders<WorkItemMongoDB>.Filter.Eq(p => p.WorkAreaId, workAreaId.ToString());
var options = new UpdateOptions { IsUpsert = true };
var workItems = await _taskCollection
.Find(filter)
.ToListAsync();
return workItems;
}
public async Task<WorkItemMongoDB> GetWorkItemDetailsByIdFromCache(Guid id)
{
var filter = Builders<WorkItemMongoDB>.Filter.Eq(p => p.Id, id.ToString());
var options = new UpdateOptions { IsUpsert = true };
var workItem = await _taskCollection
.Find(filter)
.FirstOrDefaultAsync();
return workItem;
}
public async Task<bool> UpdatePlannedAndCompleteWorksInWorkItemToCache(Guid id, double plannedWork, double completedWork, double todaysAssigned)
{
var filter = Builders<WorkItemMongoDB>.Filter.Eq(p => p.Id, id.ToString());
var updates = Builders<WorkItemMongoDB>.Update
.Inc("PlannedWork", plannedWork)
.Inc("CompletedWork", completedWork)
.Inc("TodaysAssigned", todaysAssigned);
var result = await _taskCollection.UpdateOneAsync(filter, updates);
if (result.ModifiedCount > 0)
{
return true;
}
return false;
}
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

@ -0,0 +1,42 @@
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

@ -0,0 +1,50 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Added_UploadedBy_ForeginKey_In_Decuments_Table : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "UploadedById",
table: "Documents",
type: "char(36)",
nullable: true,
collation: "ascii_general_ci");
migrationBuilder.CreateIndex(
name: "IX_Documents_UploadedById",
table: "Documents",
column: "UploadedById");
migrationBuilder.AddForeignKey(
name: "FK_Documents_Employees_UploadedById",
table: "Documents",
column: "UploadedById",
principalTable: "Employees",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Documents_Employees_UploadedById",
table: "Documents");
migrationBuilder.DropIndex(
name: "IX_Documents_UploadedById",
table: "Documents");
migrationBuilder.DropColumn(
name: "UploadedById",
table: "Documents");
}
}
}

View File

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Added_Designation_Paraneter_In_Contacts_Table : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Designation",
table: "Contacts",
type: "longtext",
nullable: false)
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Designation",
table: "Contacts");
}
}
}

View File

@ -420,6 +420,10 @@ namespace Marco.Pms.DataAccess.Migrations
.IsRequired() .IsRequired()
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<string>("Designation")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("IsActive") b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)"); .HasColumnType("tinyint(1)");
@ -752,10 +756,15 @@ namespace Marco.Pms.DataAccess.Migrations
b.Property<DateTime>("UploadedAt") b.Property<DateTime>("UploadedAt")
.HasColumnType("datetime(6)"); .HasColumnType("datetime(6)");
b.Property<Guid?>("UploadedById")
.HasColumnType("char(36)");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("UploadedById");
b.ToTable("Documents"); b.ToTable("Documents");
}); });
@ -3316,7 +3325,13 @@ namespace Marco.Pms.DataAccess.Migrations
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("Marco.Pms.Model.Employees.Employee", "UploadedBy")
.WithMany()
.HasForeignKey("UploadedById");
b.Navigation("Tenant"); b.Navigation("Tenant");
b.Navigation("UploadedBy");
}); });
modelBuilder.Entity("Marco.Pms.Model.Employees.Employee", b => modelBuilder.Entity("Marco.Pms.Model.Employees.Employee", b =>

View File

@ -12,6 +12,7 @@ namespace Marco.Pms.Model.Directory
//public Guid? ProjectId { get; set; } //public Guid? ProjectId { get; set; }
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
public string Designation { get; set; } = string.Empty;
public string Organization { get; set; } = string.Empty; public string Organization { get; set; } = string.Empty;
public string? Address { get; set; } public string? Address { get; set; }
public bool IsActive { get; set; } = true; public bool IsActive { get; set; } = true;

View File

@ -1,4 +1,7 @@
using Marco.Pms.Model.Utilities; using System.ComponentModel.DataAnnotations.Schema;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
namespace Marco.Pms.Model.DocumentManager namespace Marco.Pms.Model.DocumentManager
{ {
@ -16,10 +19,15 @@ namespace Marco.Pms.Model.DocumentManager
/// </summary> /// </summary>
public string? ThumbS3Key { get; set; } public string? ThumbS3Key { get; set; }
public string? Base64Data { get; set; } public string? Base64Data { get; set; } = null;
public long FileSize { get; set; } public long FileSize { get; set; }
public string ContentType { get; set; } = string.Empty; public string ContentType { get; set; } = string.Empty;
public Guid? UploadedById { get; set; }
[ValidateNever]
[ForeignKey("UploadedById")]
public Employee? UploadedBy { get; set; }
public DateTime UploadedAt { get; set; } public DateTime UploadedAt { get; set; }
} }
} }

View File

@ -9,6 +9,7 @@
public List<Guid>? BucketIds { get; set; } public List<Guid>? BucketIds { get; set; }
public Guid? ContactCategoryId { get; set; } public Guid? ContactCategoryId { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
public string? Designation { get; set; }
public string? Organization { get; set; } public string? Organization { get; set; }
public string? Address { get; set; } public string? Address { get; set; }
public List<ContactTagDto>? Tags { get; set; } public List<ContactTagDto>? Tags { get; set; }

View File

@ -10,6 +10,7 @@
public List<Guid>? BucketIds { get; set; } public List<Guid>? BucketIds { get; set; }
public Guid? ContactCategoryId { get; set; } public Guid? ContactCategoryId { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
public string? Designation { get; set; }
public string? Organization { get; set; } public string? Organization { get; set; }
public string? Address { get; set; } public string? Address { get; set; }
public List<ContactTagDto>? Tags { get; set; } public List<ContactTagDto>? Tags { get; set; }

View File

@ -0,0 +1,10 @@
using Marco.Pms.Model.DocumentManager;
namespace Marco.Pms.Model.Dtos.DocumentManager
{
public class DocumentBatchDto
{
public Guid? BatchId { get; set; }
public List<Document>? Documents { get; set; }
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,28 @@
namespace Marco.Pms.Model.Entitlements
{
public static class PermissionsMaster
{
public static readonly Guid DirectoryAdmin = Guid.Parse("4286a13b-bb40-4879-8c6d-18e9e393beda");
public static readonly Guid DirectoryManager = Guid.Parse("62668630-13ce-4f52-a0f0-db38af2230c5");
public static readonly Guid DirectoryUser = Guid.Parse("0f919170-92d4-4337-abd3-49b66fc871bb");
public static readonly Guid ViewProject = Guid.Parse("6ea44136-987e-44ba-9e5d-1cf8f5837ebc");
public static readonly Guid ManageProject = Guid.Parse("172fc9b6-755b-4f62-ab26-55c34a330614");
public static readonly Guid ManageTeam = Guid.Parse("b94802ce-0689-4643-9e1d-11c86950c35b");
public static readonly Guid ViewProjectInfra = Guid.Parse("8d7cc6e3-9147-41f7-aaa7-fa507e450bd4");
public static readonly Guid ManageProjectInfra = Guid.Parse("cf2825ad-453b-46aa-91d9-27c124d63373");
public static readonly Guid ViewTask = Guid.Parse("9fcc5f87-25e3-4846-90ac-67a71ab92e3c");
public static readonly Guid AddAndEditTask = Guid.Parse("08752f33-3b29-4816-b76b-ea8a968ed3c5");
public static readonly Guid AssignAndReportProgress = Guid.Parse("6a32379b-8b3f-49a6-8c48-4b7ac1b55dc2");
public static readonly Guid ApproveTask = Guid.Parse("db4e40c5-2ba9-4b6d-b8a6-a16a250ff99c");
public static readonly Guid ViewAllEmployees = Guid.Parse("60611762-7f8a-4fb5-b53f-b1139918796b");
public static readonly Guid ViewTeamMembers = Guid.Parse("b82d2b7e-0d52-45f3-997b-c008ea460e7f");
public static readonly Guid AddAndEditEmployee = Guid.Parse("a97d366a-c2bb-448d-be93-402bd2324566");
public static readonly Guid AssignRoles = Guid.Parse("fbd213e0-0250-46f1-9f5f-4b2a1e6e76a3");
public static readonly Guid TeamAttendance = Guid.Parse("915e6bff-65f6-4e3f-aea8-3fd217d3ea9e");
public static readonly Guid RegularizeAttendance = Guid.Parse("57802c4a-00aa-4a1f-a048-fd2f70dd44b6");
public static readonly Guid SelfAttendance = Guid.Parse("ccb0589f-712b-43de-92ed-5b6088e7dc4e");
public static readonly Guid ViewMasters = Guid.Parse("5ffbafe0-7ab0-48b1-bb50-c1bf76b65f9d");
public static readonly Guid ManageMasters = Guid.Parse("588a8824-f924-4955-82d8-fc51956cf323");
}
}

View File

@ -16,6 +16,7 @@ namespace Marco.Pms.Model.Mapper
Name = createContactDto.Name ?? string.Empty, Name = createContactDto.Name ?? string.Empty,
ContactCategoryId = createContactDto.ContactCategoryId, ContactCategoryId = createContactDto.ContactCategoryId,
Description = createContactDto.Description ?? string.Empty, Description = createContactDto.Description ?? string.Empty,
Designation = createContactDto.Designation ?? string.Empty,
Organization = createContactDto?.Organization ?? string.Empty, Organization = createContactDto?.Organization ?? string.Empty,
Address = createContactDto != null ? createContactDto.Address : string.Empty, Address = createContactDto != null ? createContactDto.Address : string.Empty,
CreatedById = employeeId, CreatedById = employeeId,
@ -34,6 +35,7 @@ namespace Marco.Pms.Model.Mapper
CreatedAt = contact.CreatedAt, CreatedAt = contact.CreatedAt,
CreatedById = contact.CreatedById, CreatedById = contact.CreatedById,
Description = updateContactDto.Description ?? string.Empty, Description = updateContactDto.Description ?? string.Empty,
Designation = updateContactDto.Designation ?? string.Empty,
Organization = updateContactDto?.Organization ?? string.Empty, Organization = updateContactDto?.Organization ?? string.Empty,
Address = updateContactDto != null ? updateContactDto.Address : string.Empty, Address = updateContactDto != null ? updateContactDto.Address : string.Empty,
TenantId = tenantId TenantId = tenantId
@ -47,6 +49,7 @@ namespace Marco.Pms.Model.Mapper
Name = contact.Name, Name = contact.Name,
ContactCategory = contact.ContactCategory != null ? contact.ContactCategory.ToContactCategoryVMFromContactCategoryMaster() : null, ContactCategory = contact.ContactCategory != null ? contact.ContactCategory.ToContactCategoryVMFromContactCategoryMaster() : null,
Description = contact.Description ?? string.Empty, Description = contact.Description ?? string.Empty,
Designation = contact.Designation ?? string.Empty,
Organization = contact.Organization ?? string.Empty, Organization = contact.Organization ?? string.Empty,
Address = contact.Address ?? string.Empty Address = contact.Address ?? string.Empty
}; };
@ -59,6 +62,7 @@ namespace Marco.Pms.Model.Mapper
Name = contact.Name, Name = contact.Name,
ContactCategory = contact.ContactCategory != null ? contact.ContactCategory.ToContactCategoryVMFromContactCategoryMaster() : null, ContactCategory = contact.ContactCategory != null ? contact.ContactCategory.ToContactCategoryVMFromContactCategoryMaster() : null,
Description = contact.Description ?? string.Empty, Description = contact.Description ?? string.Empty,
Designation = contact.Designation ?? string.Empty,
Organization = contact.Organization ?? string.Empty, Organization = contact.Organization ?? string.Empty,
Address = contact.Address ?? string.Empty, Address = contact.Address ?? string.Empty,
CreatedAt = contact.CreatedAt, CreatedAt = contact.CreatedAt,

View File

@ -90,29 +90,35 @@ namespace Marco.Pms.Model.Mapper
}; };
} }
public static Document ToDocumentFromForumAttachmentDto(this ForumAttachmentDto AttachmentDto, string objectKey, string thumbS3Key, DateTime uploadedAt, Guid tenantId) public static Document ToDocumentFromForumAttachmentDto(this ForumAttachmentDto AttachmentDto, string objectKey, string thumbS3Key, DateTime uploadedAt,
Guid tenantId, Guid batchId, Guid loggedInEmployeeId)
{ {
return new Document return new Document
{ {
BatchId = batchId,
UploadedById = loggedInEmployeeId,
FileName = AttachmentDto.FileName, FileName = AttachmentDto.FileName,
ContentType = AttachmentDto.ContentType, ContentType = AttachmentDto.ContentType,
S3Key = objectKey, S3Key = objectKey,
ThumbS3Key = thumbS3Key, ThumbS3Key = thumbS3Key,
Base64Data = AttachmentDto.Base64Data, //Base64Data = AttachmentDto.Base64Data,
FileSize = AttachmentDto.FileSize, FileSize = AttachmentDto.FileSize,
UploadedAt = uploadedAt, UploadedAt = uploadedAt,
TenantId = tenantId TenantId = tenantId
}; };
} }
public static Document ToDocumentFromUpdateAttachmentDto(this UpdateAttachmentDto AttachmentDto, string objectKey, string thumbS3Key, DateTime uploadedAt, Guid tenantId) public static Document ToDocumentFromUpdateAttachmentDto(this UpdateAttachmentDto AttachmentDto, string objectKey, string thumbS3Key, DateTime uploadedAt,
Guid tenantId, Guid batchId, Guid loggedInEmployeeId)
{ {
return new Document return new Document
{ {
BatchId = batchId,
UploadedById = loggedInEmployeeId,
FileName = AttachmentDto.FileName, FileName = AttachmentDto.FileName,
ContentType = AttachmentDto.ContentType, ContentType = AttachmentDto.ContentType,
S3Key = objectKey, S3Key = objectKey,
ThumbS3Key = thumbS3Key, ThumbS3Key = thumbS3Key,
Base64Data = AttachmentDto.Base64Data, //Base64Data = AttachmentDto.Base64Data,
FileSize = AttachmentDto.FileSize, FileSize = AttachmentDto.FileSize,
UploadedAt = uploadedAt, UploadedAt = uploadedAt,
TenantId = tenantId TenantId = tenantId

View File

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

View File

@ -10,6 +10,7 @@
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.20" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.20" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" Version="2.2.0" />
<PackageReference Include="MongoDB.Bson" Version="3.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -0,0 +1,9 @@
namespace Marco.Pms.Model.MongoDBModels
{
public class ActivityMasterMongoDB
{
public string Id { get; set; } = string.Empty;
public string? ActivityName { get; set; }
public string? UnitOfMeasurement { get; set; }
}
}

View File

@ -0,0 +1,22 @@
namespace Marco.Pms.Model.MongoDBModels
{
public class BuildingMongoDB
{
public string Id { get; set; } = string.Empty;
public string? BuildingName { get; set; }
public string? Description { get; set; }
public double PlannedWork { get; set; }
public double CompletedWork { get; set; }
public string ProjectId { get; set; } = string.Empty;
public List<FloorMongoDB> Floors { get; set; } = new List<FloorMongoDB>();
}
public class BuildingMongoDBVM
{
public string Id { get; set; } = string.Empty;
public string? BuildingName { get; set; }
public string? Description { get; set; }
public double PlannedWork { get; set; }
public double CompletedWork { get; set; }
public string ProjectId { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,14 @@
using MongoDB.Bson.Serialization.Attributes;
namespace Marco.Pms.Model.MongoDBModels
{
[BsonIgnoreExtraElements]
public class EmployeePermissionMongoDB
{
public string Id { get; set; } = string.Empty; // Employee ID
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

@ -0,0 +1,21 @@
namespace Marco.Pms.Model.MongoDBModels
{
public class FloorMongoDB
{
public string Id { get; set; } = string.Empty;
public string BuildingId { get; set; } = string.Empty;
public string? FloorName { get; set; }
public double PlannedWork { get; set; }
public double CompletedWork { get; set; }
public List<WorkAreaMongoDB> WorkAreas { get; set; } = new List<WorkAreaMongoDB>();
}
public class FloorMongoDBVM
{
public string Id { get; set; } = string.Empty;
public string BuildingId { get; set; } = string.Empty;
public string? FloorName { get; set; }
public double PlannedWork { get; set; }
public double CompletedWork { get; set; }
}
}

View File

@ -0,0 +1,19 @@
namespace Marco.Pms.Model.MongoDBModels
{
public class ProjectMongoDB
{
public string Id { get; set; } = string.Empty;
public string? Name { get; set; }
public string? ShortName { get; set; }
public string? ProjectAddress { get; set; }
public string? ContactPerson { get; set; }
public List<BuildingMongoDB> Buildings { get; set; } = new List<BuildingMongoDB>();
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public StatusMasterMongoDB? ProjectStatus { get; set; }
public int TeamSize { get; set; }
public double CompletedWork { get; set; }
public double PlannedWork { get; set; }
public DateTime ExpireAt { get; set; } = DateTime.UtcNow.Date.AddDays(1);
}
}

View File

@ -0,0 +1,16 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace Marco.Pms.Model.MongoDBModels
{
public class ProjectReportEmailMongoDB
{
[BsonId] // Tells MongoDB this is the primary key (_id)
[BsonRepresentation(BsonType.ObjectId)] // Optional: if your _id is ObjectId
public string Id { get; set; } = string.Empty;
public string? Body { get; set; }
public string? Subject { get; set; }
public List<string>? Receivers { get; set; }
public bool IsSent { get; set; } = false;
}
}

View File

@ -0,0 +1,8 @@
namespace Marco.Pms.Model.MongoDBModels
{
public class StatusMasterMongoDB
{
public string Id { get; set; } = string.Empty;
public string? Status { get; set; }
}
}

View File

@ -0,0 +1,13 @@
namespace Marco.Pms.Model.MongoDBModels
{
public class WorkAreaInfoMongoDB
{
public string ProjectId { get; set; } = string.Empty;
public string? ProjectName { get; set; }
public BuildingMongoDBVM? Building { get; set; }
public FloorMongoDBVM? Floor { get; set; }
public WorkAreaMongoDB? WorkArea { get; set; }
public double CompletedWork { get; set; }
public double PlannedWork { get; set; }
}
}

View File

@ -0,0 +1,16 @@
namespace Marco.Pms.Model.MongoDBModels
{
public class WorkAreaMongoDB
{
public string Id { get; set; } = string.Empty;
public string FloorId { get; set; } = string.Empty;
public string? AreaName { get; set; }
public double PlannedWork { get; set; }
public double CompletedWork { get; set; }
}
public class WorkAreaMongoDBVM
{
public string Id { get; set; } = string.Empty;
public string? AreaName { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace Marco.Pms.Model.MongoDBModels
{
public class WorkCategoryMasterMongoDB
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,17 @@
namespace Marco.Pms.Model.MongoDBModels
{
public class WorkItemMongoDB
{
public string Id { get; set; } = string.Empty;
public string WorkAreaId { get; set; } = string.Empty;
public ActivityMasterMongoDB? ActivityMaster { get; set; }
public WorkCategoryMasterMongoDB? WorkCategoryMaster { get; set; }
public string? ParentTaskId { get; set; } = null;
public double PlannedWork { get; set; } = 0;
public double TodaysAssigned { get; set; } = 0;
public double CompletedWork { get; set; } = 0;
public string? Description { get; set; }
public DateTime TaskDate { get; set; }
public DateTime ExpireAt { get; set; } = DateTime.UtcNow.Date.AddDays(1);
}
}

View File

@ -0,0 +1,14 @@
namespace Marco.Pms.Model.Utilities
{
public class ImageFilter
{
public List<Guid>? BuildingIds { get; set; }
public List<Guid>? FloorIds { get; set; }
public List<Guid>? WorkAreaIds { get; set; }
public List<Guid>? WorkCategoryIds { get; set; }
public List<Guid>? ActivityIds { get; set; }
public List<Guid>? UploadedByIds { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
}
}

View File

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

View File

@ -9,6 +9,7 @@ namespace Marco.Pms.Model.ViewModels.Directory
public Guid Id { get; set; } public Guid Id { get; set; }
public string? Name { get; set; } public string? Name { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
public string? Designation { get; set; }
public string? Organization { get; set; } public string? Organization { get; set; }
public string? Address { get; set; } public string? Address { get; set; }
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }

View File

@ -12,6 +12,7 @@ namespace Marco.Pms.Model.ViewModels.Directory
public ContactCategoryVM? ContactCategory { get; set; } public ContactCategoryVM? ContactCategory { get; set; }
public List<Guid>? BucketIds { get; set; } public List<Guid>? BucketIds { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
public string? Designation { get; set; }
public string? Organization { get; set; } public string? Organization { get; set; }
public string? Address { get; set; } public string? Address { get; set; }
public List<ContactTagVM>? Tags { get; set; } public List<ContactTagVM>? Tags { get; set; }

View File

@ -0,0 +1,10 @@
using Marco.Pms.Model.Dtos.Project;
namespace Marco.Pms.Model.ViewModels.Projects
{
public class OldProjectVM : ProjectDto
{
public List<BuildingVM>? Buildings { get; set; }
}
}

View File

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

View File

@ -1,10 +1,17 @@
using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Master;
namespace Marco.Pms.Model.ViewModels.Projects namespace Marco.Pms.Model.ViewModels.Projects
{ {
public class ProjectVM : ProjectDto public class ProjectVM
{ {
public List<BuildingVM>? Buildings { get; set; } public Guid Id { get; set; }
public string? Name { get; set; }
public string? ShortName { get; set; }
public string? ProjectAddress { get; set; }
public string? ContactPerson { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public StatusMaster? ProjectStatus { get; set; }
} }
} }

View File

@ -1,14 +1,15 @@
using System.Globalization; using Marco.Pms.DataAccess.Data;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.AttendanceModule; using Marco.Pms.Model.AttendanceModule;
using Marco.Pms.Model.Dtos.Attendance; using Marco.Pms.Model.Dtos.Attendance;
using Marco.Pms.Model.Employees; using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Mapper;
using Marco.Pms.Model.Projects; using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities; using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.AttendanceVM; using Marco.Pms.Model.ViewModels.AttendanceVM;
using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Hubs;
using Marco.Pms.Services.Service; using Marco.Pms.Services.Service;
using Marco.Pms.Services.Service.ServiceInterfaces;
using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service; using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -16,6 +17,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Globalization;
using Document = Marco.Pms.Model.DocumentManager.Document; using Document = Marco.Pms.Model.DocumentManager.Document;
namespace MarcoBMS.Services.Controllers namespace MarcoBMS.Services.Controllers
@ -27,7 +29,7 @@ namespace MarcoBMS.Services.Controllers
{ {
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
private readonly EmployeeHelper _employeeHelper; private readonly EmployeeHelper _employeeHelper;
private readonly ProjectsHelper _projectsHelper; private readonly IProjectServices _projectServices;
private readonly UserHelper _userHelper; private readonly UserHelper _userHelper;
private readonly S3UploadService _s3Service; private readonly S3UploadService _s3Service;
private readonly PermissionServices _permission; private readonly PermissionServices _permission;
@ -36,11 +38,11 @@ namespace MarcoBMS.Services.Controllers
public AttendanceController( public AttendanceController(
ApplicationDbContext context, EmployeeHelper employeeHelper, ProjectsHelper projectsHelper, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permission, IHubContext<MarcoHub> signalR) ApplicationDbContext context, EmployeeHelper employeeHelper, IProjectServices projectServices, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permission, IHubContext<MarcoHub> signalR)
{ {
_context = context; _context = context;
_employeeHelper = employeeHelper; _employeeHelper = employeeHelper;
_projectsHelper = projectsHelper; _projectServices = projectServices;
_userHelper = userHelper; _userHelper = userHelper;
_s3Service = s3Service; _s3Service = s3Service;
_logger = logger; _logger = logger;
@ -61,7 +63,13 @@ namespace MarcoBMS.Services.Controllers
{ {
Guid TenantId = GetTenantId(); 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>(); List<AttendanceLogVM> attendanceLogVMs = new List<AttendanceLogVM>();
foreach (var attendanceLog in lstAttendance) foreach (var attendanceLog in lstAttendance)
{ {
@ -83,18 +91,18 @@ namespace MarcoBMS.Services.Controllers
if (dateFrom != null && DateTime.TryParse(dateFrom, out fromDate) == false) if (dateFrom != null && DateTime.TryParse(dateFrom, out fromDate) == false)
{ {
_logger.LogError("User sent Invalid from Date while featching attendance logs"); _logger.LogWarning("User sent Invalid from Date while featching attendance logs");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
} }
if (dateTo != null && DateTime.TryParse(dateTo, out toDate) == false) if (dateTo != null && DateTime.TryParse(dateTo, out toDate) == false)
{ {
_logger.LogError("User sent Invalid to Date while featching attendance logs"); _logger.LogWarning("User sent Invalid to Date while featching attendance logs");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
} }
if (employeeId == Guid.Empty) if (employeeId == Guid.Empty)
{ {
_logger.LogError("The employee Id sent by user is empty"); _logger.LogWarning("The employee Id sent by user is empty");
return BadRequest(ApiResponse<object>.ErrorResponse("Employee ID is required and must not be Empty.", "Employee ID is required and must not be empty.", 400)); 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(); List<Attendance> attendances = await _context.Attendes.Where(c => c.EmployeeID == employeeId && c.TenantId == TenantId && c.AttendanceDate.Date >= fromDate && c.AttendanceDate.Date <= toDate).ToListAsync();
@ -139,9 +147,9 @@ namespace MarcoBMS.Services.Controllers
{ {
Guid TenantId = GetTenantId(); Guid TenantId = GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var hasTeamAttendancePermission = await _permission.HasPermission(new Guid("915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"), LoggedInEmployee.Id); var hasTeamAttendancePermission = await _permission.HasPermission(PermissionsMaster.TeamAttendance, LoggedInEmployee.Id);
var hasSelfAttendancePermission = await _permission.HasPermission(new Guid("ccb0589f-712b-43de-92ed-5b6088e7dc4e"), LoggedInEmployee.Id); var hasSelfAttendancePermission = await _permission.HasPermission(PermissionsMaster.SelfAttendance, LoggedInEmployee.Id);
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId.ToString()); var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId);
if (!hasProjectPermission) if (!hasProjectPermission)
{ {
@ -154,18 +162,18 @@ namespace MarcoBMS.Services.Controllers
if (dateFrom != null && DateTime.TryParse(dateFrom, out fromDate) == false) if (dateFrom != null && DateTime.TryParse(dateFrom, out fromDate) == false)
{ {
_logger.LogError("User sent Invalid fromDate while featching attendance logs"); _logger.LogWarning("User sent Invalid fromDate while featching attendance logs");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
} }
if (dateTo != null && DateTime.TryParse(dateTo, out toDate) == false) if (dateTo != null && DateTime.TryParse(dateTo, out toDate) == false)
{ {
_logger.LogError("User sent Invalid toDate while featching attendance logs"); _logger.LogWarning("User sent Invalid toDate while featching attendance logs");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
} }
if (projectId == Guid.Empty) if (projectId == Guid.Empty)
{ {
_logger.LogError("The project Id sent by user is less than or equal to zero"); _logger.LogWarning("The project Id sent by user is less than or equal to zero");
return BadRequest(ApiResponse<object>.ErrorResponse("Project ID is required and must be greater than zero.", "Project ID is required and must be greater than zero.", 400)); 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));
} }
@ -181,7 +189,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<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 _projectsHelper.GetTeamByProject(TenantId, projectId, true); List<ProjectAllocation> projectteam = await _projectServices.GetTeamByProject(TenantId, projectId, true);
var jobRole = await _context.JobRoles.ToListAsync(); var jobRole = await _context.JobRoles.ToListAsync();
foreach (Attendance? attendance in lstAttendance) foreach (Attendance? attendance in lstAttendance)
{ {
@ -255,9 +263,9 @@ namespace MarcoBMS.Services.Controllers
{ {
Guid TenantId = GetTenantId(); Guid TenantId = GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var hasTeamAttendancePermission = await _permission.HasPermission(new Guid("915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"), LoggedInEmployee.Id); var hasTeamAttendancePermission = await _permission.HasPermission(PermissionsMaster.TeamAttendance, LoggedInEmployee.Id);
var hasSelfAttendancePermission = await _permission.HasPermission(new Guid("ccb0589f-712b-43de-92ed-5b6088e7dc4e"), LoggedInEmployee.Id); var hasSelfAttendancePermission = await _permission.HasPermission(PermissionsMaster.SelfAttendance, LoggedInEmployee.Id);
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId.ToString()); var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId);
if (!hasProjectPermission) if (!hasProjectPermission)
{ {
@ -269,13 +277,13 @@ namespace MarcoBMS.Services.Controllers
if (date != null && DateTime.TryParse(date, out forDate) == false) if (date != null && DateTime.TryParse(date, out forDate) == false)
{ {
_logger.LogError("User sent Invalid Date while featching attendance logs"); _logger.LogWarning("User sent Invalid Date while featching attendance logs");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
} }
if (projectId == Guid.Empty) if (projectId == Guid.Empty)
{ {
_logger.LogError("The project Id sent by user is less than or equal to zero"); _logger.LogWarning("The project Id sent by user is less than or equal to zero");
return BadRequest(ApiResponse<object>.ErrorResponse("Project ID is required and must be greater than zero.", "Project ID is required and must be greater than zero.", 400)); 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));
} }
@ -288,7 +296,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<Attendance> lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date == forDate && c.TenantId == TenantId).ToListAsync();
List<ProjectAllocation> projectteam = await _projectsHelper.GetTeamByProject(TenantId, projectId, IncludeInActive); List<ProjectAllocation> projectteam = await _projectServices.GetTeamByProject(TenantId, projectId, IncludeInActive);
var idList = projectteam.Select(p => p.EmployeeId).ToList(); 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 emp = await _context.Employees.Where(e => idList.Contains(e.Id)).Include(e => e.JobRole).ToListAsync();
var jobRole = await _context.JobRoles.ToListAsync(); var jobRole = await _context.JobRoles.ToListAsync();
@ -361,7 +369,7 @@ namespace MarcoBMS.Services.Controllers
Guid TenantId = GetTenantId(); Guid TenantId = GetTenantId();
Employee LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); Employee LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var result = new List<EmployeeAttendanceVM>(); var result = new List<EmployeeAttendanceVM>();
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId.ToString()); var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId);
if (!hasProjectPermission) if (!hasProjectPermission)
{ {
@ -371,8 +379,7 @@ namespace MarcoBMS.Services.Controllers
List<Attendance> lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE && c.TenantId == TenantId).ToListAsync(); List<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 idList = projectteam.Select(p => p.EmployeeId).ToList();
var jobRole = await _context.JobRoles.ToListAsync(); var jobRole = await _context.JobRoles.ToListAsync();
@ -419,7 +426,7 @@ namespace MarcoBMS.Services.Controllers
.SelectMany(v => v.Errors) .SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage) .Select(e => e.ErrorMessage)
.ToList(); .ToList();
_logger.LogError("User sent Invalid Date while marking attendance"); _logger.LogWarning("User sent Invalid Date while marking attendance \n {Error}", string.Join(",", errors));
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
} }
@ -433,14 +440,14 @@ namespace MarcoBMS.Services.Controllers
if (recordAttendanceDot.MarkTime == null) if (recordAttendanceDot.MarkTime == null)
{ {
_logger.LogError("User sent Invalid Mark Time while marking attendance"); _logger.LogWarning("User sent Invalid Mark Time while marking attendance");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Mark Time", "Invalid Mark Time", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Mark Time", "Invalid Mark Time", 400));
} }
DateTime finalDateTime = GetDateFromTimeStamp(recordAttendanceDot.Date, recordAttendanceDot.MarkTime); DateTime finalDateTime = GetDateFromTimeStamp(recordAttendanceDot.Date, recordAttendanceDot.MarkTime);
if (recordAttendanceDot.Comment == null) if (recordAttendanceDot.Comment == null)
{ {
_logger.LogError("User sent Invalid comment while marking attendance"); _logger.LogWarning("User sent Invalid comment while marking attendance");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Comment", "Invalid Comment", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Comment", "Invalid Comment", 400));
} }
@ -474,7 +481,7 @@ namespace MarcoBMS.Services.Controllers
} }
else else
{ {
_logger.LogError("Employee {EmployeeId} sent regularization request but it check-out time is earlier than check-out"); _logger.LogWarning("Employee {EmployeeId} sent regularization request but it check-out time is earlier than check-out");
return BadRequest(ApiResponse<object>.ErrorResponse("Check-out time must be later than check-in time", "Check-out time must be later than check-in time", 400)); 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 // do nothing
@ -579,7 +586,7 @@ namespace MarcoBMS.Services.Controllers
catch (Exception ex) catch (Exception ex)
{ {
await transaction.RollbackAsync(); // Rollback on failure await transaction.RollbackAsync(); // Rollback on failure
_logger.LogError("{Error} while marking attendance", ex.Message); _logger.LogError(ex, "An Error occured while marking attendance");
var response = new var response = new
{ {
message = ex.Message, message = ex.Message,
@ -598,12 +605,13 @@ namespace MarcoBMS.Services.Controllers
if (!ModelState.IsValid) if (!ModelState.IsValid)
{ {
var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList();
_logger.LogError("Invalid attendance model received."); _logger.LogWarning("Invalid attendance model received. \n {Error}", string.Join(",", errors));
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
} }
Guid tenantId = GetTenantId(); Guid tenantId = GetTenantId();
var currentEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var batchId = Guid.NewGuid();
using var transaction = await _context.Database.BeginTransactionAsync(); using var transaction = await _context.Database.BeginTransactionAsync();
try try
@ -704,10 +712,12 @@ namespace MarcoBMS.Services.Controllers
document = new Document document = new Document
{ {
BatchId = batchId,
UploadedById = loggedInEmployee.Id,
FileName = recordAttendanceDot.Image.FileName ?? "", FileName = recordAttendanceDot.Image.FileName ?? "",
ContentType = recordAttendanceDot.Image.ContentType, ContentType = recordAttendanceDot.Image.ContentType,
S3Key = objectKey, S3Key = objectKey,
Base64Data = recordAttendanceDot.Image.Base64Data, //Base64Data = recordAttendanceDot.Image.Base64Data,
FileSize = recordAttendanceDot.Image.FileSize, FileSize = recordAttendanceDot.Image.FileSize,
UploadedAt = recordAttendanceDot.Date, UploadedAt = recordAttendanceDot.Date,
TenantId = tenantId TenantId = tenantId
@ -728,7 +738,7 @@ namespace MarcoBMS.Services.Controllers
Longitude = recordAttendanceDot.Longitude, Longitude = recordAttendanceDot.Longitude,
DocumentId = document?.Id, DocumentId = document?.Id,
TenantId = tenantId, TenantId = tenantId,
UpdatedBy = recordAttendanceDot.EmployeeID, UpdatedBy = loggedInEmployee.Id,
UpdatedOn = recordAttendanceDot.Date UpdatedOn = recordAttendanceDot.Date
}; };
_context.AttendanceLogs.Add(attendanceLog); _context.AttendanceLogs.Add(attendanceLog);
@ -755,7 +765,7 @@ namespace MarcoBMS.Services.Controllers
var notification = new var notification = new
{ {
LoggedInUserId = currentEmployee.Id, LoggedInUserId = loggedInEmployee.Id,
Keyword = "Attendance", Keyword = "Attendance",
Activity = recordAttendanceDot.Id == Guid.Empty ? 1 : 0, Activity = recordAttendanceDot.Id == Guid.Empty ? 1 : 0,
ProjectId = attendance.ProjectID, ProjectId = attendance.ProjectID,
@ -771,7 +781,7 @@ namespace MarcoBMS.Services.Controllers
catch (Exception ex) catch (Exception ex)
{ {
await transaction.RollbackAsync(); await transaction.RollbackAsync();
_logger.LogError("Error while recording attendance : {Error}", ex.Message); _logger.LogError(ex, "Error while recording attendance");
return BadRequest(ApiResponse<object>.ErrorResponse("Something went wrong", ex.Message, 500)); return BadRequest(ApiResponse<object>.ErrorResponse("Something went wrong", ex.Message, 500));
} }
} }

View File

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

View File

@ -6,6 +6,7 @@ using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities; using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.DashBoard; using Marco.Pms.Model.ViewModels.DashBoard;
using Marco.Pms.Services.Service; using Marco.Pms.Services.Service;
using Marco.Pms.Services.Service.ServiceInterfaces;
using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service; using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -21,12 +22,15 @@ namespace Marco.Pms.Services.Controllers
{ {
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
private readonly UserHelper _userHelper; private readonly UserHelper _userHelper;
private readonly IProjectServices _projectServices;
private readonly ILoggingService _logger; private readonly ILoggingService _logger;
private readonly PermissionServices _permissionServices; private readonly PermissionServices _permissionServices;
public DashboardController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, PermissionServices permissionServices) public static readonly Guid ActiveId = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731");
public DashboardController(ApplicationDbContext context, UserHelper userHelper, IProjectServices projectServices, ILoggingService logger, PermissionServices permissionServices)
{ {
_context = context; _context = context;
_userHelper = userHelper; _userHelper = userHelper;
_projectServices = projectServices;
_logger = logger; _logger = logger;
_permissionServices = permissionServices; _permissionServices = permissionServices;
} }
@ -162,46 +166,188 @@ namespace Marco.Pms.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(projectDashboardVM, "Success", 200)); return Ok(ApiResponse<object>.SuccessResponse(projectDashboardVM, "Success", 200));
} }
/// <summary>
/// Retrieves a dashboard summary of total employees and today's attendance.
/// If a projectId is provided, it returns totals for that project; otherwise, for all accessible active projects.
/// </summary>
/// <param name="projectId">Optional. The ID of a specific project to get totals for.</param>
[HttpGet("teams")] [HttpGet("teams")]
public async Task<IActionResult> GetTotalEmployees() public async Task<IActionResult> GetTotalEmployees([FromQuery] Guid? projectId)
{ {
var tenantId = _userHelper.GetTenantId(); try
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var date = DateTime.UtcNow.Date;
var Employees = await _context.Employees.Where(e => e.TenantId == tenantId && e.IsActive == true).Select(e => e.Id).ToListAsync();
var checkedInEmployee = await _context.Attendes.Where(e => e.InTime != null ? e.InTime.Value.Date == date : false).Select(e => e.EmployeeID).ToListAsync();
TeamDashboardVM teamDashboardVM = new TeamDashboardVM
{ {
TotalEmployees = Employees.Count(), var tenantId = _userHelper.GetTenantId();
InToday = checkedInEmployee.Distinct().Count() var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
};
_logger.LogInfo("Today's total checked in employees fetched by employee {EmployeeId}", LoggedInEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(teamDashboardVM, "Success", 200));
}
[HttpGet("tasks")] _logger.LogInfo("GetTotalEmployees called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty);
public async Task<IActionResult> GetTotalTasks()
{ // --- Step 1: Get the list of projects the user can access ---
var tenantId = _userHelper.GetTenantId(); // This query is more efficient as it only selects the IDs needed.
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var projects = await _projectServices.GetMyProjectIdsAsync(tenantId, loggedInEmployee);
var Tasks = await _context.WorkItems.Where(t => t.TenantId == tenantId).Select(t => new { PlannedWork = t.PlannedWork, CompletedWork = t.CompletedWork }).ToListAsync();
TasksDashboardVM tasksDashboardVM = new TasksDashboardVM var accessibleActiveProjectIds = await _context.Projects
{ .Where(p => p.ProjectStatusId == ActiveId && projects.Contains(p.Id))
TotalTasks = 0, .Select(p => p.Id)
CompletedTasks = 0 .ToListAsync();
};
foreach (var task in Tasks) if (!accessibleActiveProjectIds.Any())
{ {
tasksDashboardVM.TotalTasks += task.PlannedWork; _logger.LogInfo("User {UserId} has no accessible active projects.", loggedInEmployee.Id);
tasksDashboardVM.CompletedTasks += task.CompletedWork; return Ok(ApiResponse<TeamDashboardVM>.SuccessResponse(new TeamDashboardVM(), "No accessible active projects found.", 200));
}
// --- Step 2: Build the list of project IDs to query against ---
List<Guid> finalProjectIds;
if (projectId.HasValue)
{
// Security Check: Ensure the requested project is in the user's accessible list.
var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value);
if (!hasPermission)
{
_logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId} (not active or not accessible).", loggedInEmployee.Id, projectId.Value);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to view this project, or it is not active.", 403));
}
finalProjectIds = new List<Guid> { projectId.Value };
}
else
{
finalProjectIds = accessibleActiveProjectIds;
}
// --- Step 3: Run efficient aggregation queries SEQUENTIALLY ---
// Since we only have one DbContext instance, we await each query one by one.
// Query 1: Count total distinct employees allocated to the final project list
int totalEmployees = await _context.ProjectAllocations
.Where(pa => pa.TenantId == tenantId &&
finalProjectIds.Contains(pa.ProjectId) &&
pa.IsActive)
.Select(pa => pa.EmployeeId)
.Distinct()
.CountAsync();
// Query 2: Count total distinct employees who checked in today
// Use an efficient date range check
var today = DateTime.UtcNow.Date;
var tomorrow = today.AddDays(1);
int inTodays = await _context.Attendes
.Where(a => a.InTime >= today && a.InTime < tomorrow &&
finalProjectIds.Contains(a.ProjectID))
.Select(a => a.EmployeeID)
.Distinct()
.CountAsync();
// --- Step 4: Assemble the response ---
var teamDashboardVM = new TeamDashboardVM
{
TotalEmployees = totalEmployees,
InToday = inTodays
};
_logger.LogInfo("Successfully fetched team dashboard for user {UserId}. Total: {TotalEmployees}, InToday: {InToday}",
loggedInEmployee.Id, teamDashboardVM.TotalEmployees, teamDashboardVM.InToday);
return Ok(ApiResponse<TeamDashboardVM>.SuccessResponse(teamDashboardVM, "Dashboard data retrieved successfully.", 200));
}
catch (Exception ex)
{
_logger.LogError(ex, "An unexpected error occurred in GetTotalEmployees for projectId {ProjectId}", projectId ?? Guid.Empty);
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred.", null, 500));
} }
_logger.LogInfo("Total targeted tasks and total completed tasks fetched by employee {EmployeeId}", LoggedInEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(tasksDashboardVM, "Success", 200));
} }
/// <summary>
/// Retrieves a dashboard summary of total planned and completed tasks.
/// If a projectId is provided, it returns totals for that project; otherwise, for all accessible projects.
/// </summary>
/// <param name="projectId">Optional. The ID of a specific project to get totals for.</param>
/// <returns>An ApiResponse containing the task dashboard summary.</returns>
[HttpGet("tasks")] // Example route
public async Task<IActionResult> GetTotalTasks1([FromQuery] Guid? projectId) // Changed to FromQuery as it's optional
{
try
{
var tenantId = _userHelper.GetTenantId();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
_logger.LogInfo("GetTotalTasks called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty);
// --- Step 1: Build the base IQueryable for WorkItems ---
// This query is NOT executed yet. We will add more filters to it.
var baseWorkItemQuery = _context.WorkItems.Where(t => t.TenantId == tenantId);
// --- Step 2: Apply Filters based on the request (Project or All Accessible) ---
if (projectId.HasValue)
{
// --- Logic for a SINGLE Project ---
// 2a. Security Check: Verify permission for the specific project.
var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value);
if (!hasPermission)
{
_logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, projectId.Value);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to view this project.", 403));
}
// 2b. Add project-specific filter to the base query.
// This is more efficient than fetching workAreaIds separately.
baseWorkItemQuery = baseWorkItemQuery
.Where(wi => wi.WorkArea != null &&
wi.WorkArea.Floor != null &&
wi.WorkArea.Floor.Building != null &&
wi.WorkArea.Floor.Building.ProjectId == projectId.Value);
}
else
{
// --- Logic for ALL Accessible Projects ---
// 2c. Get a list of all projects the user is allowed to see.
var accessibleProjectIds = await _projectServices.GetMyProjectIdsAsync(tenantId, loggedInEmployee);
if (!accessibleProjectIds.Any())
{
_logger.LogInfo("User {UserId} has no accessible projects.", loggedInEmployee.Id);
// Return a zeroed-out dashboard if the user has no projects.
return Ok(ApiResponse<TasksDashboardVM>.SuccessResponse(new TasksDashboardVM(), "No accessible projects found.", 200));
}
// 2d. Add a filter to include all work items from all accessible projects.
baseWorkItemQuery = baseWorkItemQuery
.Where(wi => wi.WorkArea != null &&
wi.WorkArea.Floor != null &&
wi.WorkArea.Floor.Building != null &&
accessibleProjectIds.Contains(wi.WorkArea.Floor.Building.ProjectId));
}
// --- Step 3: Execute the Aggregation Query ON THE DATABASE SERVER ---
// This is the most powerful optimization. The database does all the summing.
// EF Core translates this into a single, efficient SQL query like:
// SELECT SUM(PlannedWork), SUM(CompletedWork) FROM WorkItems WHERE ...
var tasksDashboardVM = await baseWorkItemQuery
.GroupBy(x => 1) // Group by a constant to aggregate all rows into one result.
.Select(g => new TasksDashboardVM
{
TotalTasks = g.Sum(wi => wi.PlannedWork),
CompletedTasks = g.Sum(wi => wi.CompletedWork)
})
.FirstOrDefaultAsync(); // Use FirstOrDefaultAsync as GroupBy might return no rows.
// If the query returned no work items, the result will be null. Default to a zeroed object.
tasksDashboardVM ??= new TasksDashboardVM();
_logger.LogInfo("Successfully fetched task dashboard for user {UserId}. Total: {TotalTasks}, Completed: {CompletedTasks}",
loggedInEmployee.Id, tasksDashboardVM.TotalTasks, tasksDashboardVM.CompletedTasks);
return Ok(ApiResponse<TasksDashboardVM>.SuccessResponse(tasksDashboardVM, "Dashboard data retrieved successfully.", 200));
}
catch (Exception ex)
{
_logger.LogError(ex, "An unexpected error occurred in GetTotalTasks for projectId {ProjectId}", projectId ?? Guid.Empty);
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred.", null, 500));
}
}
[HttpGet("pending-attendance")] [HttpGet("pending-attendance")]
public async Task<IActionResult> GetPendingAttendance() public async Task<IActionResult> GetPendingAttendance()
{ {
@ -221,7 +367,7 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Number of pending regularization and pending check-out are fetched successfully for employee {EmployeeId}", LoggedInEmployee.Id); _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)); return Ok(ApiResponse<object>.SuccessResponse(response, "Pending regularization and pending check-out are fetched successfully", 200));
} }
_logger.LogError("No attendance entry was found for employee {EmployeeId}", LoggedInEmployee.Id); _logger.LogWarning("No attendance entry was found for employee {EmployeeId}", LoggedInEmployee.Id);
return NotFound(ApiResponse<object>.ErrorResponse("No attendance entry was found for this employee", "No attendance entry was found for this employee", 404)); return NotFound(ApiResponse<object>.ErrorResponse("No attendance entry was found for this employee", "No attendance entry was found for this employee", 404));
} }
@ -235,14 +381,14 @@ namespace Marco.Pms.Services.Controllers
List<ProjectProgressionVM>? projectProgressionVMs = new List<ProjectProgressionVM>(); List<ProjectProgressionVM>? projectProgressionVMs = new List<ProjectProgressionVM>();
if (date != null && DateTime.TryParse(date, out currentDate) == false) if (date != null && DateTime.TryParse(date, out currentDate) == false)
{ {
_logger.LogError($"user send invalid date"); _logger.LogWarning($"user send invalid date");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid date.", "Invalid date.", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid date.", "Invalid date.", 400));
} }
Project? project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == projectId); Project? project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == projectId);
if (project == null) if (project == null)
{ {
_logger.LogError("Employee {EmployeeId} was attempted to get project attendance for date {Date}, but project not found in database", LoggedInEmployee.Id, currentDate); _logger.LogWarning("Employee {EmployeeId} was attempted to get project attendance for date {Date}, but project not found in database", LoggedInEmployee.Id, currentDate);
return NotFound(ApiResponse<object>.ErrorResponse("Project not found", "Project not found", 404)); 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(); List<ProjectAllocation>? projectAllocation = await _context.ProjectAllocations.Where(p => p.ProjectId == projectId && p.IsActive && p.TenantId == tenantId).ToListAsync();
@ -288,14 +434,14 @@ namespace Marco.Pms.Services.Controllers
DateTime currentDate = DateTime.UtcNow; DateTime currentDate = DateTime.UtcNow;
if (date != null && DateTime.TryParse(date, out currentDate) == false) if (date != null && DateTime.TryParse(date, out currentDate) == false)
{ {
_logger.LogError($"user send invalid date"); _logger.LogWarning($"user send invalid date");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid date.", "Invalid date.", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid date.", "Invalid date.", 400));
} }
Project? project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == projectId); Project? project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == projectId);
if (project == null) if (project == null)
{ {
_logger.LogError("Employee {EmployeeId} was attempted to get activities performed for date {Date}, but project not found in database", LoggedInEmployee.Id, currentDate); _logger.LogWarning("Employee {EmployeeId} was attempted to get activities performed for date {Date}, but project not found in database", LoggedInEmployee.Id, currentDate);
return NotFound(ApiResponse<object>.ErrorResponse("Project not found", "Project not found", 404)); return NotFound(ApiResponse<object>.ErrorResponse("Project not found", "Project not found", 404));
} }
@ -373,7 +519,7 @@ namespace Marco.Pms.Services.Controllers
// Step 2: Permission check // Step 2: Permission check
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
bool hasAssigned = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.ToString()); bool hasAssigned = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId);
if (!hasAssigned) if (!hasAssigned)
{ {

View File

@ -33,20 +33,7 @@ namespace Marco.Pms.Services.Controllers
CategoryIds = categoryIds CategoryIds = categoryIds
}; };
var response = await _directoryHelper.GetListOfContacts(search, active, filterDto, projectId); var response = await _directoryHelper.GetListOfContacts(search, active, filterDto, projectId);
return StatusCode(response.StatusCode, response);
if (response.StatusCode == 200)
{
return Ok(response);
}
else if (response.StatusCode == 401)
{
return Unauthorized(response);
}
else
{
return BadRequest(response);
}
} }
@ -54,18 +41,7 @@ namespace Marco.Pms.Services.Controllers
public async Task<IActionResult> GetContactsListByBucketId(Guid bucketId) public async Task<IActionResult> GetContactsListByBucketId(Guid bucketId)
{ {
var response = await _directoryHelper.GetContactsListByBucketId(bucketId); var response = await _directoryHelper.GetContactsListByBucketId(bucketId);
if (response.StatusCode == 200) return StatusCode(response.StatusCode, response);
{
return Ok(response);
}
else if (response.StatusCode == 401)
{
return Unauthorized(response);
}
else
{
return BadRequest(response);
}
} }
[HttpPost] [HttpPost]
@ -77,65 +53,38 @@ namespace Marco.Pms.Services.Controllers
.SelectMany(v => v.Errors) .SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage) .Select(e => e.ErrorMessage)
.ToList(); .ToList();
_logger.LogError("User sent Invalid Date while marking attendance"); _logger.LogWarning("User sent Invalid Date while marking attendance");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
} }
var response = await _directoryHelper.CreateContact(createContact); var response = await _directoryHelper.CreateContact(createContact);
if (response.StatusCode == 200) return StatusCode(response.StatusCode, response);
{
return Ok(response);
}
else
{
return BadRequest(response);
}
} }
[HttpPut("{id}")] [HttpPut("{id}")]
public async Task<IActionResult> UpdateContact(Guid id, [FromBody] UpdateContactDto updateContact) public async Task<IActionResult> UpdateContact(Guid id, [FromBody] UpdateContactDto updateContact)
{ {
var response = await _directoryHelper.UpdateContact(id, updateContact); var response = await _directoryHelper.UpdateContact(id, updateContact);
if (response.StatusCode == 200) return StatusCode(response.StatusCode, response);
{
return Ok(response);
}
else if (response.StatusCode == 404)
{
return NotFound(response);
}
else if (response.StatusCode == 401)
{
return Unauthorized(response);
}
else
{
return BadRequest(response);
}
} }
[HttpGet("profile/{id}")] [HttpGet("profile/{id}")]
public async Task<IActionResult> GetContactProfile(Guid id) public async Task<IActionResult> GetContactProfile(Guid id)
{ {
var response = await _directoryHelper.GetContactProfile(id); var response = await _directoryHelper.GetContactProfile(id);
if (response.StatusCode == 200) return StatusCode(response.StatusCode, response);
{
return Ok(response);
}
else if (response.StatusCode == 404)
{
return NotFound(response);
}
else
{
return BadRequest(response);
}
} }
[HttpGet("organization")] [HttpGet("organization")]
public async Task<IActionResult> GetOrganizationList() public async Task<IActionResult> GetOrganizationList()
{ {
var response = await _directoryHelper.GetOrganizationList(); var response = await _directoryHelper.GetOrganizationList();
return Ok(response); return StatusCode(response.StatusCode, response);
}
[HttpGet("designations")]
public async Task<IActionResult> GetDesignationList()
{
var response = await _directoryHelper.GetDesignationList();
return StatusCode(response.StatusCode, response);
} }
[HttpDelete("{id}")] [HttpDelete("{id}")]
@ -256,7 +205,7 @@ namespace Marco.Pms.Services.Controllers
.SelectMany(v => v.Errors) .SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage) .Select(e => e.ErrorMessage)
.ToList(); .ToList();
_logger.LogError("User sent Invalid Date while marking attendance"); _logger.LogWarning("User sent Invalid Date while marking attendance");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
} }
var response = await _directoryHelper.CreateBucket(bucketDto); var response = await _directoryHelper.CreateBucket(bucketDto);

View File

@ -1,6 +1,4 @@
using System.Data; using Marco.Pms.DataAccess.Data;
using System.Net;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Dtos.Attendance; using Marco.Pms.Model.Dtos.Attendance;
using Marco.Pms.Model.Dtos.Employees; using Marco.Pms.Model.Dtos.Employees;
using Marco.Pms.Model.Employees; using Marco.Pms.Model.Employees;
@ -11,6 +9,7 @@ using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Model.ViewModels.Employee;
using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Hubs;
using Marco.Pms.Services.Service; using Marco.Pms.Services.Service;
using Marco.Pms.Services.Service.ServiceInterfaces;
using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service; using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -18,6 +17,8 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Data;
using System.Net;
namespace MarcoBMS.Services.Controllers namespace MarcoBMS.Services.Controllers
{ {
@ -37,15 +38,13 @@ namespace MarcoBMS.Services.Controllers
private readonly ILoggingService _logger; private readonly ILoggingService _logger;
private readonly IHubContext<MarcoHub> _signalR; private readonly IHubContext<MarcoHub> _signalR;
private readonly PermissionServices _permission; private readonly PermissionServices _permission;
private readonly ProjectsHelper _projectsHelper; private readonly IProjectServices _projectServices;
private readonly Guid ViewAllEmployees;
private readonly Guid ViewTeamMembers;
private readonly Guid tenantId; private readonly Guid tenantId;
public EmployeeController(UserManager<ApplicationUser> userManager, IEmailSender emailSender, public EmployeeController(UserManager<ApplicationUser> userManager, IEmailSender emailSender,
ApplicationDbContext context, EmployeeHelper employeeHelper, UserHelper userHelper, IConfiguration configuration, ILoggingService logger, ApplicationDbContext context, EmployeeHelper employeeHelper, UserHelper userHelper, IConfiguration configuration, ILoggingService logger,
IHubContext<MarcoHub> signalR, PermissionServices permission, ProjectsHelper projectsHelper) IHubContext<MarcoHub> signalR, PermissionServices permission, IProjectServices projectServices)
{ {
_context = context; _context = context;
_userManager = userManager; _userManager = userManager;
@ -56,9 +55,7 @@ namespace MarcoBMS.Services.Controllers
_logger = logger; _logger = logger;
_signalR = signalR; _signalR = signalR;
_permission = permission; _permission = permission;
ViewAllEmployees = Guid.Parse("60611762-7f8a-4fb5-b53f-b1139918796b"); _projectServices = projectServices;
ViewTeamMembers = Guid.Parse("b82d2b7e-0d52-45f3-997b-c008ea460e7f");
_projectsHelper = projectsHelper;
tenantId = _userHelper.GetTenantId(); tenantId = _userHelper.GetTenantId();
} }
@ -123,11 +120,10 @@ namespace MarcoBMS.Services.Controllers
loggedInEmployee.Id, projectid ?? Guid.Empty, ShowInactive); loggedInEmployee.Id, projectid ?? Guid.Empty, ShowInactive);
// Step 3: Fetch project access and permissions // Step 3: Fetch project access and permissions
List<Project> projects = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); var projectIds = await _projectServices.GetMyProjectIdsAsync(tenantId, loggedInEmployee);
var projectIds = projects.Select(p => p.Id).ToList();
var hasViewAllEmployeesPermission = await _permission.HasPermission(ViewAllEmployees, loggedInEmployee.Id); var hasViewAllEmployeesPermission = await _permission.HasPermission(PermissionsMaster.ViewAllEmployees, loggedInEmployee.Id);
var hasViewTeamMembersPermission = await _permission.HasPermission(ViewTeamMembers, loggedInEmployee.Id); var hasViewTeamMembersPermission = await _permission.HasPermission(PermissionsMaster.ViewTeamMembers, loggedInEmployee.Id);
List<EmployeeVM> result = new(); List<EmployeeVM> result = new();
@ -387,7 +383,7 @@ namespace MarcoBMS.Services.Controllers
Employee? existingEmployee = await _context.Employees.FirstOrDefaultAsync(e => e.Id == model.Id.Value); Employee? existingEmployee = await _context.Employees.FirstOrDefaultAsync(e => e.Id == model.Id.Value);
if (existingEmployee == null) if (existingEmployee == null)
{ {
_logger.LogError("User tries to update employee {EmployeeId} but not found in database", model.Id); _logger.LogWarning("User tries to update employee {EmployeeId} but not found in database", model.Id);
return NotFound(ApiResponse<object>.ErrorResponse("Employee not found", "Employee not found", 404)); return NotFound(ApiResponse<object>.ErrorResponse("Employee not found", "Employee not found", 404));
} }
byte[]? imageBytes = null; byte[]? imageBytes = null;
@ -500,7 +496,7 @@ namespace MarcoBMS.Services.Controllers
} }
else else
{ {
_logger.LogError("Employee with ID {EmploueeId} not found in database", id); _logger.LogWarning("Employee with ID {EmploueeId} not found in database", id);
} }
return Ok(ApiResponse<object>.SuccessResponse(new { }, "Employee Suspended successfully", 200)); return Ok(ApiResponse<object>.SuccessResponse(new { }, "Employee Suspended successfully", 200));
} }

View File

@ -44,10 +44,12 @@ namespace Marco.Pms.Services.Controllers
.SelectMany(v => v.Errors) .SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage) .Select(e => e.ErrorMessage)
.ToList(); .ToList();
_logger.LogError("{error}", errors); _logger.LogWarning("{error}", errors);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
} }
Guid tenantId = _userHelper.GetTenantId(); Guid tenantId = _userHelper.GetTenantId();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var batchId = Guid.NewGuid();
TicketForum ticketForum = createTicketDto.ToTicketForumFromCreateTicketDto(tenantId); TicketForum ticketForum = createTicketDto.ToTicketForumFromCreateTicketDto(tenantId);
_context.Tickets.Add(ticketForum); _context.Tickets.Add(ticketForum);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
@ -64,7 +66,7 @@ namespace Marco.Pms.Services.Controllers
var Image = attachmentDto; var Image = attachmentDto;
if (string.IsNullOrEmpty(Image.Base64Data)) if (string.IsNullOrEmpty(Image.Base64Data))
{ {
_logger.LogError("Base64 data is missing"); _logger.LogWarning("Base64 data is missing");
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
} }
@ -79,7 +81,7 @@ namespace Marco.Pms.Services.Controllers
string objectKey = $"tenant-{tenantId}/project-{createTicketDto.LinkedProjectId}/froum/{fileName}"; string objectKey = $"tenant-{tenantId}/project-{createTicketDto.LinkedProjectId}/froum/{fileName}";
await _s3Service.UploadFileAsync(base64, fileType, objectKey); await _s3Service.UploadFileAsync(base64, fileType, objectKey);
Document document = attachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, createTicketDto.CreatedAt, tenantId); Document document = attachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, createTicketDto.CreatedAt, tenantId, batchId, loggedInEmployee.Id);
_context.Documents.Add(document); _context.Documents.Add(document);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
@ -158,11 +160,19 @@ namespace Marco.Pms.Services.Controllers
.SelectMany(v => v.Errors) .SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage) .Select(e => e.ErrorMessage)
.ToList(); .ToList();
_logger.LogError("{error}", errors); _logger.LogWarning("{error}", errors);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
} }
Guid tenantId = _userHelper.GetTenantId(); Guid tenantId = _userHelper.GetTenantId();
var existingTicket = await _context.Tickets.Include(t => t.TicketTypeMaster).Include(t => t.TicketStatusMaster).Include(t => t.Priority).AsNoTracking().FirstOrDefaultAsync(t => t.Id == updateTicketDto.Id); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var batchId = Guid.NewGuid();
var existingTicket = await _context.Tickets
.Include(t => t.TicketTypeMaster)
.Include(t => t.TicketStatusMaster)
.Include(t => t.Priority)
.AsNoTracking()
.FirstOrDefaultAsync(t => t.Id == updateTicketDto.Id);
if (existingTicket != null) if (existingTicket != null)
{ {
TicketForum ticketForum = updateTicketDto.ToTicketForumFromUpdateTicketDto(existingTicket); TicketForum ticketForum = updateTicketDto.ToTicketForumFromUpdateTicketDto(existingTicket);
@ -187,7 +197,7 @@ namespace Marco.Pms.Services.Controllers
var Image = attachmentDto; var Image = attachmentDto;
if (string.IsNullOrEmpty(Image.Base64Data)) if (string.IsNullOrEmpty(Image.Base64Data))
{ {
_logger.LogError("Base64 data is missing"); _logger.LogWarning("Base64 data is missing");
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
} }
@ -202,7 +212,7 @@ namespace Marco.Pms.Services.Controllers
string objectKey = $"tenant-{tenantId}/project-{updateTicketDto.LinkedProjectId}/froum/{fileName}"; string objectKey = $"tenant-{tenantId}/project-{updateTicketDto.LinkedProjectId}/froum/{fileName}";
await _s3Service.UploadFileAsync(base64, fileType, objectKey); await _s3Service.UploadFileAsync(base64, fileType, objectKey);
Document document = attachmentDto.ToDocumentFromUpdateAttachmentDto(objectKey, objectKey, updateTicketDto.CreatedAt, tenantId); Document document = attachmentDto.ToDocumentFromUpdateAttachmentDto(objectKey, objectKey, updateTicketDto.CreatedAt, tenantId, batchId, loggedInEmployee.Id);
_context.Documents.Add(document); _context.Documents.Add(document);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
@ -326,7 +336,7 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Ticket {TicketId} updated", updateTicketDto.Id); _logger.LogInfo("Ticket {TicketId} updated", updateTicketDto.Id);
return Ok(ApiResponse<object>.SuccessResponse(ticketVM, "Ticket Updated Successfully", 200)); return Ok(ApiResponse<object>.SuccessResponse(ticketVM, "Ticket Updated Successfully", 200));
} }
_logger.LogError("Ticket {TicketId} not Found in database", updateTicketDto.Id); _logger.LogWarning("Ticket {TicketId} not Found in database", updateTicketDto.Id);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket not Found", "Ticket not Found", 404)); return NotFound(ApiResponse<object>.ErrorResponse("Ticket not Found", "Ticket not Found", 404));
} }
@ -339,11 +349,14 @@ namespace Marco.Pms.Services.Controllers
.SelectMany(v => v.Errors) .SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage) .Select(e => e.ErrorMessage)
.ToList(); .ToList();
_logger.LogError("{error}", errors); _logger.LogWarning("{error}", errors);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
} }
Guid tenantId = _userHelper.GetTenantId(); Guid tenantId = _userHelper.GetTenantId();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var batchId = Guid.NewGuid();
List<TicketAttachment> attachments = new List<TicketAttachment>(); List<TicketAttachment> attachments = new List<TicketAttachment>();
List<Document> documents = new List<Document>(); List<Document> documents = new List<Document>();
@ -351,7 +364,7 @@ namespace Marco.Pms.Services.Controllers
if (ticket == null) if (ticket == null)
{ {
_logger.LogError("Ticket {TicketId} not Found in database", addCommentDto.TicketId); _logger.LogWarning("Ticket {TicketId} not Found in database", addCommentDto.TicketId);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket not Found", "Ticket not Found", 404)); return NotFound(ApiResponse<object>.ErrorResponse("Ticket not Found", "Ticket not Found", 404));
} }
@ -366,7 +379,7 @@ namespace Marco.Pms.Services.Controllers
var Image = attachmentDto; var Image = attachmentDto;
if (string.IsNullOrEmpty(Image.Base64Data)) if (string.IsNullOrEmpty(Image.Base64Data))
{ {
_logger.LogError("Base64 data is missing"); _logger.LogWarning("Base64 data is missing");
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
} }
@ -381,7 +394,7 @@ namespace Marco.Pms.Services.Controllers
string objectKey = $"tenant-{tenantId}/project-{ticket.LinkedProjectId}/froum/{fileName}"; string objectKey = $"tenant-{tenantId}/project-{ticket.LinkedProjectId}/froum/{fileName}";
await _s3Service.UploadFileAsync(base64, fileType, objectKey); await _s3Service.UploadFileAsync(base64, fileType, objectKey);
Document document = attachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, addCommentDto.SentAt, tenantId); Document document = attachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, addCommentDto.SentAt, tenantId, batchId, loggedInEmployee.Id);
_context.Documents.Add(document); _context.Documents.Add(document);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
@ -424,18 +437,21 @@ namespace Marco.Pms.Services.Controllers
.SelectMany(v => v.Errors) .SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage) .Select(e => e.ErrorMessage)
.ToList(); .ToList();
_logger.LogError("{error}", errors); _logger.LogWarning("{error}", errors);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
} }
Guid tenantId = _userHelper.GetTenantId(); Guid tenantId = _userHelper.GetTenantId();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var batchId = Guid.NewGuid();
List<TicketAttachment> attachments = new List<TicketAttachment>(); List<TicketAttachment> attachments = new List<TicketAttachment>();
TicketForum? ticket = await _context.Tickets.FirstOrDefaultAsync(t => t.Id == updateCommentDto.TicketId); TicketForum? ticket = await _context.Tickets.FirstOrDefaultAsync(t => t.Id == updateCommentDto.TicketId);
if (ticket == null) if (ticket == null)
{ {
_logger.LogError("Ticket {TicketId} not Found in database", updateCommentDto.TicketId); _logger.LogWarning("Ticket {TicketId} not Found in database", updateCommentDto.TicketId);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket not Found", "Ticket not Found", 404)); return NotFound(ApiResponse<object>.ErrorResponse("Ticket not Found", "Ticket not Found", 404));
} }
@ -458,7 +474,7 @@ namespace Marco.Pms.Services.Controllers
var Image = attachmentDto; var Image = attachmentDto;
if (string.IsNullOrEmpty(Image.Base64Data)) if (string.IsNullOrEmpty(Image.Base64Data))
{ {
_logger.LogError("Base64 data is missing"); _logger.LogWarning("Base64 data is missing");
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
} }
@ -473,7 +489,7 @@ namespace Marco.Pms.Services.Controllers
string objectKey = $"tenant-{tenantId}/project-{ticket.LinkedProjectId}/froum/{fileName}"; string objectKey = $"tenant-{tenantId}/project-{ticket.LinkedProjectId}/froum/{fileName}";
await _s3Service.UploadFileAsync(base64, fileType, objectKey); await _s3Service.UploadFileAsync(base64, fileType, objectKey);
Document document = attachmentDto.ToDocumentFromUpdateAttachmentDto(objectKey, objectKey, existingComment.SentAt, tenantId); Document document = attachmentDto.ToDocumentFromUpdateAttachmentDto(objectKey, objectKey, existingComment.SentAt, tenantId, batchId, loggedInEmployee.Id);
_context.Documents.Add(document); _context.Documents.Add(document);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
@ -536,11 +552,14 @@ namespace Marco.Pms.Services.Controllers
.SelectMany(v => v.Errors) .SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage) .Select(e => e.ErrorMessage)
.ToList(); .ToList();
_logger.LogError("{error}", errors); _logger.LogWarning("{error}", errors);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
} }
Guid tenantId = _userHelper.GetTenantId(); Guid tenantId = _userHelper.GetTenantId();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var batchId = Guid.NewGuid();
List<TicketAttachmentVM> ticketAttachmentVMs = new List<TicketAttachmentVM>(); List<TicketAttachmentVM> ticketAttachmentVMs = new List<TicketAttachmentVM>();
List<Guid> ticketIds = forumAttachmentDtos.Select(f => f.TicketId.HasValue ? f.TicketId.Value : Guid.Empty).ToList(); List<Guid> ticketIds = forumAttachmentDtos.Select(f => f.TicketId.HasValue ? f.TicketId.Value : Guid.Empty).ToList();
@ -549,7 +568,7 @@ namespace Marco.Pms.Services.Controllers
if (tickets == null || tickets.Count > 0) if (tickets == null || tickets.Count > 0)
{ {
_logger.LogError("Tickets not Found in database"); _logger.LogWarning("Tickets not Found in database");
return NotFound(ApiResponse<object>.ErrorResponse("Ticket not Found", "Ticket not Found", 404)); return NotFound(ApiResponse<object>.ErrorResponse("Ticket not Found", "Ticket not Found", 404));
} }
@ -559,12 +578,12 @@ namespace Marco.Pms.Services.Controllers
{ {
if (string.IsNullOrEmpty(forumAttachmentDto.Base64Data)) if (string.IsNullOrEmpty(forumAttachmentDto.Base64Data))
{ {
_logger.LogError("Base64 data is missing"); _logger.LogWarning("Base64 data is missing");
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
} }
if (forumAttachmentDto.TicketId == null) if (forumAttachmentDto.TicketId == null)
{ {
_logger.LogError("ticket ID is missing"); _logger.LogWarning("ticket ID is missing");
return BadRequest(ApiResponse<object>.ErrorResponse("ticket ID is missing", "ticket ID is missing", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("ticket ID is missing", "ticket ID is missing", 400));
} }
var ticket = tickets.FirstOrDefault(t => t.Id == forumAttachmentDto.TicketId); var ticket = tickets.FirstOrDefault(t => t.Id == forumAttachmentDto.TicketId);
@ -579,7 +598,7 @@ namespace Marco.Pms.Services.Controllers
string objectKey = $"tenant-{tenantId}/project-{ticket?.LinkedProjectId}/froum/{fileName}"; string objectKey = $"tenant-{tenantId}/project-{ticket?.LinkedProjectId}/froum/{fileName}";
await _s3Service.UploadFileAsync(base64, fileType, objectKey); await _s3Service.UploadFileAsync(base64, fileType, objectKey);
Document document = forumAttachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, forumAttachmentDto.SentAt, tenantId); Document document = forumAttachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, forumAttachmentDto.SentAt, tenantId, batchId, loggedInEmployee.Id);
_context.Documents.Add(document); _context.Documents.Add(document);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();

View File

@ -0,0 +1,404 @@
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Activities;
using Marco.Pms.Model.Dtos.DocumentManager;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Mapper;
using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Services.Service;
using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
namespace Marco.Pms.Services.Controllers
{
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class ImageController : ControllerBase
{
private readonly ApplicationDbContext _context;
private readonly S3UploadService _s3Service;
private readonly UserHelper _userHelper;
private readonly ILoggingService _logger;
private readonly PermissionServices _permission;
private readonly Guid tenantId;
public ImageController(ApplicationDbContext context, S3UploadService s3Service, UserHelper userHelper, ILoggingService logger, PermissionServices permission)
{
_context = context;
_s3Service = s3Service;
_userHelper = userHelper;
_logger = logger;
tenantId = userHelper.GetTenantId();
_permission = permission;
}
[HttpGet("images/{projectId}")]
public async Task<IActionResult> GetImageList(Guid projectId, [FromQuery] string? filter, [FromQuery] int? pageNumber = 1, [FromQuery] int? pageSize = 10)
{
_logger.LogInfo("[GetImageList] Called by Employee for ProjectId: {ProjectId}", projectId);
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Step 1: Validate project existence
var isProjectExist = await _context.Projects.AnyAsync(p => p.Id == projectId && p.TenantId == tenantId);
if (!isProjectExist)
{
_logger.LogWarning("[GetImageList] ProjectId: {ProjectId} not found", projectId);
return BadRequest(ApiResponse<object>.ErrorResponse("Project not found", "Project not found in database", 400));
}
// Step 2: Check project access permission
var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId);
if (!hasPermission)
{
_logger.LogWarning("[GetImageList] Access denied for EmployeeId: {EmployeeId} on ProjectId: {ProjectId}", loggedInEmployee.Id, projectId);
return StatusCode(403, ApiResponse<object>.ErrorResponse("You don't have access", "You don't have access", 403));
}
// Step 3: Deserialize filter
ImageFilter? imageFilter = null;
if (!string.IsNullOrWhiteSpace(filter))
{
try
{
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
//string unescapedJsonString = JsonSerializer.Deserialize<string>(filter, options) ?? "";
//imageFilter = JsonSerializer.Deserialize<ImageFilter>(unescapedJsonString, options);
imageFilter = JsonSerializer.Deserialize<ImageFilter>(filter, options);
}
catch (Exception ex)
{
_logger.LogWarning("[GetImageList] Failed to parse filter: {Message}", ex.Message);
}
}
// Step 4: Extract filter values
var buildingIds = imageFilter?.BuildingIds;
var floorIds = imageFilter?.FloorIds;
var workAreaIds = imageFilter?.WorkAreaIds;
var activityIds = imageFilter?.ActivityIds;
var workCategoryIds = imageFilter?.WorkCategoryIds;
var startDate = imageFilter?.StartDate;
var endDate = imageFilter?.EndDate;
var uploadedByIds = imageFilter?.UploadedByIds;
// Step 5: Fetch building > floor > area > work item hierarchy
List<Building>? buildings = null;
List<Floor>? floors = null;
List<WorkArea>? workAreas = null;
if (buildingIds != null && buildingIds.Count > 0)
{
buildings = await _context.Buildings
.Where(b => b.ProjectId == projectId && buildingIds.Contains(b.Id))
.ToListAsync();
}
else
{
buildings = await _context.Buildings
.Where(b => b.ProjectId == projectId)
.ToListAsync();
buildingIds = buildings.Select(b => b.Id).ToList();
}
if (floorIds != null && floorIds.Count > 0)
{
floors = await _context.Floor
.Where(f => buildingIds.Contains(f.BuildingId) && floorIds.Contains(f.Id))
.ToListAsync();
}
else
{
floors = await _context.Floor
.Where(f => buildingIds.Contains(f.BuildingId))
.ToListAsync();
floorIds = floors.Select(f => f.Id).ToList();
}
if (workAreaIds != null && workAreaIds.Count > 0)
{
workAreas = await _context.WorkAreas
.Where(wa => floorIds.Contains(wa.FloorId) && workAreaIds.Contains(wa.Id))
.ToListAsync();
}
else
{
workAreas = await _context.WorkAreas
.Where(wa => floorIds.Contains(wa.FloorId))
.ToListAsync();
workAreaIds = workAreas.Select(wa => wa.Id).ToList();
}
var workItemsQuery = _context.WorkItems.Include(w => w.ActivityMaster).Include(w => w.WorkCategoryMaster)
.Where(wi => workAreaIds.Contains(wi.WorkAreaId));
if (activityIds?.Any() == true) workItemsQuery = workItemsQuery.Where(wi => activityIds.Contains(wi.ActivityId));
if (workCategoryIds?.Any() == true)
{
workItemsQuery = workItemsQuery.Where(wi => wi.WorkCategoryMaster != null && workCategoryIds.Contains(wi.WorkCategoryMaster.Id));
}
var workItems = await workItemsQuery.ToListAsync();
var workItemIds = workItems.Select(wi => wi.Id).ToList();
// Step 6: Fetch task allocations and comments
var tasks = await _context.TaskAllocations.Include(t => t.ReportedBy)
.Where(t => workItemIds.Contains(t.WorkItemId)).ToListAsync();
var taskIds = tasks.Select(t => t.Id).ToList();
var comments = await _context.TaskComments.Include(c => c.Employee)
.Where(c => taskIds.Contains(c.TaskAllocationId)).ToListAsync();
var commentIds = comments.Select(c => c.Id).ToList();
var attachments = await _context.TaskAttachments
.Where(ta => taskIds.Contains(ta.ReferenceId) || commentIds.Contains(ta.ReferenceId)).ToListAsync();
var documentIds = attachments.Select(ta => ta.DocumentId).ToList();
// Step 7: Fetch and filter documents
List<DocumentBatchDto> documents = new List<DocumentBatchDto>();
var docQuery = _context.Documents.Include(d => d.UploadedBy)
.Where(d => documentIds.Contains(d.Id) && d.TenantId == tenantId);
if (startDate != null && endDate != null)
{
docQuery = docQuery.Where(d => d.UploadedAt.Date >= startDate.Value.Date && d.UploadedAt.Date <= endDate.Value.Date);
}
if (pageNumber != null && pageSize != null)
{
documents = await docQuery
.GroupBy(d => d.BatchId)
.OrderByDescending(g => g.Max(d => d.UploadedAt))
.Skip((pageNumber.Value - 1) * pageSize.Value)
.Take(pageSize.Value)
.Select(g => new DocumentBatchDto
{
BatchId = g.Key,
Documents = g.ToList()
})
.ToListAsync();
Console.Write("Pagenation Success");
}
// Step 8: Build response
var documentVM = documents.Select(d =>
{
var docIds = d.Documents?.Select(x => x.Id).ToList() ?? new List<Guid>();
var refId = attachments.FirstOrDefault(ta => docIds.Contains(ta.DocumentId))?.ReferenceId;
var task = tasks.FirstOrDefault(t => t.Id == refId);
var comment = comments.FirstOrDefault(c => c.Id == refId);
var source = task != null ? "Report" : comment != null ? "Comment" : "";
var uploadedBy = task?.ReportedBy ?? comment?.Employee;
if (comment != null)
{
task = tasks.FirstOrDefault(t => t.Id == comment.TaskAllocationId);
}
if (task != null)
{
comment = comments.OrderBy(c => c.CommentDate).FirstOrDefault(c => c.TaskAllocationId == task.Id);
}
var workItem = workItems.FirstOrDefault(w => w.Id == task?.WorkItemId);
var workArea = workAreas.FirstOrDefault(wa => wa.Id == workItem?.WorkAreaId);
var floor = floors.FirstOrDefault(f => f.Id == workArea?.FloorId);
var building = buildings.FirstOrDefault(b => b.Id == floor?.BuildingId);
return new
{
BatchId = d.BatchId,
Documents = d.Documents?.Select(x => new
{
Id = x.Id,
thumbnailUrl = x.ThumbS3Key != null ? _s3Service.GeneratePreSignedUrlAsync(x.ThumbS3Key) : (x.S3Key != null ? _s3Service.GeneratePreSignedUrlAsync(x.S3Key) : null),
Url = x.S3Key != null ? _s3Service.GeneratePreSignedUrlAsync(x.S3Key) : null,
UploadedBy = x.UploadedBy?.ToBasicEmployeeVMFromEmployee() ?? uploadedBy?.ToBasicEmployeeVMFromEmployee(),
UploadedAt = x.UploadedAt,
}).ToList(),
Source = source,
ProjectId = projectId,
BuildingId = building?.Id,
BuildingName = building?.Name,
FloorIds = floor?.Id,
FloorName = floor?.FloorName,
WorkAreaId = workArea?.Id,
WorkAreaName = workArea?.AreaName,
TaskId = task?.Id,
ActivityId = workItem?.ActivityMaster?.Id,
ActivityName = workItem?.ActivityMaster?.ActivityName,
WorkCategoryId = workItem?.WorkCategoryMaster?.Id,
WorkCategoryName = workItem?.WorkCategoryMaster?.Name,
CommentId = comment?.Id,
Comment = comment?.Comment
};
}).ToList();
if (uploadedByIds?.Any() == true)
{
documentVM = documentVM.Where(d => d.Documents != null && d.Documents.Any(x => uploadedByIds.Contains(x.UploadedBy?.Id ?? Guid.Empty))).ToList();
}
_logger.LogInfo("[GetImageList] Fetched {Count} documents for ProjectId: {ProjectId}", documentVM.Count, projectId);
return Ok(ApiResponse<object>.SuccessResponse(documentVM, $"{documentVM.Count} image records fetched successfully", 200));
}
[HttpGet("batch/{batchId}")]
public async Task<IActionResult> GetImagesByBatch(Guid batchId)
{
_logger.LogInfo("GetImagesByBatch called for BatchId: {BatchId}", batchId);
// Step 1: Get the logged-in employee
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Step 2: Retrieve all documents in the batch
var documents = await _context.Documents
.Include(d => d.UploadedBy)
.Where(d => d.BatchId == batchId)
.ToListAsync();
if (!documents.Any())
{
_logger.LogWarning("No documents found for BatchId: {BatchId}", batchId);
return NotFound(ApiResponse<object>.ErrorResponse("No images found", "No images associated with this batch", 404));
}
var documentIds = documents.Select(d => d.Id).ToList();
// Step 3: Get task/comment reference IDs linked to these documents
var referenceIds = await _context.TaskAttachments
.Where(ta => documentIds.Contains(ta.DocumentId))
.Select(ta => ta.ReferenceId)
.Distinct()
.ToListAsync();
// Step 4: Try to identify the source of the attachment (task or comment)
var task = await _context.TaskAllocations
.Include(t => t.ReportedBy)
.FirstOrDefaultAsync(t => referenceIds.Contains(t.Id));
TaskComment? comment = null;
WorkItem? workItem = null;
Employee? uploadedBy = null;
string source = "";
if (task != null)
{
uploadedBy = task.ReportedBy;
workItem = await _context.WorkItems
.Include(wi => wi.ActivityMaster)
.Include(wi => wi.WorkCategoryMaster)
.FirstOrDefaultAsync(wi => wi.Id == task.WorkItemId);
source = "Report";
}
else
{
comment = await _context.TaskComments
.Include(tc => tc.TaskAllocation)
.Include(tc => tc.Employee)
.FirstOrDefaultAsync(tc => referenceIds.Contains(tc.Id));
var workItemId = comment?.TaskAllocation?.WorkItemId;
uploadedBy = comment?.Employee;
workItem = await _context.WorkItems
.Include(wi => wi.ActivityMaster)
.Include(wi => wi.WorkCategoryMaster)
.FirstOrDefaultAsync(wi => wi.Id == workItemId);
source = "Comment";
}
// Step 5: Traverse up to building level
var workAreaId = workItem?.WorkAreaId;
var workArea = await _context.WorkAreas
.Include(wa => wa.Floor)
.FirstOrDefaultAsync(wa => wa.Id == workAreaId);
var buildingId = workArea?.Floor?.BuildingId;
var building = await _context.Buildings
.FirstOrDefaultAsync(b => b.Id == buildingId);
// Step 6: Construct the response
var response = new
{
BatchId = batchId,
Documents = documents?.Select(x => new
{
Id = x.Id,
thumbnailUrl = x.ThumbS3Key != null ? _s3Service.GeneratePreSignedUrlAsync(x.ThumbS3Key) : (x.S3Key != null ? _s3Service.GeneratePreSignedUrlAsync(x.S3Key) : null),
Url = x.S3Key != null ? _s3Service.GeneratePreSignedUrlAsync(x.S3Key) : null,
UploadedBy = x.UploadedBy?.ToBasicEmployeeVMFromEmployee() ?? uploadedBy?.ToBasicEmployeeVMFromEmployee(),
UploadedAt = x.UploadedAt,
}).ToList(),
Source = source,
ProjectId = building?.ProjectId,
BuildingId = building?.Id,
BuildingName = building?.Name,
FloorIds = workArea?.Floor?.Id,
FloorName = workArea?.Floor?.FloorName,
WorkAreaId = workArea?.Id,
WorkAreaName = workArea?.AreaName,
TaskId = task?.Id,
ActivityId = workItem?.ActivityMaster?.Id,
ActivityName = workItem?.ActivityMaster?.ActivityName,
WorkCategoryId = workItem?.WorkCategoryMaster?.Id,
WorkCategoryName = workItem?.WorkCategoryMaster?.Name,
CommentId = comment?.Id,
Comment = comment?.Comment
};
_logger.LogInfo("Fetched {Count} image(s) for BatchId: {BatchId}", response.Documents?.Count ?? 0, batchId);
return Ok(ApiResponse<object>.SuccessResponse(response, "Images for provided batchId fetched successfully", 200));
}
[HttpGet("{documentId}")]
public async Task<IActionResult> GetImage(Guid documentId)
{
// Log the start of the image fetch process
_logger.LogInfo("GetImage called for DocumentId: {DocumentId}", documentId);
// Step 1: Get the currently logged-in employee (for future use like permission checks or auditing)
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Step 2: Fetch the document from the database based on the provided ID
var document = await _context.Documents.FirstOrDefaultAsync(d => d.Id == documentId);
// Step 3: If document doesn't exist, return a 400 Bad Request response
if (document == null)
{
_logger.LogWarning("Document not found for DocumentId: {DocumentId}", documentId);
return BadRequest(ApiResponse<object>.ErrorResponse("Document not found", "Document not found", 400));
}
// Step 4: Generate pre-signed URLs for thumbnail and full image (if keys exist)
string? thumbnailUrl = document.ThumbS3Key != null
? _s3Service.GeneratePreSignedUrlAsync(document.ThumbS3Key)
: null;
string? imageUrl = document.S3Key != null
? _s3Service.GeneratePreSignedUrlAsync(document.S3Key)
: null;
// Step 5: Prepare the response object
var response = new
{
ThumbnailUrl = thumbnailUrl,
ImageUrl = imageUrl
};
// Step 6: Log successful fetch and return the result
_logger.LogInfo("Image fetched successfully for DocumentId: {DocumentId}", documentId);
return Ok(ApiResponse<object>.SuccessResponse(response, "Image fetched successfully", 200));
}
}
}

View File

@ -169,7 +169,7 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Ticket Status master {TicketStatusId} added successfully from tenant {tenantId}", statusMaster.Id, tenantId); _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)); return Ok(ApiResponse<object>.SuccessResponse(statusVM, "Ticket Status master added successfully", 200));
} }
_logger.LogError("User sent empyt payload"); _logger.LogWarning("User sent empyt payload");
return BadRequest(ApiResponse<object>.ErrorResponse("Sent Empty payload", "Sent Empty payload", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Sent Empty payload", "Sent Empty payload", 400));
} }
@ -190,10 +190,10 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Ticket Status master {TicketStatusId} updated successfully from tenant {tenantId}", statusMaster.Id, tenantId); _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)); return Ok(ApiResponse<object>.SuccessResponse(statusVM, "Ticket Status master updated successfully", 200));
} }
_logger.LogError("Ticket Status master {TicketStatusId} not found in database", statusMasterDto.Id != null ? statusMasterDto.Id.Value : Guid.Empty); _logger.LogWarning("Ticket Status master {TicketStatusId} not found in database", statusMasterDto.Id != null ? statusMasterDto.Id.Value : Guid.Empty);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket Status master not found", "Ticket Status master not found", 404)); return NotFound(ApiResponse<object>.ErrorResponse("Ticket Status master not found", "Ticket Status master not found", 404));
} }
_logger.LogError("User sent empyt payload"); _logger.LogWarning("User sent empyt payload");
return BadRequest(ApiResponse<object>.ErrorResponse("Sent Empty payload", "Sent Empty payload", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Sent Empty payload", "Sent Empty payload", 400));
} }
@ -220,7 +220,7 @@ namespace Marco.Pms.Services.Controllers
} }
else else
{ {
_logger.LogError("Ticket Status {TickeStatusId} not found in database", id); _logger.LogWarning("Ticket Status {TickeStatusId} not found in database", id);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket Status not found", "Ticket Status not found", 404)); return NotFound(ApiResponse<object>.ErrorResponse("Ticket Status not found", "Ticket Status not found", 404));
} }
} }
@ -257,7 +257,7 @@ namespace Marco.Pms.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(typeVM, "Ticket type master added successfully", 200)); return Ok(ApiResponse<object>.SuccessResponse(typeVM, "Ticket type master added successfully", 200));
} }
_logger.LogError("User sent empyt payload"); _logger.LogWarning("User sent empyt payload");
return BadRequest(ApiResponse<object>.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400));
} }
@ -278,10 +278,10 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Ticket Type master {TicketTypeId} updated successfully from tenant {tenantId}", typeMaster.Id, tenantId); _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)); return Ok(ApiResponse<object>.SuccessResponse(typeVM, "Ticket type master updated successfully", 200));
} }
_logger.LogError("Ticket type master {TicketTypeId} not found in database", typeMasterDto.Id != null ? typeMasterDto.Id.Value : Guid.Empty); _logger.LogWarning("Ticket type master {TicketTypeId} not found in database", typeMasterDto.Id != null ? typeMasterDto.Id.Value : Guid.Empty);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket type master not found", "Ticket type master not found", 404)); return NotFound(ApiResponse<object>.ErrorResponse("Ticket type master not found", "Ticket type master not found", 404));
} }
_logger.LogError("User sent empyt payload"); _logger.LogWarning("User sent empyt payload");
return BadRequest(ApiResponse<object>.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400));
} }
@ -308,7 +308,7 @@ namespace Marco.Pms.Services.Controllers
} }
else else
{ {
_logger.LogError("Ticket Type {TickeTypeId} not found in database", id); _logger.LogWarning("Ticket Type {TickeTypeId} not found in database", id);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket Type not found", "Ticket Type not found", 404)); return NotFound(ApiResponse<object>.ErrorResponse("Ticket Type not found", "Ticket Type not found", 404));
} }
} }
@ -346,7 +346,7 @@ namespace Marco.Pms.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(typeVM, "Ticket Priority master added successfully", 200)); return Ok(ApiResponse<object>.SuccessResponse(typeVM, "Ticket Priority master added successfully", 200));
} }
_logger.LogError("User sent empyt payload"); _logger.LogWarning("User sent empyt payload");
return BadRequest(ApiResponse<object>.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400));
} }
[HttpPost("ticket-priorities/edit/{id}")] [HttpPost("ticket-priorities/edit/{id}")]
@ -366,10 +366,10 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Ticket Priority master {TicketPriorityId} updated successfully from tenant {tenantId}", typeMaster.Id, tenantId); _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)); return Ok(ApiResponse<object>.SuccessResponse(typeVM, "Ticket Priority master updated successfully", 200));
} }
_logger.LogError("Ticket Priority master {TicketPriorityId} not found in database", priorityMasterDto.Id != null ? priorityMasterDto.Id.Value : Guid.Empty); _logger.LogWarning("Ticket Priority master {TicketPriorityId} not found in database", priorityMasterDto.Id != null ? priorityMasterDto.Id.Value : Guid.Empty);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket Priority master not found", "Ticket Priority master not found", 404)); return NotFound(ApiResponse<object>.ErrorResponse("Ticket Priority master not found", "Ticket Priority master not found", 404));
} }
_logger.LogError("User sent empyt payload"); _logger.LogWarning("User sent empyt payload");
return BadRequest(ApiResponse<object>.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400));
} }
@ -396,7 +396,7 @@ namespace Marco.Pms.Services.Controllers
} }
else else
{ {
_logger.LogError("Ticket Priority {TickePriorityId} not found in database", id); _logger.LogWarning("Ticket Priority {TickePriorityId} not found in database", id);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket Priority not found", "Ticket Priority not found", 404)); return NotFound(ApiResponse<object>.ErrorResponse("Ticket Priority not found", "Ticket Priority not found", 404));
} }
} }
@ -433,7 +433,7 @@ namespace Marco.Pms.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(typeVM, "Ticket tag master added successfully", 200)); return Ok(ApiResponse<object>.SuccessResponse(typeVM, "Ticket tag master added successfully", 200));
} }
_logger.LogError("User sent empyt payload"); _logger.LogWarning("User sent empyt payload");
return BadRequest(ApiResponse<object>.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400));
} }
@ -454,10 +454,10 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Ticket Tag master {TicketTypeId} updated successfully from tenant {tenantId}", tagMaster.Id, tenantId); _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)); return Ok(ApiResponse<object>.SuccessResponse(typeVM, "Ticket tag master updated successfully", 200));
} }
_logger.LogError("Ticket tag master {TicketTypeId} not found in database", tagMasterDto.Id != null ? tagMasterDto.Id.Value : Guid.Empty); _logger.LogWarning("Ticket tag master {TicketTypeId} not found in database", tagMasterDto.Id != null ? tagMasterDto.Id.Value : Guid.Empty);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket tag master not found", "Ticket tag master not found", 404)); return NotFound(ApiResponse<object>.ErrorResponse("Ticket tag master not found", "Ticket tag master not found", 404));
} }
_logger.LogError("User sent empyt payload"); _logger.LogWarning("User sent empyt payload");
return BadRequest(ApiResponse<object>.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400));
} }
@ -484,7 +484,7 @@ namespace Marco.Pms.Services.Controllers
} }
else else
{ {
_logger.LogError("Ticket Tag {TickeTagId} not found in database", id); _logger.LogWarning("Ticket Tag {TickeTagId} not found in database", id);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket tag not found", "Ticket tag not found", 404)); return NotFound(ApiResponse<object>.ErrorResponse("Ticket tag not found", "Ticket tag not found", 404));
} }
} }
@ -548,7 +548,7 @@ namespace Marco.Pms.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(workCategoryMasterVM, "Work category master added successfully", 200)); return Ok(ApiResponse<object>.SuccessResponse(workCategoryMasterVM, "Work category master added successfully", 200));
} }
_logger.LogError("User sent empyt payload"); _logger.LogWarning("User sent empyt payload");
return BadRequest(ApiResponse<object>.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400));
} }
@ -563,7 +563,7 @@ namespace Marco.Pms.Services.Controllers
{ {
if (workCategory.IsSystem) if (workCategory.IsSystem)
{ {
_logger.LogError("User tries to update system-defined work category"); _logger.LogWarning("User tries to update system-defined work category");
return BadRequest(ApiResponse<object>.ErrorResponse("Cannot update system-defined work", "Cannot update system-defined work", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Cannot update system-defined work", "Cannot update system-defined work", 400));
} }
workCategory = workCategoryMasterDto.ToWorkCategoryMasterFromWorkCategoryMasterDto(tenantId); workCategory = workCategoryMasterDto.ToWorkCategoryMasterFromWorkCategoryMasterDto(tenantId);
@ -574,10 +574,10 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo("Work category master {WorkCategoryId} updated successfully from tenant {tenantId}", workCategory.Id, tenantId); _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)); return Ok(ApiResponse<object>.SuccessResponse(workCategoryMasterVM, "Work category master updated successfully", 200));
} }
_logger.LogError("Work category master {WorkCategoryId} not found in database", workCategoryMasterDto.Id ?? Guid.Empty); _logger.LogWarning("Work category master {WorkCategoryId} not found in database", workCategoryMasterDto.Id ?? Guid.Empty);
return NotFound(ApiResponse<object>.ErrorResponse("Work category master not found", "Work category master not found", 404)); return NotFound(ApiResponse<object>.ErrorResponse("Work category master not found", "Work category master not found", 404));
} }
_logger.LogError("User sent empyt payload"); _logger.LogWarning("User sent empyt payload");
return BadRequest(ApiResponse<object>.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400));
} }
@ -605,7 +605,7 @@ namespace Marco.Pms.Services.Controllers
} }
else else
{ {
_logger.LogError("Work category {WorkCategoryId} not found in database", id); _logger.LogWarning("Work category {WorkCategoryId} not found in database", id);
return NotFound(ApiResponse<object>.ErrorResponse("Work category not found", "Work category not found", 404)); return NotFound(ApiResponse<object>.ErrorResponse("Work category not found", "Work category not found", 404));
} }
} }
@ -628,7 +628,7 @@ namespace Marco.Pms.Services.Controllers
.SelectMany(v => v.Errors) .SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage) .Select(e => e.ErrorMessage)
.ToList(); .ToList();
_logger.LogError("User sent Invalid Date while marking attendance"); _logger.LogWarning("User sent Invalid Date while marking attendance");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
} }
var response = await _masterHelper.CreateWorkStatus(createWorkStatusDto); var response = await _masterHelper.CreateWorkStatus(createWorkStatusDto);
@ -742,7 +742,7 @@ namespace Marco.Pms.Services.Controllers
.SelectMany(v => v.Errors) .SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage) .Select(e => e.ErrorMessage)
.ToList(); .ToList();
_logger.LogError("User sent Invalid Date while marking attendance"); _logger.LogWarning("User sent Invalid Date while marking attendance");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
} }
var response = await _masterHelper.CreateContactTag(contactTagDto); var response = await _masterHelper.CreateContactTag(contactTagDto);

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,19 @@
using System.Data; using Marco.Pms.DataAccess.Data;
using System.Globalization;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Dtos.Attendance;
using Marco.Pms.Model.Dtos.Mail; using Marco.Pms.Model.Dtos.Mail;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Mail; using Marco.Pms.Model.Mail;
using Marco.Pms.Model.MongoDBModels;
using Marco.Pms.Model.Utilities; using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Report; using Marco.Pms.Services.Helpers;
using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service; using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using MongoDB.Driver; using MongoDB.Driver;
using System.Data;
using System.Globalization;
using System.Net.Mail;
namespace Marco.Pms.Services.Controllers namespace Marco.Pms.Services.Controllers
{ {
@ -26,34 +27,135 @@ namespace Marco.Pms.Services.Controllers
private readonly ILoggingService _logger; private readonly ILoggingService _logger;
private readonly UserHelper _userHelper; private readonly UserHelper _userHelper;
private readonly IWebHostEnvironment _env; private readonly IWebHostEnvironment _env;
public ReportController(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, UserHelper userHelper, 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)
{ {
_context = context; _context = context;
_emailSender = emailSender; _emailSender = emailSender;
_logger = logger; _logger = logger;
_userHelper = userHelper; _userHelper = userHelper;
_env = env; _env = env;
_reportHelper = reportHelper;
_configuration = configuration;
_cache = cache;
_serviceScopeFactory = serviceScopeFactory;
} }
[HttpPost("set-mail")] /// <summary>
/// Adds new mail details for a project report.
/// </summary>
/// <param name="mailDetailsDto">The mail details data.</param>
/// <returns>An API response indicating success or failure.</returns>
[HttpPost("mail-details")] // More specific route for adding mail details
public async Task<IActionResult> AddMailDetails([FromBody] MailDetailsDto mailDetailsDto) public async Task<IActionResult> AddMailDetails([FromBody] MailDetailsDto mailDetailsDto)
{ {
// 1. Get Tenant ID and Basic Authorization Check
Guid tenantId = _userHelper.GetTenantId(); Guid tenantId = _userHelper.GetTenantId();
MailDetails mailDetails = new MailDetails if (tenantId == Guid.Empty)
{
_logger.LogWarning("Authorization Error: Attempt to add mail details with an empty or invalid tenant ID.");
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized", "Tenant ID not found or invalid.", 401));
}
// 2. Input Validation (Leverage Model Validation attributes on DTO)
if (mailDetailsDto == null)
{
_logger.LogWarning("Validation Error: MailDetails DTO is null. TenantId: {TenantId}", tenantId);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Data", "Request body is empty.", 400));
}
// Ensure ProjectId and Recipient are not empty
if (mailDetailsDto.ProjectId == Guid.Empty)
{
_logger.LogWarning("Validation Error: Project ID is empty. TenantId: {TenantId}", tenantId);
return BadRequest(ApiResponse<object>.ErrorResponse("Validation Failed", "Project ID cannot be empty.", 400));
}
if (string.IsNullOrWhiteSpace(mailDetailsDto.Recipient))
{
_logger.LogWarning("Validation Error: Recipient email is empty. ProjectId: {ProjectId}, TenantId: {TenantId}", mailDetailsDto.ProjectId, tenantId);
return BadRequest(ApiResponse<object>.ErrorResponse("Validation Failed", "Recipient email cannot be empty.", 400));
}
// Optional: Validate email format using regex or System.Net.Mail.MailAddress
try
{
var mailAddress = new MailAddress(mailDetailsDto.Recipient);
_logger.LogInfo("nothing");
}
catch (FormatException)
{
_logger.LogWarning("Validation Error: Invalid recipient email format '{Recipient}'. ProjectId: {ProjectId}, TenantId: {TenantId}", mailDetailsDto.Recipient, mailDetailsDto.ProjectId, tenantId);
return BadRequest(ApiResponse<object>.ErrorResponse("Validation Failed", "Invalid recipient email format.", 400));
}
// 3. Validate MailListId (Foreign Key Check)
// Ensure the MailListId refers to an existing MailBody (template) within the same tenant.
if (mailDetailsDto.MailListId != Guid.Empty) // Only validate if a MailListId is provided
{
bool mailTemplateExists;
try
{
mailTemplateExists = await _context.MailingList
.AsNoTracking()
.AnyAsync(m => m.Id == mailDetailsDto.MailListId && m.TenantId == tenantId);
}
catch (Exception ex)
{
_logger.LogError(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
{ {
ProjectId = mailDetailsDto.ProjectId, ProjectId = mailDetailsDto.ProjectId,
Recipient = mailDetailsDto.Recipient, Recipient = mailDetailsDto.Recipient,
Schedule = mailDetailsDto.Schedule, Schedule = mailDetailsDto.Schedule,
MailListId = mailDetailsDto.MailListId, MailListId = mailDetailsDto.MailListId,
TenantId = tenantId TenantId = tenantId,
}; };
_context.MailDetails.Add(mailDetails);
await _context.SaveChangesAsync(); try
return Ok("Success"); {
_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));
}
} }
[HttpPost("mail-template")] [HttpPost("mail-template1")]
public async Task<IActionResult> AddMailTemplate([FromBody] MailTemeplateDto mailTemeplateDto) public async Task<IActionResult> AddMailTemplate1([FromBody] MailTemeplateDto mailTemeplateDto)
{ {
Guid tenantId = _userHelper.GetTenantId(); Guid tenantId = _userHelper.GetTenantId();
if (string.IsNullOrWhiteSpace(mailTemeplateDto.Body) && string.IsNullOrWhiteSpace(mailTemeplateDto.Title)) if (string.IsNullOrWhiteSpace(mailTemeplateDto.Body) && string.IsNullOrWhiteSpace(mailTemeplateDto.Title))
@ -80,263 +182,298 @@ namespace Marco.Pms.Services.Controllers
return Ok("Success"); 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")] [HttpGet("project-statistics")]
public async Task<IActionResult> SendProjectReport() public async Task<IActionResult> SendProjectReport()
{ {
Guid tenantId = _userHelper.GetTenantId(); Guid tenantId = _userHelper.GetTenantId();
// Use AsNoTracking() for read-only queries to improve performance // 1. OPTIMIZATION: Perform grouping and projection on the database server.
List<MailDetails> mailDetails = await _context.MailDetails // This is far more efficient than loading all entities into memory.
var projectMailGroups = await _context.MailDetails
.AsNoTracking() .AsNoTracking()
.Include(m => m.MailBody)
.Where(m => m.TenantId == tenantId) .Where(m => m.TenantId == tenantId)
.ToListAsync();
int successCount = 0;
int notFoundCount = 0;
int invalidIdCount = 0;
var groupedMails = mailDetails
.GroupBy(m => new { m.ProjectId, m.MailListId }) .GroupBy(m => new { m.ProjectId, m.MailListId })
.Select(g => new .Select(g => new
{ {
ProjectId = g.Key.ProjectId, ProjectId = g.Key.ProjectId,
MailListId = g.Key.MailListId,
Recipients = g.Select(m => m.Recipient).Distinct().ToList(), Recipients = g.Select(m => m.Recipient).Distinct().ToList(),
MailBody = g.FirstOrDefault()?.MailBody?.Body ?? "", // Project the mail body and subject from the first record in the group
Subject = g.FirstOrDefault()?.MailBody?.Subject ?? string.Empty, MailInfo = g.Select(m => new { Body = m.MailBody != null ? m.MailBody.Body : "", Subject = m.MailBody != null ? m.MailBody.Subject : "" }).FirstOrDefault()
}) })
.ToList(); .ToListAsync();
var semaphore = new SemaphoreSlim(1); if (!projectMailGroups.Any())
// Using Task.WhenAll to send reports concurrently for better performance
var sendTasks = groupedMails.Select(async mailDetail =>
{ {
await semaphore.WaitAsync(); return Ok(ApiResponse<object>.SuccessResponse(new { }, "No projects found to send reports for.", 200));
try }
int successCount = 0;
int notFoundCount = 0;
int invalidIdCount = 0;
int failureCount = 0;
// 2. OPTIMIZATION: Use true concurrency by removing SemaphoreSlim(1)
// and giving each task its own isolated set of services (including DbContext).
var sendTasks = projectMailGroups.Select(async mailGroup =>
{
// SOLUTION: Create a new Dependency Injection scope for each parallel task.
using (var scope = _serviceScopeFactory.CreateScope())
{ {
var response = await GetProjectStatistics(mailDetail.ProjectId, mailDetail.Recipients, mailDetail.MailBody, mailDetail.Subject, tenantId); // Resolve a new instance of the helper from this isolated scope.
if (response.StatusCode == 200) // This ensures each task gets its own thread-safe DbContext.
Interlocked.Increment(ref successCount); var reportHelper = scope.ServiceProvider.GetRequiredService<ReportHelper>();
else if (response.StatusCode == 404)
Interlocked.Increment(ref notFoundCount); try
else if (response.StatusCode == 400) {
Interlocked.Increment(ref invalidIdCount); // Ensure MailInfo and ProjectId are valid before proceeding
} if (mailGroup.MailInfo == null || mailGroup.ProjectId == Guid.Empty)
finally {
{ Interlocked.Increment(ref invalidIdCount);
semaphore.Release(); return;
}
var response = await reportHelper.GetProjectStatistics(
mailGroup.ProjectId,
mailGroup.Recipients,
mailGroup.MailInfo.Body,
mailGroup.MailInfo.Subject,
tenantId);
// Use a switch expression for cleaner counting
switch (response.StatusCode)
{
case 200: Interlocked.Increment(ref successCount); break;
case 404: Interlocked.Increment(ref notFoundCount); break;
case 400: Interlocked.Increment(ref invalidIdCount); break;
default: Interlocked.Increment(ref failureCount); break;
}
}
catch (Exception ex)
{
// 3. OPTIMIZATION: Make the process resilient.
// If one task fails unexpectedly, log it and continue with others.
_logger.LogError(ex, "Failed to send report for project {ProjectId}", mailGroup.ProjectId);
Interlocked.Increment(ref failureCount);
}
} }
}).ToList(); }).ToList();
await Task.WhenAll(sendTasks); 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( _logger.LogInfo(
"Emails of project reports sent for tenant {TenantId}. Successfully sent: {SuccessCount}, Projects not found: {NotFoundCount}, Invalid IDs: {InvalidIdsCount}", "Project report sending complete for tenant {TenantId}. Success: {SuccessCount}, Not Found: {NotFoundCount}, Invalid ID: {InvalidIdCount}, Failures: {FailureCount}",
tenantId, successCount, notFoundCount, invalidIdCount); tenantId, successCount, notFoundCount, invalidIdCount, failureCount);
return Ok(ApiResponse<object>.SuccessResponse( return Ok(ApiResponse<object>.SuccessResponse(
new { }, new { successCount, notFoundCount, invalidIdCount, failureCount },
$"Reports sent successfully: {successCount}. Projects not found: {notFoundCount}. Invalid IDs: {invalidIdCount}.", summaryMessage,
200)); 200));
} }
/// <summary>
/// Retrieves project statistics for a given project ID and sends an email report. [HttpPost("add-report-mail")]
/// </summary> public async Task<IActionResult> StoreProjectStatistics()
/// <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)
{ {
DateTime reportDate = DateTime.UtcNow.AddDays(-1).Date; Guid tenantId = _userHelper.GetTenantId();
if (projectId == Guid.Empty) // 1. Database-Side Grouping (Still the most efficient way to get initial data)
{ var projectMailGroups = await _context.MailDetails
_logger.LogError("Provided empty project ID while fetching project report.");
return ApiResponse<object>.ErrorResponse("Provided empty Project ID.", "Provided empty Project ID.", 400);
}
var project = await _context.Projects
.AsNoTracking() .AsNoTracking()
.FirstOrDefaultAsync(p => p.Id == projectId); .Where(m => m.TenantId == tenantId && m.ProjectId != Guid.Empty)
.GroupBy(m => new { m.ProjectId, m.MailListId })
if (project == null) .Select(g => new
{
_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);
}
var statisticReport = new ProjectStatisticReport
{
Date = reportDate,
ProjectName = project.Name ?? "",
TimeStamp = DateTime.Now.ToString("dd-MMM-yyyy HH:mm:ss", CultureInfo.InvariantCulture)
};
// Preload relevant data
var projectAllocations = await _context.ProjectAllocations
.Include(p => p.Employee)
.Where(p => p.ProjectId == project.Id && p.IsActive)
.ToListAsync();
var assignedEmployeeIds = projectAllocations.Select(p => p.EmployeeId).ToHashSet();
var attendances = await _context.Attendes
.AsNoTracking()
.Where(a => a.ProjectID == project.Id && a.InTime != null && a.InTime.Value.Date == reportDate)
.ToListAsync();
var checkedInEmployeeIds = attendances.Select(a => a.EmployeeID).Distinct().ToHashSet();
var checkoutPendingIds = attendances.Where(a => a.OutTime == null).Select(a => a.EmployeeID).Distinct().ToHashSet();
var regularizationIds = attendances
.Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE)
.Select(a => a.EmployeeID).Distinct().ToHashSet();
// Preload buildings, floors, areas
var buildings = await _context.Buildings.Where(b => b.ProjectId == project.Id).ToListAsync();
var buildingIds = buildings.Select(b => b.Id).ToList();
var floors = await _context.Floor.Where(f => buildingIds.Contains(f.BuildingId)).ToListAsync();
var floorIds = floors.Select(f => f.Id).ToList();
var areas = await _context.WorkAreas.Where(a => floorIds.Contains(a.FloorId)).ToListAsync();
var areaIds = areas.Select(a => a.Id).ToList();
var workItems = await _context.WorkItems
.Include(w => w.ActivityMaster)
.Where(w => areaIds.Contains(w.WorkAreaId))
.ToListAsync();
var itemIds = workItems.Select(i => i.Id).ToList();
var tasks = await _context.TaskAllocations
.Where(t => itemIds.Contains(t.WorkItemId))
.ToListAsync();
var taskIds = tasks.Select(t => t.Id).ToList();
var taskMembers = await _context.TaskMembers
.Include(m => m.Employee)
.Where(m => taskIds.Contains(m.TaskAllocationId))
.ToListAsync();
// Aggregate data
double totalPlannedWork = workItems.Sum(w => w.PlannedWork);
double totalCompletedWork = workItems.Sum(w => w.CompletedWork);
var todayAssignedTasks = tasks.Where(t => t.AssignmentDate.Date == reportDate).ToList();
var reportPending = tasks.Where(t => t.ReportedDate == null).ToList();
double totalPlannedTask = todayAssignedTasks.Sum(t => t.PlannedTask);
double totalCompletedTask = todayAssignedTasks.Sum(t => t.CompletedTask);
var jobRoles = await _context.JobRoles
.Where(j => j.TenantId == project.TenantId)
.ToListAsync();
// Team on site
var teamOnSite = jobRoles
.Select(role =>
{ {
var count = projectAllocations.Count(p => p.JobRoleId == role.Id && checkedInEmployeeIds.Contains(p.EmployeeId)); g.Key.ProjectId,
return new TeamOnSite { RoleName = role.Name, NumberofEmployees = count }; 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()
}) })
.OrderByDescending(t => t.NumberofEmployees) .ToListAsync();
.ToList();
// Task details if (!projectMailGroups.Any())
var performedTasks = todayAssignedTasks.Select(task =>
{ {
var workItem = workItems.FirstOrDefault(w => w.Id == task.WorkItemId); _logger.LogInfo("No project mail details found for tenant {TenantId} to process.", tenantId);
var area = areas.FirstOrDefault(a => a.Id == workItem?.WorkAreaId); return Ok(ApiResponse<object>.SuccessResponse("No project reports to generate.", "No project reports to generate.", 200));
var floor = floors.FirstOrDefault(f => f.Id == area?.FloorId);
var building = buildings.FirstOrDefault(b => b.Id == floor?.BuildingId);
string activityName = workItem?.ActivityMaster?.ActivityName ?? "";
string location = $"{building?.Name} > {floor?.FloorName}</span><br/><span style=\"color: gray; font-size: small; padding-left: 10px;\"> {floor?.FloorName}-{area?.AreaName}";
double pending = (workItem?.PlannedWork ?? 0) - (workItem?.CompletedWork ?? 0);
var taskTeam = taskMembers
.Where(m => m.TaskAllocationId == task.Id)
.Select(m =>
{
string name = $"{m.Employee?.FirstName ?? ""} {m.Employee?.LastName ?? ""}";
var role = jobRoles.FirstOrDefault(r => r.Id == m.Employee?.JobRoleId);
return new TaskTeam { Name = name, RoleName = role?.Name ?? "" };
})
.ToList();
return new PerformedTask
{
Activity = activityName,
Location = location,
AssignedToday = task.PlannedTask,
CompletedToday = task.CompletedTask,
Pending = pending,
Comment = task.Description,
Team = taskTeam
};
}).ToList();
// Attendance details
var performedAttendance = attendances.Select(att =>
{
var alloc = projectAllocations.FirstOrDefault(p => p.EmployeeId == att.EmployeeID);
var role = jobRoles.FirstOrDefault(r => r.Id == alloc?.JobRoleId);
string name = $"{alloc?.Employee?.FirstName ?? ""} {alloc?.Employee?.LastName ?? ""}";
return new PerformedAttendance
{
Name = name,
RoleName = role?.Name ?? "",
InTime = att.InTime ?? DateTime.UtcNow,
OutTime = att.OutTime,
Comment = att.Comment
};
}).ToList();
// Fill report
statisticReport.TodaysAttendances = checkedInEmployeeIds.Count;
statisticReport.TotalEmployees = assignedEmployeeIds.Count;
statisticReport.RegularizationPending = regularizationIds.Count;
statisticReport.CheckoutPending = checkoutPendingIds.Count;
statisticReport.TotalPlannedWork = totalPlannedWork;
statisticReport.TotalCompletedWork = totalCompletedWork;
statisticReport.TotalPlannedTask = totalPlannedTask;
statisticReport.TotalCompletedTask = totalCompletedTask;
statisticReport.CompletionStatus = totalPlannedWork > 0 ? totalCompletedWork / totalPlannedWork : 0;
statisticReport.TodaysAssignTasks = todayAssignedTasks.Count;
statisticReport.ReportPending = reportPending.Count;
statisticReport.TeamOnSite = teamOnSite;
statisticReport.PerformedTasks = performedTasks;
statisticReport.PerformedAttendance = performedAttendance;
// Send Email
var emailBody = await _emailSender.SendProjectStatisticsEmail(recipientEmails, body, subject, statisticReport);
var employee = await _context.Employees.FirstOrDefaultAsync(e => e.Email != null && recipientEmails.Contains(e.Email)) ?? new Employee();
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); string env = _configuration["environment:Title"] ?? string.Empty;
await _context.SaveChangesAsync(); // 2. Process each group concurrently, but with isolated DBContexts.
return ApiResponse<object>.SuccessResponse(statisticReport, "Email sent successfully", 200); var processingTasks = projectMailGroups.Select(async group =>
{
// 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));
}
return Ok(ApiResponse<object>.SuccessResponse(mailList, "Fetched list of mail body successfully", 200));
} }
} }
} }

View File

@ -10,6 +10,7 @@ using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels; using Marco.Pms.Model.ViewModels;
using Marco.Pms.Model.ViewModels.Master; using Marco.Pms.Model.ViewModels.Master;
using Marco.Pms.Model.ViewModels.Roles; using Marco.Pms.Model.ViewModels.Roles;
using Marco.Pms.Services.Helpers;
using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service; using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -29,14 +30,17 @@ namespace MarcoBMS.Services.Controllers
private readonly UserHelper _userHelper; private readonly UserHelper _userHelper;
private readonly UserManager<ApplicationUser> _userManager; private readonly UserManager<ApplicationUser> _userManager;
private readonly ILoggingService _logger; private readonly ILoggingService _logger;
private readonly CacheUpdateHelper _cache;
public RolesController(UserManager<ApplicationUser> userManager, ApplicationDbContext context, RolesHelper rolesHelper, UserHelper userHelper, ILoggingService logger) public RolesController(UserManager<ApplicationUser> userManager, ApplicationDbContext context, RolesHelper rolesHelper, UserHelper userHelper, ILoggingService logger,
CacheUpdateHelper cache)
{ {
_context = context; _context = context;
_userManager = userManager; _userManager = userManager;
_rolesHelper = rolesHelper; _rolesHelper = rolesHelper;
_userHelper = userHelper; _userHelper = userHelper;
_logger = logger; _logger = logger;
_cache = cache;
} }
private Guid GetTenantId() private Guid GetTenantId()
@ -288,10 +292,16 @@ namespace MarcoBMS.Services.Controllers
_context.RolePermissionMappings.Add(item); _context.RolePermissionMappings.Add(item);
modified = true; modified = true;
} }
if (item.FeaturePermissionId == Guid.Parse("172fc9b6-755b-4f62-ab26-55c34a330614"))
{
await _cache.ClearAllProjectIdsByRoleId(id);
}
} }
if (modified) if (modified)
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
await _cache.ClearAllPermissionIdsByRoleId(id);
ApplicationRolesVM response = role.ToRoleVMFromApplicationRole(); ApplicationRolesVM response = role.ToRoleVMFromApplicationRole();
List<FeaturePermission> permissions = await _rolesHelper.GetFeaturePermissionByRoleID(response.Id); List<FeaturePermission> permissions = await _rolesHelper.GetFeaturePermissionByRoleID(response.Id);
response.FeaturePermission = permissions.Select(c => c.ToFeaturePermissionVMFromFeaturePermission()).ToList(); response.FeaturePermission = permissions.Select(c => c.ToFeaturePermissionVMFromFeaturePermission()).ToList();
@ -424,12 +434,16 @@ namespace MarcoBMS.Services.Controllers
if (role.IsEnabled == true) if (role.IsEnabled == true)
{ {
_context.EmployeeRoleMappings.Add(mapping); _context.EmployeeRoleMappings.Add(mapping);
await _cache.AddApplicationRole(role.EmployeeId, [mapping.RoleId]);
} }
} }
else if (role.IsEnabled == false) else if (role.IsEnabled == false)
{ {
_context.EmployeeRoleMappings.Remove(existingItem); _context.EmployeeRoleMappings.Remove(existingItem);
await _cache.RemoveRoleId(existingItem.EmployeeId, existingItem.RoleId);
await _cache.ClearAllPermissionIdsByEmployeeID(existingItem.EmployeeId);
} }
await _cache.ClearAllProjectIds(role.EmployeeId);
} }
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();

View File

@ -6,11 +6,14 @@ using Marco.Pms.Model.Mapper;
using Marco.Pms.Model.Projects; using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities; using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Activities; using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Services.Helpers;
using Marco.Pms.Services.Hubs;
using Marco.Pms.Services.Service; using Marco.Pms.Services.Service;
using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service; using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Document = Marco.Pms.Model.DocumentManager.Document; using Document = Marco.Pms.Model.DocumentManager.Document;
@ -27,19 +30,20 @@ namespace MarcoBMS.Services.Controllers
private readonly UserHelper _userHelper; private readonly UserHelper _userHelper;
private readonly S3UploadService _s3Service; private readonly S3UploadService _s3Service;
private readonly ILoggingService _logger; private readonly ILoggingService _logger;
private readonly IHubContext<MarcoHub> _signalR;
private readonly CacheUpdateHelper _cache;
private readonly PermissionServices _permissionServices; private readonly PermissionServices _permissionServices;
private readonly Guid Approve_Task;
private readonly Guid Assign_Report_Task;
public TaskController(ApplicationDbContext context, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permissionServices) public TaskController(ApplicationDbContext context, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permissionServices,
IHubContext<MarcoHub> signalR, CacheUpdateHelper cache)
{ {
_context = context; _context = context;
_userHelper = userHelper; _userHelper = userHelper;
_s3Service = s3Service; _s3Service = s3Service;
_logger = logger; _logger = logger;
_signalR = signalR;
_cache = cache;
_permissionServices = permissionServices; _permissionServices = permissionServices;
Approve_Task = Guid.Parse("db4e40c5-2ba9-4b6d-b8a6-a16a250ff99c");
Assign_Report_Task = Guid.Parse("6a32379b-8b3f-49a6-8c48-4b7ac1b55dc2");
} }
private Guid GetTenantId() private Guid GetTenantId()
@ -67,7 +71,7 @@ namespace MarcoBMS.Services.Controllers
var employee = await _userHelper.GetCurrentEmployeeAsync(); var employee = await _userHelper.GetCurrentEmployeeAsync();
// Check for permission to approve tasks // Check for permission to approve tasks
var hasPermission = await _permissionServices.HasPermission(Assign_Report_Task, employee.Id); var hasPermission = await _permissionServices.HasPermission(PermissionsMaster.AssignAndReportProgress, employee.Id);
if (!hasPermission) if (!hasPermission)
{ {
_logger.LogWarning("Employee {EmployeeId} attempted to assign Task without permission", employee.Id); _logger.LogWarning("Employee {EmployeeId} attempted to assign Task without permission", employee.Id);
@ -81,6 +85,8 @@ namespace MarcoBMS.Services.Controllers
_context.TaskAllocations.Add(taskAllocation); _context.TaskAllocations.Add(taskAllocation);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
await _cache.UpdatePlannedAndCompleteWorksInWorkItem(taskAllocation.WorkItemId, todaysAssigned: taskAllocation.PlannedTask);
_logger.LogInfo("Task {TaskId} assigned by Employee {EmployeeId}", taskAllocation.Id, employee.Id); _logger.LogInfo("Task {TaskId} assigned by Employee {EmployeeId}", taskAllocation.Id, employee.Id);
var response = taskAllocation.ToAssignTaskVMFromTaskAllocation(); var response = taskAllocation.ToAssignTaskVMFromTaskAllocation();
@ -131,7 +137,7 @@ namespace MarcoBMS.Services.Controllers
var tenantId = GetTenantId(); var tenantId = GetTenantId();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var hasPermission = await _permissionServices.HasPermission(Assign_Report_Task, loggedInEmployee.Id); var hasPermission = await _permissionServices.HasPermission(PermissionsMaster.AssignAndReportProgress, loggedInEmployee.Id);
if (!hasPermission) if (!hasPermission)
{ {
_logger.LogWarning("Unauthorized task report attempt by Employee {EmployeeId} for Task {TaskId}", loggedInEmployee.Id, reportTask.Id); _logger.LogWarning("Unauthorized task report attempt by Employee {EmployeeId} for Task {TaskId}", loggedInEmployee.Id, reportTask.Id);
@ -194,16 +200,22 @@ namespace MarcoBMS.Services.Controllers
var comment = reportTask.ToCommentFromReportTaskDto(tenantId, loggedInEmployee.Id); var comment = reportTask.ToCommentFromReportTaskDto(tenantId, loggedInEmployee.Id);
_context.TaskComments.Add(comment); _context.TaskComments.Add(comment);
int numberofImages = 0;
var workAreaId = taskAllocation.WorkItem?.WorkAreaId;
var workArea = await _context.WorkAreas.Include(a => a.Floor)
.FirstOrDefaultAsync(a => a.Id == workAreaId) ?? new WorkArea();
var buildingId = workArea.Floor?.BuildingId;
var building = await _context.Buildings
.FirstOrDefaultAsync(b => b.Id == buildingId);
var batchId = Guid.NewGuid();
var projectId = building?.ProjectId;
if (reportTask.Images?.Any() == true) if (reportTask.Images?.Any() == true)
{ {
var workAreaId = taskAllocation.WorkItem?.WorkAreaId;
var workArea = await _context.WorkAreas.Include(a => a.Floor)
.FirstOrDefaultAsync(a => a.Id == workAreaId) ?? new WorkArea();
var buildingId = workArea.Floor?.BuildingId;
var building = await _context.Buildings
.FirstOrDefaultAsync(b => b.Id == buildingId);
foreach (var image in reportTask.Images) foreach (var image in reportTask.Images)
{ {
@ -219,16 +231,18 @@ namespace MarcoBMS.Services.Controllers
var fileType = _s3Service.GetContentTypeFromBase64(base64); var fileType = _s3Service.GetContentTypeFromBase64(base64);
var fileName = _s3Service.GenerateFileName(fileType, tenantId, "task_report"); var fileName = _s3Service.GenerateFileName(fileType, tenantId, "task_report");
var objectKey = $"tenant-{tenantId}/project-{building?.ProjectId}/Actitvity/{fileName}"; var objectKey = $"tenant-{tenantId}/project-{projectId}/Actitvity/{fileName}";
await _s3Service.UploadFileAsync(base64, fileType, objectKey); await _s3Service.UploadFileAsync(base64, fileType, objectKey);
var document = new Document var document = new Document
{ {
BatchId = batchId,
UploadedById = loggedInEmployee.Id,
FileName = image.FileName ?? "", FileName = image.FileName ?? "",
ContentType = image.ContentType ?? "", ContentType = image.ContentType ?? "",
S3Key = objectKey, S3Key = objectKey,
Base64Data = image.Base64Data, //Base64Data = image.Base64Data,
FileSize = image.FileSize, FileSize = image.FileSize,
UploadedAt = DateTime.UtcNow, UploadedAt = DateTime.UtcNow,
TenantId = tenantId TenantId = tenantId
@ -241,10 +255,15 @@ namespace MarcoBMS.Services.Controllers
ReferenceId = reportTask.Id ReferenceId = reportTask.Id
}; };
_context.TaskAttachments.Add(attachment); _context.TaskAttachments.Add(attachment);
numberofImages += 1;
} }
} }
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
var selectedWorkAreaId = taskAllocation.WorkItem?.WorkAreaId ?? Guid.Empty;
await _cache.UpdatePlannedAndCompleteWorksInWorkItem(taskAllocation.WorkItemId, completedWork: taskAllocation.CompletedTask);
await _cache.UpdatePlannedAndCompleteWorksInBuilding(selectedWorkAreaId, completedWork: taskAllocation.CompletedTask);
var response = taskAllocation.ToReportTaskVMFromTaskAllocation(); var response = taskAllocation.ToReportTaskVMFromTaskAllocation();
var comments = await _context.TaskComments var comments = await _context.TaskComments
@ -254,6 +273,9 @@ namespace MarcoBMS.Services.Controllers
response.Comments = comments.Select(c => c.ToCommentVMFromTaskComment()).ToList(); response.Comments = comments.Select(c => c.ToCommentVMFromTaskComment()).ToList();
response.checkList = checkListVMs; response.checkList = checkListVMs;
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Task_Report", NumberOfImages = numberofImages, ProjectId = projectId };
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
_logger.LogInfo("Task {TaskId} reported successfully by Employee {EmployeeId}", taskAllocation.Id, loggedInEmployee.Id); _logger.LogInfo("Task {TaskId} reported successfully by Employee {EmployeeId}", taskAllocation.Id, loggedInEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(response, "Task reported successfully", 200)); return Ok(ApiResponse<object>.SuccessResponse(response, "Task reported successfully", 200));
@ -265,7 +287,7 @@ namespace MarcoBMS.Services.Controllers
_logger.LogInfo("AddCommentForTask called for TaskAllocationId: {TaskId}", createComment.TaskAllocationId); _logger.LogInfo("AddCommentForTask called for TaskAllocationId: {TaskId}", createComment.TaskAllocationId);
var tenantId = GetTenantId(); var tenantId = GetTenantId();
var employee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Validate Task Allocation and associated WorkItem // Validate Task Allocation and associated WorkItem
var taskAllocation = await _context.TaskAllocations var taskAllocation = await _context.TaskAllocations
@ -285,15 +307,18 @@ namespace MarcoBMS.Services.Controllers
var buildingId = workArea.Floor?.BuildingId ?? Guid.Empty; var buildingId = workArea.Floor?.BuildingId ?? Guid.Empty;
var building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == buildingId); var building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == buildingId);
var projectId = building?.ProjectId;
// Save comment // Save comment
var comment = createComment.ToCommentFromCommentDto(tenantId, employee.Id); var comment = createComment.ToCommentFromCommentDto(tenantId, loggedInEmployee.Id);
_context.TaskComments.Add(comment); _context.TaskComments.Add(comment);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
_logger.LogInfo("Comment saved with Id: {CommentId}", comment.Id); _logger.LogInfo("Comment saved with Id: {CommentId}", comment.Id);
// Process image uploads // Process image uploads
var images = createComment.Images; var images = createComment.Images;
var batchId = Guid.NewGuid();
int numberofImages = 0;
if (images != null && images.Any()) if (images != null && images.Any())
{ {
@ -312,17 +337,19 @@ namespace MarcoBMS.Services.Controllers
var fileType = _s3Service.GetContentTypeFromBase64(base64); var fileType = _s3Service.GetContentTypeFromBase64(base64);
var fileName = _s3Service.GenerateFileName(fileType, tenantId, "task_comment"); var fileName = _s3Service.GenerateFileName(fileType, tenantId, "task_comment");
var objectKey = $"tenant-{tenantId}/project-{building?.ProjectId}/Activity/{fileName}"; var objectKey = $"tenant-{tenantId}/project-{projectId}/Activity/{fileName}";
await _s3Service.UploadFileAsync(base64, fileType, objectKey); await _s3Service.UploadFileAsync(base64, fileType, objectKey);
_logger.LogInfo("Image uploaded to S3 with key: {ObjectKey}", objectKey); _logger.LogInfo("Image uploaded to S3 with key: {ObjectKey}", objectKey);
var document = new Document var document = new Document
{ {
BatchId = batchId,
UploadedById = loggedInEmployee.Id,
FileName = image.FileName ?? string.Empty, FileName = image.FileName ?? string.Empty,
ContentType = image.ContentType ?? fileType, ContentType = image.ContentType ?? fileType,
S3Key = objectKey, S3Key = objectKey,
Base64Data = image.Base64Data, //Base64Data = image.Base64Data,
FileSize = image.FileSize, FileSize = image.FileSize,
UploadedAt = DateTime.UtcNow, UploadedAt = DateTime.UtcNow,
TenantId = tenantId TenantId = tenantId
@ -337,6 +364,7 @@ namespace MarcoBMS.Services.Controllers
}; };
_context.TaskAttachments.Add(attachment); _context.TaskAttachments.Add(attachment);
numberofImages += 1;
} }
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
@ -347,6 +375,9 @@ namespace MarcoBMS.Services.Controllers
var response = comment.ToCommentVMFromTaskComment(); var response = comment.ToCommentVMFromTaskComment();
_logger.LogInfo("Returning response for commentId: {CommentId}", comment.Id); _logger.LogInfo("Returning response for commentId: {CommentId}", comment.Id);
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Task_Comment", NumberOfImages = numberofImages, ProjectId = projectId };
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
return Ok(ApiResponse<object>.SuccessResponse(response, "Comment saved successfully", 200)); return Ok(ApiResponse<object>.SuccessResponse(response, "Comment saved successfully", 200));
} }
@ -653,6 +684,7 @@ namespace MarcoBMS.Services.Controllers
/// </summary> /// </summary>
/// <param name="approveTask">DTO containing task approval details.</param> /// <param name="approveTask">DTO containing task approval details.</param>
/// <returns>IActionResult indicating success or failure.</returns> /// <returns>IActionResult indicating success or failure.</returns>
[HttpPost("approve")] [HttpPost("approve")]
public async Task<IActionResult> ApproveTask(ApproveTaskDto approveTask) public async Task<IActionResult> ApproveTask(ApproveTaskDto approveTask)
{ {
@ -675,7 +707,7 @@ namespace MarcoBMS.Services.Controllers
} }
// Check for permission to approve tasks // Check for permission to approve tasks
var hasPermission = await _permissionServices.HasPermission(Approve_Task, loggedInEmployee.Id); var hasPermission = await _permissionServices.HasPermission(PermissionsMaster.ApproveTask, loggedInEmployee.Id);
if (!hasPermission) if (!hasPermission)
{ {
_logger.LogWarning("Employee {EmployeeId} attempted to approve Task {TaskId} without permission", loggedInEmployee.Id, approveTask.Id); _logger.LogWarning("Employee {EmployeeId} attempted to approve Task {TaskId} without permission", loggedInEmployee.Id, approveTask.Id);
@ -719,17 +751,21 @@ namespace MarcoBMS.Services.Controllers
}; };
_context.TaskComments.Add(comment); _context.TaskComments.Add(comment);
var workAreaId = taskAllocation.WorkItem?.WorkAreaId;
var workArea = await _context.WorkAreas.Include(a => a.Floor)
.FirstOrDefaultAsync(a => a.Id == workAreaId) ?? new WorkArea();
var buildingId = workArea.Floor?.BuildingId;
var building = await _context.Buildings
.FirstOrDefaultAsync(b => b.Id == buildingId);
var projectId = building?.ProjectId;
int numberofImages = 0;
// Handle image attachments, if any // Handle image attachments, if any
if (approveTask.Images?.Count > 0) if (approveTask.Images?.Count > 0)
{ {
var workAreaId = taskAllocation.WorkItem?.WorkAreaId; var batchId = Guid.NewGuid();
var workArea = await _context.WorkAreas.Include(a => a.Floor)
.FirstOrDefaultAsync(a => a.Id == workAreaId) ?? new WorkArea();
var buildingId = workArea.Floor?.BuildingId;
var building = await _context.Buildings
.FirstOrDefaultAsync(b => b.Id == buildingId);
foreach (var image in approveTask.Images) foreach (var image in approveTask.Images)
{ {
@ -743,16 +779,18 @@ namespace MarcoBMS.Services.Controllers
var fileType = _s3Service.GetContentTypeFromBase64(base64); var fileType = _s3Service.GetContentTypeFromBase64(base64);
var fileName = _s3Service.GenerateFileName(fileType, tenantId, "task_comment"); var fileName = _s3Service.GenerateFileName(fileType, tenantId, "task_comment");
var objectKey = $"tenant-{tenantId}/project-{building?.ProjectId}/Activity/{fileName}"; var objectKey = $"tenant-{tenantId}/project-{projectId}/Activity/{fileName}";
await _s3Service.UploadFileAsync(base64, fileType, objectKey); await _s3Service.UploadFileAsync(base64, fileType, objectKey);
var document = new Document var document = new Document
{ {
BatchId = batchId,
UploadedById = loggedInEmployee.Id,
FileName = fileName, FileName = fileName,
ContentType = image.ContentType ?? string.Empty, ContentType = image.ContentType ?? string.Empty,
S3Key = objectKey, S3Key = objectKey,
Base64Data = image.Base64Data, //Base64Data = image.Base64Data,
FileSize = image.FileSize, FileSize = image.FileSize,
UploadedAt = DateTime.UtcNow, UploadedAt = DateTime.UtcNow,
TenantId = tenantId TenantId = tenantId
@ -769,12 +807,16 @@ namespace MarcoBMS.Services.Controllers
_context.TaskAttachments.Add(attachment); _context.TaskAttachments.Add(attachment);
_logger.LogInfo("Attachment uploaded for Task {TaskId}: {FileName}", approveTask.Id, fileName); _logger.LogInfo("Attachment uploaded for Task {TaskId}: {FileName}", approveTask.Id, fileName);
numberofImages += 1;
} }
} }
// Commit all changes to the database // Commit all changes to the database
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Task_Report", NumberOfImages = numberofImages, ProjectId = projectId };
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
_logger.LogInfo("Task {TaskId} successfully approved by Employee {EmployeeId}", approveTask.Id, loggedInEmployee.Id); _logger.LogInfo("Task {TaskId} successfully approved by Employee {EmployeeId}", approveTask.Id, loggedInEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse("Task has been approved", "Task has been approved", 200)); return Ok(ApiResponse<object>.SuccessResponse("Task has been approved", "Task has been approved", 200));

View File

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

View File

@ -19,6 +19,7 @@ COPY ["Marco.Pms.Services/Marco.Pms.Services.csproj", "Marco.Pms.Services/"]
COPY ["Marco.Pms.DataAccess/Marco.Pms.DataAccess.csproj", "Marco.Pms.DataAccess/"] COPY ["Marco.Pms.DataAccess/Marco.Pms.DataAccess.csproj", "Marco.Pms.DataAccess/"]
COPY ["Marco.Pms.Model/Marco.Pms.Model.csproj", "Marco.Pms.Model/"] COPY ["Marco.Pms.Model/Marco.Pms.Model.csproj", "Marco.Pms.Model/"]
COPY ["Marco.Pms.Utility/Marco.Pms.Utility.csproj", "Marco.Pms.Utility/"] COPY ["Marco.Pms.Utility/Marco.Pms.Utility.csproj", "Marco.Pms.Utility/"]
COPY ["Marco.Pms.CacheHelper/Marco.Pms.CacheHelper.csproj", "Marco.Pms.CacheHelper/"]
RUN dotnet restore "./Marco.Pms.Services/Marco.Pms.Services.csproj" RUN dotnet restore "./Marco.Pms.Services/Marco.Pms.Services.csproj"
COPY . . COPY . .
WORKDIR "/src/Marco.Pms.Services" WORKDIR "/src/Marco.Pms.Services"

View File

@ -0,0 +1,867 @@
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
{
public class CacheUpdateHelper
{
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)
{
_projectCache = projectCache;
_employeeCache = employeeCache;
_reportCache = reportCache;
_logger = logger;
_dbContextFactory = dbContextFactory;
_context = context;
_generalHelper = generalHelper;
}
// ------------------------------------ Project Details 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);
}
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");
}
}
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);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while updating project {ProjectId} to Cache", project.Id);
return false;
}
}
public async Task<ProjectMongoDB?> GetProjectDetails(Guid projectId)
{
try
{
var response = await _projectCache.GetProjectDetailsFromCache(projectId);
return response;
}
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");
return null;
}
}
public async Task<List<ProjectMongoDB>?> GetProjectDetailsList(List<Guid> projectIds)
{
try
{
var response = await _projectCache.GetProjectDetailsListFromCache(projectIds);
if (response.Any())
{
return response;
}
else
{
return null;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occured while getting list of project details from to Cache");
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
{
await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while adding project infra for project {ProjectId} to Cache: {Error}", projectId, ex.Message);
}
}
public async Task UpdateBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null)
{
try
{
var response = await _projectCache.UpdateBuildngInfraToCache(projectId, building, floor, workArea, buildingId);
if (!response)
{
await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId);
}
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while updating project infra for project {ProjectId} to Cache: {Error}", projectId, ex.Message);
}
}
public async Task<List<BuildingMongoDB>?> GetBuildingInfra(Guid projectId)
{
try
{
var response = await _projectCache.GetBuildingInfraFromCache(projectId);
return response;
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while getting project infra for project {ProjectId} form Cache: {Error}", projectId, ex.Message);
return null;
}
}
public async Task UpdatePlannedAndCompleteWorksInBuilding(Guid workAreaId, double plannedWork = 0, double completedWork = 0)
{
try
{
await _projectCache.UpdatePlannedAndCompleteWorksInBuildingFromCache(workAreaId, plannedWork, completedWork);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while updating planned work and completed work in building infra form Cache: {Error}", ex.Message);
}
}
public async Task<WorkAreaInfoMongoDB?> GetBuildingAndFloorByWorkAreaId(Guid workAreaId)
{
try
{
var response = await _projectCache.GetBuildingAndFloorByWorkAreaIdFromCache(workAreaId);
return response;
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while fetching workArea Details using its ID form Cache: {Error}", ex.Message);
return null;
}
}
// ------------------------------------------------------- WorkItem -------------------------------------------------------
public async Task<List<WorkItemMongoDB>?> GetWorkItemsByWorkAreaIds(List<Guid> workAreaIds)
{
try
{
var response = await _projectCache.GetWorkItemsByWorkAreaIdsFromCache(workAreaIds);
if (response.Count > 0)
{
return response;
}
return null;
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while fetching workItems list using workArea IDs list form Cache: {Error}", ex.Message);
return null;
}
}
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
{
await _projectCache.ManageWorkItemDetailsToCache(workItems);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while saving workItems form Cache: {Error}", ex.Message);
}
}
public async Task<List<WorkItemMongoDB>?> GetWorkItemDetailsByWorkArea(Guid workAreaId)
{
try
{
var workItems = await _projectCache.GetWorkItemDetailsByWorkAreaFromCache(workAreaId);
if (workItems.Count > 0)
{
return workItems;
}
else
{
return null;
}
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while fetching list of workItems form Cache: {Error}", ex.Message);
return null;
}
}
public async Task<WorkItemMongoDB?> GetWorkItemDetailsById(Guid id)
{
try
{
var workItem = await _projectCache.GetWorkItemDetailsByIdFromCache(id);
if (workItem.Id != "")
{
return workItem;
}
else
{
return null;
}
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while fetching list of workItems form Cache: {Error}", ex.Message);
return null;
}
}
public async Task UpdatePlannedAndCompleteWorksInWorkItem(Guid id, double plannedWork = 0, double completedWork = 0, double todaysAssigned = 0)
{
try
{
var response = await _projectCache.UpdatePlannedAndCompleteWorksInWorkItemToCache(id, plannedWork, completedWork, todaysAssigned);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while updating planned work, completed work, and today's assigned work in workItems in Cache: {Error}", ex.Message);
}
}
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);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while adding Application roleIds to Cache to employee {Employee}: {Error}", employeeId, ex.Message);
}
}
public async Task<bool> AddProjects(Guid employeeId, List<Guid> projectIds)
{
try
{
var response = await _employeeCache.AddProjectsToCache(employeeId, projectIds);
return response;
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while adding projectIds for employee {EmployeeId} to Cache: {Error}", employeeId, ex.Message);
return false;
}
}
public async Task<List<Guid>?> GetProjects(Guid employeeId)
{
try
{
var response = await _employeeCache.GetProjectsFromCache(employeeId);
if (response.Count > 0)
{
return response;
}
return null;
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while getting projectIds for employee {EmployeeId} from Cache: {Error}", employeeId, ex.Message);
return null;
}
}
public async Task<List<Guid>?> GetPermissions(Guid employeeId)
{
try
{
var response = await _employeeCache.GetPermissionsFromCache(employeeId);
if (response.Count > 0)
{
return response;
}
return null;
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while getting permissionIds for employee {EmployeeId} from Cache: {Error}", employeeId, ex.Message);
return null;
}
}
public async Task ClearAllProjectIds(Guid employeeId)
{
try
{
var response = await _employeeCache.ClearAllProjectIdsFromCache(employeeId);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while deleting projectIds from Cache for employee {EmployeeId}: {Error}", employeeId, ex.Message);
}
}
public async Task ClearAllProjectIdsByRoleId(Guid roleId)
{
try
{
await _employeeCache.ClearAllProjectIdsByRoleIdFromCache(roleId);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while deleting projectIds from Cache for Application Role {RoleId}: {Error}", roleId, ex.Message);
}
}
public async Task ClearAllProjectIdsByPermissionId(Guid permissionId)
{
try
{
await _employeeCache.ClearAllProjectIdsByPermissionIdFromCache(permissionId);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while deleting projectIds from Cache for Permission {PermissionId}: {Error}", permissionId, ex.Message);
}
}
public async Task ClearAllPermissionIdsByEmployeeID(Guid employeeId)
{
try
{
var response = await _employeeCache.ClearAllPermissionIdsByEmployeeIDFromCache(employeeId);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while deleting permissionIds from Cache for employee {EmployeeId}: {Error}", employeeId, ex.Message);
}
}
public async Task ClearAllPermissionIdsByRoleId(Guid roleId)
{
try
{
var response = await _employeeCache.ClearAllPermissionIdsByRoleIdFromCache(roleId);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while deleting permissionIds from Cache for Application role {RoleId}: {Error}", roleId, ex.Message);
}
}
public async Task RemoveRoleId(Guid employeeId, Guid roleId)
{
try
{
var response = await _employeeCache.RemoveRoleIdFromCache(employeeId, roleId);
}
catch (Exception ex)
{
_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

@ -1,6 +1,7 @@
using Marco.Pms.DataAccess.Data; using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Directory; using Marco.Pms.Model.Directory;
using Marco.Pms.Model.Dtos.Directory; using Marco.Pms.Model.Dtos.Directory;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Mapper;
using Marco.Pms.Model.Projects; using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities; using Marco.Pms.Model.Utilities;
@ -20,9 +21,6 @@ namespace Marco.Pms.Services.Helpers
private readonly ILoggingService _logger; private readonly ILoggingService _logger;
private readonly UserHelper _userHelper; private readonly UserHelper _userHelper;
private readonly PermissionServices _permissionServices; private readonly PermissionServices _permissionServices;
private readonly Guid directoryAdmin;
private readonly Guid directoryManager;
private readonly Guid directoryUser;
public DirectoryHelper(ApplicationDbContext context, ILoggingService logger, UserHelper userHelper, PermissionServices permissionServices) public DirectoryHelper(ApplicationDbContext context, ILoggingService logger, UserHelper userHelper, PermissionServices permissionServices)
{ {
@ -30,13 +28,8 @@ namespace Marco.Pms.Services.Helpers
_logger = logger; _logger = logger;
_userHelper = userHelper; _userHelper = userHelper;
_permissionServices = permissionServices; _permissionServices = permissionServices;
directoryAdmin = Guid.Parse("4286a13b-bb40-4879-8c6d-18e9e393beda");
directoryManager = Guid.Parse("62668630-13ce-4f52-a0f0-db38af2230c5");
directoryUser = Guid.Parse("0f919170-92d4-4337-abd3-49b66fc871bb");
} }
public async Task<ApiResponse<object>> GetListOfContacts(string? search, bool active, ContactFilterDto? filterDto, Guid? projectId) public async Task<ApiResponse<object>> GetListOfContacts(string? search, bool active, ContactFilterDto? filterDto, Guid? projectId)
{ {
Guid tenantId = _userHelper.GetTenantId(); Guid tenantId = _userHelper.GetTenantId();
@ -45,12 +38,12 @@ namespace Marco.Pms.Services.Helpers
var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync(); var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync();
List<EmployeeBucketMapping>? employeeBuckets = await _context.EmployeeBucketMappings.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).ToListAsync(); List<EmployeeBucketMapping>? employeeBuckets = await _context.EmployeeBucketMappings.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).ToListAsync();
List<Guid> bucketIds = employeeBuckets.Select(c => c.BucketId).ToList(); List<Guid> bucketIds = employeeBuckets.Select(c => c.BucketId).ToList();
if (permissionIds.Contains(directoryAdmin)) if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin))
{ {
var buckets = await _context.Buckets.Where(b => b.TenantId == tenantId).ToListAsync(); var buckets = await _context.Buckets.Where(b => b.TenantId == tenantId).ToListAsync();
bucketIds = buckets.Select(b => b.Id).ToList(); bucketIds = buckets.Select(b => b.Id).ToList();
} }
else if (permissionIds.Contains(directoryManager) || permissionIds.Contains(directoryUser)) else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) || permissionIds.Contains(PermissionsMaster.DirectoryUser))
{ {
var buckets = await _context.Buckets.Where(b => b.CreatedByID == LoggedInEmployee.Id).ToListAsync(); var buckets = await _context.Buckets.Where(b => b.CreatedByID == LoggedInEmployee.Id).ToListAsync();
var createdBucketIds = buckets.Select(b => b.Id).ToList(); var createdBucketIds = buckets.Select(b => b.Id).ToList();
@ -59,7 +52,7 @@ namespace Marco.Pms.Services.Helpers
} }
else else
{ {
_logger.LogError("Employee {EmployeeId} attemped to access a contacts, but do not have permission", LoggedInEmployee.Id); _logger.LogWarning("Employee {EmployeeId} attemped to access a contacts, but do not have permission", LoggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("You don't have permission", "You don't have permission", 401); return ApiResponse<object>.ErrorResponse("You don't have permission", "You don't have permission", 401);
} }
@ -199,17 +192,17 @@ namespace Marco.Pms.Services.Helpers
var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync(); var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync();
EmployeeBucketMapping? employeeBucket = null; EmployeeBucketMapping? employeeBucket = null;
if (permissionIds.Contains(directoryAdmin)) if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin))
{ {
employeeBucket = employeeBuckets.FirstOrDefault(); employeeBucket = employeeBuckets.FirstOrDefault();
} }
else if (permissionIds.Contains(directoryManager) || permissionIds.Contains(directoryUser)) else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) || permissionIds.Contains(PermissionsMaster.DirectoryUser))
{ {
employeeBucket = employeeBuckets.FirstOrDefault(eb => eb.EmployeeId == LoggedInEmployee.Id); employeeBucket = employeeBuckets.FirstOrDefault(eb => eb.EmployeeId == LoggedInEmployee.Id);
} }
else else
{ {
_logger.LogError("Employee {EmployeeId} attemped to access a contacts with in bucket {BucketId}, but do not have permission", LoggedInEmployee.Id, id); _logger.LogWarning("Employee {EmployeeId} attemped to access a contacts with in bucket {BucketId}, but do not have permission", LoggedInEmployee.Id, id);
return ApiResponse<object>.ErrorResponse("You don't have permission", "You don't have permission", 401); return ApiResponse<object>.ErrorResponse("You don't have permission", "You don't have permission", 401);
} }
@ -483,12 +476,12 @@ namespace Marco.Pms.Services.Helpers
var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync(); var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync();
List<EmployeeBucketMapping>? employeeBuckets = await _context.EmployeeBucketMappings.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).ToListAsync(); List<EmployeeBucketMapping>? employeeBuckets = await _context.EmployeeBucketMappings.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).ToListAsync();
List<Guid> bucketIds = employeeBuckets.Select(c => c.BucketId).ToList(); List<Guid> bucketIds = employeeBuckets.Select(c => c.BucketId).ToList();
if (permissionIds.Contains(directoryAdmin)) if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin))
{ {
var buckets = await _context.Buckets.Where(b => b.TenantId == tenantId).ToListAsync(); var buckets = await _context.Buckets.Where(b => b.TenantId == tenantId).ToListAsync();
bucketIds = buckets.Select(b => b.Id).ToList(); bucketIds = buckets.Select(b => b.Id).ToList();
} }
else if (permissionIds.Contains(directoryManager) || permissionIds.Contains(directoryUser)) else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) || permissionIds.Contains(PermissionsMaster.DirectoryUser))
{ {
var buckets = await _context.Buckets.Where(b => b.CreatedByID == LoggedInEmployee.Id).ToListAsync(); var buckets = await _context.Buckets.Where(b => b.CreatedByID == LoggedInEmployee.Id).ToListAsync();
var createdBucketIds = buckets.Select(b => b.Id).ToList(); var createdBucketIds = buckets.Select(b => b.Id).ToList();
@ -497,7 +490,7 @@ namespace Marco.Pms.Services.Helpers
} }
else else
{ {
_logger.LogError("Employee {EmployeeId} attemped to update a contact, but do not have permission", LoggedInEmployee.Id); _logger.LogWarning("Employee {EmployeeId} attemped to update a contact, but do not have permission", LoggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("You don't have permission", "You don't have permission", 401); return ApiResponse<object>.ErrorResponse("You don't have permission", "You don't have permission", 401);
} }
@ -747,6 +740,7 @@ namespace Marco.Pms.Services.Helpers
{ {
Guid tenantId = _userHelper.GetTenantId(); Guid tenantId = _userHelper.GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var hasAdminPermission = await _permissionServices.HasPermission(PermissionsMaster.DirectoryAdmin, LoggedInEmployee.Id);
if (id != Guid.Empty) if (id != Guid.Empty)
{ {
Contact? contact = await _context.Contacts.Include(c => c.ContactCategory).Include(c => c.CreatedBy).FirstOrDefaultAsync(c => c.Id == id && c.IsActive); Contact? contact = await _context.Contacts.Include(c => c.ContactCategory).Include(c => c.CreatedBy).FirstOrDefaultAsync(c => c.Id == id && c.IsActive);
@ -806,11 +800,19 @@ namespace Marco.Pms.Services.Helpers
} }
List<ContactBucketMapping>? contactBuckets = await _context.ContactBucketMappings.Where(cb => cb.ContactId == contact.Id).ToListAsync(); List<ContactBucketMapping>? contactBuckets = await _context.ContactBucketMappings.Where(cb => cb.ContactId == contact.Id).ToListAsync();
List<EmployeeBucketMapping>? employeeBuckets = await _context.EmployeeBucketMappings.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).ToListAsync(); List<EmployeeBucketMapping>? employeeBuckets = await _context.EmployeeBucketMappings.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).ToListAsync();
if (contactBuckets.Any() && employeeBuckets.Any()) if (contactBuckets.Any() && (employeeBuckets.Any() || hasAdminPermission))
{ {
List<Guid> contactBucketIds = contactBuckets.Select(cb => cb.BucketId).ToList(); List<Guid> contactBucketIds = contactBuckets.Select(cb => cb.BucketId).ToList();
List<Guid> employeeBucketIds = employeeBuckets.Select(eb => eb.BucketId).ToList(); List<Guid> employeeBucketIds = employeeBuckets.Select(eb => eb.BucketId).ToList();
List<Bucket>? buckets = await _context.Buckets.Where(b => contactBucketIds.Contains(b.Id) && employeeBucketIds.Contains(b.Id)).ToListAsync(); List<Bucket>? buckets = null;
if (hasAdminPermission)
{
buckets = await _context.Buckets.Where(b => contactBucketIds.Contains(b.Id)).ToListAsync();
}
else
{
buckets = await _context.Buckets.Where(b => contactBucketIds.Contains(b.Id) && employeeBucketIds.Contains(b.Id)).ToListAsync();
}
List<BucketVM>? bucketVMs = new List<BucketVM>(); List<BucketVM>? bucketVMs = new List<BucketVM>();
foreach (var bucket in buckets) foreach (var bucket in buckets)
{ {
@ -860,40 +862,101 @@ namespace Marco.Pms.Services.Helpers
} }
public async Task<ApiResponse<object>> GetOrganizationList() public async Task<ApiResponse<object>> GetOrganizationList()
{ {
// Step 1: Retrieve tenant and employee context
Guid tenantId = _userHelper.GetTenantId(); Guid tenantId = _userHelper.GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var organizationList = await _context.Contacts.Where(c => c.TenantId == tenantId).Select(c => c.Organization).Distinct().ToListAsync(); _logger.LogInfo("GetOrganizationList called by EmployeeId: {EmployeeId} for TenantId: {TenantId}",
_logger.LogInfo("Employee {EmployeeId} fetched list of organizations in a tenant {TenantId}", LoggedInEmployee.Id, tenantId); loggedInEmployee.Id, tenantId);
return ApiResponse<object>.SuccessResponse(organizationList, $"{organizationList.Count} records of organization names fetched from contacts", 200);
// Step 2: Fetch distinct, non-empty organization names
var organizationList = await _context.Contacts
.Where(c => c.TenantId == tenantId && !string.IsNullOrWhiteSpace(c.Organization))
.Select(c => c.Organization.Trim())
.Distinct()
.ToListAsync();
_logger.LogInfo("EmployeeId: {EmployeeId} fetched {Count} organization names from TenantId: {TenantId}",
loggedInEmployee.Id, organizationList.Count, tenantId);
// Step 3: Return success response
return ApiResponse<object>.SuccessResponse(
organizationList,
$"{organizationList.Count} records of organization names fetched from contacts",
200
);
}
public async Task<ApiResponse<object>> GetDesignationList()
{
// Step 1: Get tenant and logged-in employee details
Guid tenantId = _userHelper.GetTenantId();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
_logger.LogInfo("GetDesignationList called by EmployeeId: {EmployeeId} in TenantId: {TenantId}",
loggedInEmployee.Id, tenantId);
// Step 2: Fetch distinct, non-null designations from contacts
var designationList = await _context.Contacts
.Where(c => c.TenantId == tenantId && !string.IsNullOrWhiteSpace(c.Designation))
.Select(c => c.Designation.Trim())
.Distinct()
.ToListAsync();
_logger.LogInfo("EmployeeId: {EmployeeId} fetched {Count} designations from TenantId: {TenantId}",
loggedInEmployee.Id, designationList.Count, tenantId);
// Step 3: Return result
return ApiResponse<object>.SuccessResponse(
designationList,
$"{designationList.Count} records of designation fetched from contacts",
200
);
} }
public async Task<ApiResponse<object>> DeleteContact(Guid id, bool active) public async Task<ApiResponse<object>> DeleteContact(Guid id, bool active)
{ {
// Step 1: Get tenant and logged-in employee info
Guid tenantId = _userHelper.GetTenantId(); Guid tenantId = _userHelper.GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
if (id != Guid.Empty)
_logger.LogInfo("DeleteContact called by EmployeeId: {EmployeeId} for ContactId: {ContactId} with Active: {IsActive}",
loggedInEmployee.Id, id, active);
// Step 2: Validate contact ID
if (id == Guid.Empty)
{ {
Contact? contact = await _context.Contacts.FirstOrDefaultAsync(c => c.Id == id && c.TenantId == tenantId); _logger.LogWarning("Empty contact ID received from EmployeeId: {EmployeeId}", loggedInEmployee.Id);
if (contact == null) return ApiResponse<object>.ErrorResponse("Contact ID is empty", "Contact ID is empty", 400);
{
_logger.LogWarning("Employee with ID {LoggedInEmployeeId} tries to delete contact with ID {ContactId} is not found in database", LoggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("Contact not found", "Contact not found", 404);
}
contact.IsActive = active;
_context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog
{
RefereanceId = contact.Id,
UpdatedById = LoggedInEmployee.Id,
UpdateAt = DateTime.UtcNow
});
await _context.SaveChangesAsync();
_logger.LogInfo("Contact {ContactId} has been deleted by Employee {Employee}", id, LoggedInEmployee.Id);
return ApiResponse<object>.SuccessResponse(new { }, "Contact is deleted Successfully", 200);
} }
_logger.LogInfo("Employee ID {EmployeeId} sent an empty contact id", LoggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("Contact ID is empty", "Contact ID is empty", 400); // Step 3: Check if contact exists under current tenant
var contact = await _context.Contacts
.FirstOrDefaultAsync(c => c.Id == id && c.TenantId == tenantId);
if (contact == null)
{
_logger.LogWarning("EmployeeId {EmployeeId} attempted to delete non-existent contact Id: {ContactId}", loggedInEmployee.Id, id);
return ApiResponse<object>.ErrorResponse("Contact not found", "Contact not found", 404);
}
// Step 4: Soft delete or restore contact
contact.IsActive = active;
// Step 5: Log the update in DirectoryUpdateLog
_context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog
{
RefereanceId = contact.Id,
UpdatedById = loggedInEmployee.Id,
UpdateAt = DateTime.UtcNow
});
await _context.SaveChangesAsync();
string status = active ? "restored" : "deleted";
_logger.LogInfo("Contact {ContactId} successfully {Status} by EmployeeId: {EmployeeId}",
contact.Id, status, loggedInEmployee.Id);
// Step 6: Return success response
return ApiResponse<object>.SuccessResponse(new { }, $"Contact {status} successfully", 200);
} }
// -------------------------------- Contact Notes -------------------------------- // -------------------------------- Contact Notes --------------------------------
@ -919,9 +982,9 @@ namespace Marco.Pms.Services.Helpers
} }
// --- Permission Checks --- // --- Permission Checks ---
var hasAdminPermission = await _permissionServices.HasPermission(directoryAdmin, loggedInEmployee.Id); var hasAdminPermission = await _permissionServices.HasPermission(PermissionsMaster.DirectoryAdmin, loggedInEmployee.Id);
var hasManagerPermission = await _permissionServices.HasPermission(directoryManager, loggedInEmployee.Id); var hasManagerPermission = await _permissionServices.HasPermission(PermissionsMaster.DirectoryAdmin, loggedInEmployee.Id);
var hasUserPermission = await _permissionServices.HasPermission(directoryUser, loggedInEmployee.Id); var hasUserPermission = await _permissionServices.HasPermission(PermissionsMaster.DirectoryUser, loggedInEmployee.Id);
IQueryable<ContactNote> notesQuery = _context.ContactNotes IQueryable<ContactNote> notesQuery = _context.ContactNotes
.Include(cn => cn.UpdatedBy) .Include(cn => cn.UpdatedBy)
@ -1093,7 +1156,7 @@ namespace Marco.Pms.Services.Helpers
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
if (noteDto != null && id == noteDto.Id) if (noteDto != null && id == noteDto.Id)
{ {
Contact? contact = await _context.Contacts.FirstOrDefaultAsync(c => c.Id == noteDto.ContactId && c.IsActive && c.TenantId == tenantId); Contact? contact = await _context.Contacts.FirstOrDefaultAsync(c => c.Id == noteDto.ContactId && c.TenantId == tenantId);
if (contact != null) if (contact != null)
{ {
ContactNote? contactNote = await _context.ContactNotes.Include(cn => cn.Createdby).Include(cn => cn.Contact).FirstOrDefaultAsync(n => n.Id == noteDto.Id && n.ContactId == contact.Id && n.IsActive); ContactNote? contactNote = await _context.ContactNotes.Include(cn => cn.Createdby).Include(cn => cn.Contact).FirstOrDefaultAsync(n => n.Id == noteDto.Id && n.ContactId == contact.Id && n.IsActive);
@ -1164,22 +1227,25 @@ namespace Marco.Pms.Services.Helpers
List<EmployeeBucketMapping> employeeBuckets = await _context.EmployeeBucketMappings.Where(b => b.EmployeeId == LoggedInEmployee.Id).ToListAsync(); List<EmployeeBucketMapping> employeeBuckets = await _context.EmployeeBucketMappings.Where(b => b.EmployeeId == LoggedInEmployee.Id).ToListAsync();
var bucketIds = employeeBuckets.Select(b => b.BucketId).ToList(); 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>(); List<Bucket> bucketList = new List<Bucket>();
if (permissionIds.Contains(directoryAdmin)) if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin))
{ {
bucketList = await _context.Buckets.Include(b => b.CreatedBy).Where(b => b.TenantId == tenantId).ToListAsync(); 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(directoryManager) || permissionIds.Contains(directoryUser)) else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) || permissionIds.Contains(PermissionsMaster.DirectoryUser))
{ {
bucketList = await _context.Buckets.Include(b => b.CreatedBy).Where(b => bucketIds.Contains(b.Id) || b.CreatedByID == LoggedInEmployee.Id).ToListAsync(); bucketList = await _context.Buckets.Include(b => b.CreatedBy).Where(b => bucketIds.Contains(b.Id) || b.CreatedByID == LoggedInEmployee.Id).ToListAsync();
} }
else else
{ {
_logger.LogError("Employee {EmployeeId} attemped to access a buckets list, but do not have permission", LoggedInEmployee.Id); _logger.LogWarning("Employee {EmployeeId} attemped to access a buckets list, but do not have permission", LoggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("You don't have permission", "You don't have permission", 401); 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>(); List<AssignBucketVM> bucketVMs = new List<AssignBucketVM>();
if (bucketList.Any()) if (bucketList.Any())
{ {
@ -1191,7 +1257,11 @@ namespace Marco.Pms.Services.Helpers
var emplyeeIds = employeeBucketMappings.Select(eb => eb.EmployeeId).ToList(); var emplyeeIds = employeeBucketMappings.Select(eb => eb.EmployeeId).ToList();
List<ContactBucketMapping>? contactBuckets = contactBucketMappings.Where(cb => cb.BucketId == bucket.Id).ToList(); List<ContactBucketMapping>? contactBuckets = contactBucketMappings.Where(cb => cb.BucketId == bucket.Id).ToList();
AssignBucketVM bucketVM = bucket.ToAssignBucketVMFromBucket(); AssignBucketVM bucketVM = bucket.ToAssignBucketVMFromBucket();
bucketVM.EmployeeIds = emplyeeIds; if (bucketVM.CreatedBy != null)
{
emplyeeIds.Add(bucketVM.CreatedBy.Id);
}
bucketVM.EmployeeIds = emplyeeIds.Distinct().ToList();
bucketVM.NumberOfContacts = contactBuckets.Count; bucketVM.NumberOfContacts = contactBuckets.Count;
bucketVMs.Add(bucketVM); bucketVMs.Add(bucketVM);
} }
@ -1208,10 +1278,10 @@ namespace Marco.Pms.Services.Helpers
{ {
var assignedRoleIds = await _context.EmployeeRoleMappings.Where(r => r.EmployeeId == LoggedInEmployee.Id).Select(r => r.RoleId).ToListAsync(); var assignedRoleIds = await _context.EmployeeRoleMappings.Where(r => r.EmployeeId == LoggedInEmployee.Id).Select(r => r.RoleId).ToListAsync();
var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync(); var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync();
var demo = !permissionIds.Contains(directoryUser); var demo = !permissionIds.Contains(PermissionsMaster.DirectoryUser);
if (!permissionIds.Contains(directoryAdmin) && !permissionIds.Contains(directoryManager) && !permissionIds.Contains(directoryUser)) if (!permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && !permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && !permissionIds.Contains(PermissionsMaster.DirectoryUser))
{ {
_logger.LogError("Employee {EmployeeId} attemped to create a bucket, but do not have permission", LoggedInEmployee.Id); _logger.LogWarning("Employee {EmployeeId} attemped to create a bucket, but do not have permission", LoggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("You don't have permission", "You don't have permission", 401); return ApiResponse<object>.ErrorResponse("You don't have permission", "You don't have permission", 401);
} }
@ -1266,15 +1336,15 @@ namespace Marco.Pms.Services.Helpers
} }
Bucket? accessableBucket = null; Bucket? accessableBucket = null;
if (permissionIds.Contains(directoryAdmin)) if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin))
{ {
accessableBucket = bucket; accessableBucket = bucket;
} }
else if (permissionIds.Contains(directoryManager) && bucketIds.Contains(id)) else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && bucketIds.Contains(id))
{ {
accessableBucket = bucket; accessableBucket = bucket;
} }
else if (permissionIds.Contains(directoryUser)) else if (permissionIds.Contains(PermissionsMaster.DirectoryUser))
{ {
if (bucket.CreatedByID == LoggedInEmployee.Id) if (bucket.CreatedByID == LoggedInEmployee.Id)
{ {
@ -1283,7 +1353,7 @@ namespace Marco.Pms.Services.Helpers
} }
if (accessableBucket == null) if (accessableBucket == null)
{ {
_logger.LogError("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id); _logger.LogWarning("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id);
return ApiResponse<object>.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 401); return ApiResponse<object>.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 401);
} }
@ -1332,15 +1402,15 @@ namespace Marco.Pms.Services.Helpers
var bucketIds = employeeBuckets.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).Select(eb => eb.BucketId).ToList(); var bucketIds = employeeBuckets.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).Select(eb => eb.BucketId).ToList();
var employeeBucketIds = employeeBuckets.Select(eb => eb.EmployeeId).ToList(); var employeeBucketIds = employeeBuckets.Select(eb => eb.EmployeeId).ToList();
Bucket? accessableBucket = null; Bucket? accessableBucket = null;
if (permissionIds.Contains(directoryAdmin)) if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin))
{ {
accessableBucket = bucket; accessableBucket = bucket;
} }
else if (permissionIds.Contains(directoryManager) && bucketIds.Contains(bucketId)) else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && bucketIds.Contains(bucketId))
{ {
accessableBucket = bucket; accessableBucket = bucket;
} }
else if (permissionIds.Contains(directoryUser)) else if (permissionIds.Contains(PermissionsMaster.DirectoryUser))
{ {
if (bucket.CreatedByID == LoggedInEmployee.Id) if (bucket.CreatedByID == LoggedInEmployee.Id)
{ {
@ -1349,7 +1419,7 @@ namespace Marco.Pms.Services.Helpers
} }
if (accessableBucket == null) if (accessableBucket == null)
{ {
_logger.LogError("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id); _logger.LogWarning("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id);
return ApiResponse<object>.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 401); 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(); var employeeIds = await _context.Employees.Where(e => e.TenantId == tenantId && e.IsActive).Select(e => e.Id).ToListAsync();
@ -1369,7 +1439,7 @@ namespace Marco.Pms.Services.Helpers
_context.EmployeeBucketMappings.Add(employeeBucketMapping); _context.EmployeeBucketMappings.Add(employeeBucketMapping);
assignedEmployee += 1; assignedEmployee += 1;
} }
else else if (!assignBucket.IsActive)
{ {
EmployeeBucketMapping? employeeBucketMapping = employeeBuckets.FirstOrDefault(eb => eb.BucketId == bucketId && eb.EmployeeId == assignBucket.EmployeeId); EmployeeBucketMapping? employeeBucketMapping = employeeBuckets.FirstOrDefault(eb => eb.BucketId == bucketId && eb.EmployeeId == assignBucket.EmployeeId);
if (employeeBucketMapping != null) if (employeeBucketMapping != null)
@ -1403,7 +1473,7 @@ namespace Marco.Pms.Services.Helpers
} }
if (removededEmployee > 0) if (removededEmployee > 0)
{ {
_logger.LogError("Employee {EmployeeId} removed {conut} number of employees from bucket {BucketId}", LoggedInEmployee.Id, removededEmployee, bucketId); _logger.LogWarning("Employee {EmployeeId} removed {conut} number of employees from bucket {BucketId}", LoggedInEmployee.Id, removededEmployee, bucketId);
} }
return ApiResponse<object>.SuccessResponse(bucketVM, "Details updated successfully", 200); return ApiResponse<object>.SuccessResponse(bucketVM, "Details updated successfully", 200);
} }
@ -1433,15 +1503,15 @@ namespace Marco.Pms.Services.Helpers
var bucketIds = employeeBuckets.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).Select(eb => eb.BucketId).ToList(); var bucketIds = employeeBuckets.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).Select(eb => eb.BucketId).ToList();
Bucket? accessableBucket = null; Bucket? accessableBucket = null;
if (permissionIds.Contains(directoryAdmin)) if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin))
{ {
accessableBucket = bucket; accessableBucket = bucket;
} }
else if (permissionIds.Contains(directoryManager) && bucketIds.Contains(id)) else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && bucketIds.Contains(id))
{ {
accessableBucket = bucket; accessableBucket = bucket;
} }
else if (permissionIds.Contains(directoryUser)) else if (permissionIds.Contains(PermissionsMaster.DirectoryUser))
{ {
if (bucket.CreatedByID == LoggedInEmployee.Id) if (bucket.CreatedByID == LoggedInEmployee.Id)
{ {
@ -1450,7 +1520,7 @@ namespace Marco.Pms.Services.Helpers
} }
if (accessableBucket == null) if (accessableBucket == null)
{ {
_logger.LogError("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id); _logger.LogWarning("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id);
return ApiResponse<object>.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 401); 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) catch (Exception ex)
{ {
_logger.LogError("{Error}", ex.Message); _logger.LogError(ex, "Error occured while fetching employee by application user ID {ApplicationUserId}", ApplicationUserID);
return new Employee(); return new Employee();
} }
} }
@ -66,7 +66,7 @@ namespace MarcoBMS.Services.Helpers
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError("{Error}", ex.Message); _logger.LogError(ex, "Error occoured while filtering employees by string {SearchString} or project {ProjectId}", searchString, ProjectId ?? Guid.Empty);
return new List<EmployeeVM>(); return new List<EmployeeVM>();
} }
} }
@ -76,14 +76,14 @@ namespace MarcoBMS.Services.Helpers
try try
{ {
List<EmployeeVM> result = new List<EmployeeVM>(); List<EmployeeVM> result = new List<EmployeeVM>();
if (ProjectId != null) if (ProjectId.HasValue)
{ {
result = await (from pa in _context.ProjectAllocations.Where(c => c.ProjectId == ProjectId) result = await _context.ProjectAllocations
join em in _context.Employees.Where(c => c.TenantId == TenentId && c.IsActive == true).Include(fp => fp.JobRole) // Include Feature .Include(pa => pa.Employee)
on pa.EmployeeId equals em.Id .ThenInclude(e => e!.JobRole)
select em.ToEmployeeVMFromEmployee() .Where(c => c.ProjectId == ProjectId.Value && c.IsActive && c.Employee != null)
) .Select(pa => pa.Employee!.ToEmployeeVMFromEmployee())
.ToListAsync(); .ToListAsync();
} }
@ -102,7 +102,7 @@ namespace MarcoBMS.Services.Helpers
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError("{Error}", ex.Message); _logger.LogError(ex, "Error occured while featching list of employee by project ID {ProjectId}", ProjectId ?? Guid.Empty);
return new List<EmployeeVM>(); return new List<EmployeeVM>();
} }
} }

View File

@ -0,0 +1,214 @@
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.MongoDBModels;
using MarcoBMS.Services.Service;
using Microsoft.EntityFrameworkCore;
namespace Marco.Pms.Services.Helpers
{
public class GeneralHelper
{
private readonly IDbContextFactory<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

@ -868,7 +868,7 @@ namespace Marco.Pms.Services.Helpers
_logger.LogInfo("Contact tag master {ConatctTagId} updated successfully by employee {EmployeeId}", contactTagVm.Id, LoggedInEmployee.Id); _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); return ApiResponse<object>.SuccessResponse(contactTagVm, "Contact Tag master updated successfully", 200);
} }
_logger.LogError("Contact Tag master {ContactTagId} not found in database", id); _logger.LogWarning("Contact Tag master {ContactTagId} not found in database", id);
return ApiResponse<object>.ErrorResponse("Contact Tag master not found", "Contact tag master not found", 404); 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); _logger.LogWarning("Employee with ID {LoggedInEmployeeId} sended empty payload", LoggedInEmployee.Id);
@ -914,7 +914,7 @@ namespace Marco.Pms.Services.Helpers
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Step 2: Check permission to view master data // Step 2: Check permission to view master data
bool hasViewPermission = await _permissionService.HasPermission(View_Master, loggedInEmployee.Id); bool hasViewPermission = await _permissionService.HasPermission(PermissionsMaster.ViewMasters, loggedInEmployee.Id);
if (!hasViewPermission) if (!hasViewPermission)
{ {
_logger.LogWarning("Access denied for employeeId: {EmployeeId}", loggedInEmployee.Id); _logger.LogWarning("Access denied for employeeId: {EmployeeId}", loggedInEmployee.Id);
@ -944,7 +944,7 @@ namespace Marco.Pms.Services.Helpers
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError("Error occurred while fetching work status list : {Error}", ex.Message); _logger.LogWarning("Error occurred while fetching work status list : {Error}", ex.Message);
return ApiResponse<object>.ErrorResponse("An error occurred", "Unable to fetch work status list", 500); return ApiResponse<object>.ErrorResponse("An error occurred", "Unable to fetch work status list", 500);
} }
} }
@ -959,7 +959,7 @@ namespace Marco.Pms.Services.Helpers
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Step 2: Check if user has permission to manage master data // Step 2: Check if user has permission to manage master data
var hasManageMasterPermission = await _permissionService.HasPermission(Manage_Master, loggedInEmployee.Id); var hasManageMasterPermission = await _permissionService.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id);
if (!hasManageMasterPermission) if (!hasManageMasterPermission)
{ {
_logger.LogWarning("Access denied for employeeId: {EmployeeId}", loggedInEmployee.Id); _logger.LogWarning("Access denied for employeeId: {EmployeeId}", loggedInEmployee.Id);
@ -993,7 +993,7 @@ namespace Marco.Pms.Services.Helpers
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError("Error occurred while creating work status : {Error}", ex.Message); _logger.LogWarning("Error occurred while creating work status : {Error}", ex.Message);
return ApiResponse<object>.ErrorResponse("An error occurred", "Unable to create work status", 500); return ApiResponse<object>.ErrorResponse("An error occurred", "Unable to create work status", 500);
} }
} }
@ -1015,7 +1015,7 @@ namespace Marco.Pms.Services.Helpers
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Step 3: Check permissions // Step 3: Check permissions
var hasManageMasterPermission = await _permissionService.HasPermission(Manage_Master, loggedInEmployee.Id); var hasManageMasterPermission = await _permissionService.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id);
if (!hasManageMasterPermission) if (!hasManageMasterPermission)
{ {
_logger.LogWarning("Access denied. EmployeeId: {EmployeeId} does not have Manage Master permission.", loggedInEmployee.Id); _logger.LogWarning("Access denied. EmployeeId: {EmployeeId} does not have Manage Master permission.", loggedInEmployee.Id);
@ -1053,7 +1053,7 @@ namespace Marco.Pms.Services.Helpers
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError("Error occurred while updating work status ID: {Id} : {Error}", id, ex.Message); _logger.LogError(ex, "Error occurred while updating work status ID: {Id}", id);
return ApiResponse<object>.ErrorResponse("An error occurred", "Unable to update the work status at this time", 500); return ApiResponse<object>.ErrorResponse("An error occurred", "Unable to update the work status at this time", 500);
} }
} }
@ -1068,7 +1068,7 @@ namespace Marco.Pms.Services.Helpers
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Step 2: Check permission to manage master data // Step 2: Check permission to manage master data
var hasManageMasterPermission = await _permissionService.HasPermission(Manage_Master, loggedInEmployee.Id); var hasManageMasterPermission = await _permissionService.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id);
if (!hasManageMasterPermission) if (!hasManageMasterPermission)
{ {
_logger.LogWarning("Delete denied. EmployeeId: {EmployeeId} lacks Manage_Master permission.", loggedInEmployee.Id); _logger.LogWarning("Delete denied. EmployeeId: {EmployeeId} lacks Manage_Master permission.", loggedInEmployee.Id);
@ -1108,7 +1108,7 @@ namespace Marco.Pms.Services.Helpers
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError("Error occurred while deleting WorkStatus Id: {Id} : {Error}", id, ex.Message); _logger.LogError(ex, "Error occurred while deleting WorkStatus Id: {Id}", id);
return ApiResponse<object>.ErrorResponse("An error occurred", "Unable to delete work status", 500); return ApiResponse<object>.ErrorResponse("An error occurred", "Unable to delete work status", 500);
} }
} }

View File

@ -1,37 +0,0 @@
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Projects;
using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore;
namespace ModelServices.Helpers
{
public class ProjectHelper
{
private readonly ApplicationDbContext _context;
public ProjectHelper(ApplicationDbContext context)
{
_context = context;
}
public async Task<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

@ -1,96 +0,0 @@
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Projects;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using ModelServices.Helpers;
namespace MarcoBMS.Services.Helpers
{
public class ProjectsHelper
{
private readonly ApplicationDbContext _context;
private readonly RolesHelper _rolesHelper;
public ProjectsHelper(ApplicationDbContext context, RolesHelper rolesHelper)
{
_context = context;
_rolesHelper = rolesHelper;
}
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)
{
List<FeaturePermission> featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(LoggedInEmployee.Id);
string[] projectsId = [];
List<Project> projects = new List<Project>();
// 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 (featurePermission != null && featurePermission.Exists(c => c.Id.ToString() == "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
var projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); // Get distinct Guids
// Filter projects based on the retrieved ProjectIds
projects = await projectQuery.Where(c => projectIds.Contains(c.Id)).ToListAsync();
}
return projects;
}
}
}

View File

@ -0,0 +1,365 @@
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)
{
_context = context;
_emailSender = emailSender;
_logger = logger;
_cache = cache;
}
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);
if (project == null)
{
var projectSQL = await _context.Projects
.AsNoTracking()
.FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId);
if (projectSQL != null)
{
project = new ProjectMongoDB
{
Id = projectSQL.Id.ToString(),
Name = projectSQL.Name,
ShortName = projectSQL.ShortName,
ProjectAddress = projectSQL.ProjectAddress,
ContactPerson = projectSQL.ContactPerson
};
await _cache.AddProjectDetails(projectSQL);
}
}
if (project != null)
{
var statisticReport = new ProjectStatisticReport
{
Date = reportDate,
ProjectName = project.Name ?? "",
TimeStamp = DateTime.Now.ToString("dd-MMM-yyyy HH:mm:ss", CultureInfo.InvariantCulture)
};
// Preload relevant data
var projectAllocations = await _context.ProjectAllocations
.Include(p => p.Employee)
.Where(p => p.ProjectId == projectId && p.IsActive)
.ToListAsync();
var assignedEmployeeIds = projectAllocations.Select(p => p.EmployeeId).ToHashSet();
var attendances = await _context.Attendes
.AsNoTracking()
.Where(a => a.ProjectID == projectId && a.InTime != null && a.InTime.Value.Date == reportDate)
.ToListAsync();
var checkedInEmployeeIds = attendances.Select(a => a.EmployeeID).Distinct().ToHashSet();
var checkoutPendingIds = attendances.Where(a => a.OutTime == null).Select(a => a.EmployeeID).Distinct().ToHashSet();
var regularizationIds = attendances
.Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE)
.Select(a => a.EmployeeID).Distinct().ToHashSet();
// Preload buildings, floors, areas
List<BuildingMongoDBVM>? buildings = null;
List<FloorMongoDBVM>? floors = null;
List<WorkAreaMongoDB>? areas = null;
List<WorkItemMongoDB>? workItems = null;
// Fetch Buildings
buildings = project.Buildings
.Select(b => new BuildingMongoDBVM
{
Id = b.Id,
ProjectId = b.ProjectId,
BuildingName = b.BuildingName,
Description = b.Description
}).ToList();
if (!buildings.Any())
{
buildings = await _context.Buildings
.Where(b => b.ProjectId == projectId)
.Select(b => new BuildingMongoDBVM
{
Id = b.Id.ToString(),
ProjectId = b.ProjectId.ToString(),
BuildingName = b.Name,
Description = b.Description
})
.ToListAsync();
}
// fetch Floors
floors = project.Buildings
.SelectMany(b => b.Floors.Select(f => new FloorMongoDBVM
{
Id = f.Id.ToString(),
BuildingId = f.BuildingId,
FloorName = f.FloorName
})).ToList();
if (!floors.Any())
{
var buildingIds = buildings.Select(b => Guid.Parse(b.Id)).ToList();
floors = await _context.Floor
.Where(f => buildingIds.Contains(f.BuildingId))
.Select(f => new FloorMongoDBVM
{
Id = f.Id.ToString(),
BuildingId = f.BuildingId.ToString(),
FloorName = f.FloorName
})
.ToListAsync();
}
// fetch Work Areas
areas = project.Buildings
.SelectMany(b => b.Floors)
.SelectMany(f => f.WorkAreas).ToList();
if (!areas.Any())
{
var floorIds = floors.Select(f => Guid.Parse(f.Id)).ToList();
areas = await _context.WorkAreas
.Where(a => floorIds.Contains(a.FloorId))
.Select(wa => new WorkAreaMongoDB
{
Id = wa.Id.ToString(),
FloorId = wa.FloorId.ToString(),
AreaName = wa.AreaName,
})
.ToListAsync();
}
var areaIds = areas.Select(a => Guid.Parse(a.Id)).ToList();
// fetch Work Items
workItems = await _cache.GetWorkItemsByWorkAreaIds(areaIds);
if (workItems == null || !workItems.Any())
{
workItems = await _context.WorkItems
.Include(w => w.ActivityMaster)
.Where(w => areaIds.Contains(w.WorkAreaId))
.Select(wi => new WorkItemMongoDB
{
Id = wi.Id.ToString(),
WorkAreaId = wi.WorkAreaId.ToString(),
PlannedWork = wi.PlannedWork,
CompletedWork = wi.CompletedWork,
Description = wi.Description,
TaskDate = wi.TaskDate,
ActivityMaster = new ActivityMasterMongoDB
{
ActivityName = wi.ActivityMaster != null ? wi.ActivityMaster.ActivityName : null,
UnitOfMeasurement = wi.ActivityMaster != null ? wi.ActivityMaster.UnitOfMeasurement : null
}
})
.ToListAsync();
}
var itemIds = workItems.Select(i => Guid.Parse(i.Id)).ToList();
var tasks = await _context.TaskAllocations
.Where(t => itemIds.Contains(t.WorkItemId))
.ToListAsync();
var taskIds = tasks.Select(t => t.Id).ToList();
var taskMembers = await _context.TaskMembers
.Include(m => m.Employee)
.Where(m => taskIds.Contains(m.TaskAllocationId))
.ToListAsync();
// Aggregate data
double totalPlannedWork = workItems.Sum(w => w.PlannedWork);
double totalCompletedWork = workItems.Sum(w => w.CompletedWork);
var todayAssignedTasks = tasks.Where(t => t.AssignmentDate.Date == reportDate).ToList();
var reportPending = tasks.Where(t => t.ReportedDate == null).ToList();
double totalPlannedTask = todayAssignedTasks.Sum(t => t.PlannedTask);
double totalCompletedTask = todayAssignedTasks.Sum(t => t.CompletedTask);
var jobRoleIds = projectAllocations.Select(pa => pa.JobRoleId).ToList();
var jobRoles = await _context.JobRoles
.Where(j => j.TenantId == tenantId && jobRoleIds.Contains(j.Id))
.ToListAsync();
// Team on site
var teamOnSite = jobRoles
.Select(role =>
{
var count = projectAllocations.Count(p => p.JobRoleId == role.Id && checkedInEmployeeIds.Contains(p.EmployeeId));
return new TeamOnSite { RoleName = role.Name, NumberofEmployees = count };
})
.OrderByDescending(t => t.NumberofEmployees)
.ToList();
// Task details
var performedTasks = todayAssignedTasks.Select(task =>
{
var workItem = workItems.FirstOrDefault(w => w.Id == task.WorkItemId.ToString());
var area = areas.FirstOrDefault(a => a.Id == workItem?.WorkAreaId);
var floor = floors.FirstOrDefault(f => f.Id == area?.FloorId);
var building = buildings.FirstOrDefault(b => b.Id == floor?.BuildingId);
string activityName = workItem?.ActivityMaster?.ActivityName ?? "";
string location = $"{building?.BuildingName} > {floor?.FloorName}</span><br/><span style=\"color: gray; font-size: small; padding-left: 10px;\"> {floor?.FloorName}-{area?.AreaName}";
double pending = (workItem?.PlannedWork ?? 0) - (workItem?.CompletedWork ?? 0);
var taskTeam = taskMembers
.Where(m => m.TaskAllocationId == task.Id)
.Select(m =>
{
string name = $"{m.Employee?.FirstName ?? ""} {m.Employee?.LastName ?? ""}";
var role = jobRoles.FirstOrDefault(r => r.Id == m.Employee?.JobRoleId);
return new TaskTeam { Name = name, RoleName = role?.Name ?? "" };
})
.ToList();
return new PerformedTask
{
Activity = activityName,
Location = location,
AssignedToday = task.PlannedTask,
CompletedToday = task.CompletedTask,
Pending = pending,
Comment = task.Description,
Team = taskTeam
};
}).ToList();
// Attendance details
var performedAttendance = attendances.Select(att =>
{
var alloc = projectAllocations.FirstOrDefault(p => p.EmployeeId == att.EmployeeID);
var role = jobRoles.FirstOrDefault(r => r.Id == alloc?.JobRoleId);
string name = $"{alloc?.Employee?.FirstName ?? ""} {alloc?.Employee?.LastName ?? ""}";
return new PerformedAttendance
{
Name = name,
RoleName = role?.Name ?? "",
InTime = att.InTime ?? DateTime.UtcNow,
OutTime = att.OutTime,
Comment = att.Comment
};
}).ToList();
// Fill report
statisticReport.TodaysAttendances = checkedInEmployeeIds.Count;
statisticReport.TotalEmployees = assignedEmployeeIds.Count;
statisticReport.RegularizationPending = regularizationIds.Count;
statisticReport.CheckoutPending = checkoutPendingIds.Count;
statisticReport.TotalPlannedWork = totalPlannedWork;
statisticReport.TotalCompletedWork = totalCompletedWork;
statisticReport.TotalPlannedTask = totalPlannedTask;
statisticReport.TotalCompletedTask = totalCompletedTask;
statisticReport.CompletionStatus = totalPlannedWork > 0 ? totalCompletedWork / totalPlannedWork : 0;
statisticReport.TodaysAssignTasks = todayAssignedTasks.Count;
statisticReport.ReportPending = reportPending.Count;
statisticReport.TeamOnSite = teamOnSite;
statisticReport.PerformedTasks = performedTasks;
statisticReport.PerformedAttendance = performedAttendance;
return statisticReport;
}
return null;
}
/// <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

@ -2,37 +2,94 @@
using Marco.Pms.DataAccess.Data; using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Entitlements;
using Marco.Pms.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace MarcoBMS.Services.Helpers namespace MarcoBMS.Services.Helpers
{ {
public class RolesHelper public class RolesHelper
{ {
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
public RolesHelper(ApplicationDbContext context) private readonly CacheUpdateHelper _cache;
private readonly ILoggingService _logger;
public RolesHelper(ApplicationDbContext context, CacheUpdateHelper cache, ILoggingService logger, IDbContextFactory<ApplicationDbContext> dbContextFactory)
{ {
_context = context; _context = context;
_cache = cache;
_logger = logger;
_dbContextFactory = dbContextFactory;
} }
public async Task<List<FeaturePermission>> GetFeaturePermissionByEmployeeID(Guid EmployeeID) /// <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)
{ {
List<Guid> roleMappings = await _context.EmployeeRoleMappings.Where(c => c.EmployeeId == EmployeeID && c.IsEnabled == true).Select(c => c.RoleId).ToListAsync(); _logger.LogInfo("Fetching feature permissions for EmployeeId: {EmployeeId}", EmployeeId);
// _context.RolePermissionMappings 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);
var result = await (from rpm in _context.RolePermissionMappings // --- Step 2: Asynchronously update the cache using the DbContextFactory ---
join fp in _context.FeaturePermissions.Where(c => c.IsEnabled == true).Include(fp => fp.Feature) // Include Feature _ = Task.Run(async () =>
on rpm.FeaturePermissionId equals fp.Id {
where roleMappings.Contains(rpm.ApplicationRoleId) try
select fp) {
.ToListAsync(); // Create a NEW, short-lived DbContext instance for this background task.
await using var contextForCache = await _dbContextFactory.CreateDbContextAsync();
return result; // 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();
// return null; 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);
}
});
// --- 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>();
}
} }
public async Task<List<FeaturePermission>> GetFeaturePermissionByRoleID(Guid roleId) public async Task<List<FeaturePermission>> GetFeaturePermissionByRoleID1(Guid roleId)
{ {
List<Guid> roleMappings = await _context.RolePermissionMappings.Where(c => c.ApplicationRoleId == roleId).Select(c => c.ApplicationRoleId).ToListAsync(); List<Guid> roleMappings = await _context.RolePermissionMappings.Where(c => c.ApplicationRoleId == roleId).Select(c => c.ApplicationRoleId).ToListAsync();
@ -49,5 +106,49 @@ namespace MarcoBMS.Services.Helpers
// return null; // 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

@ -0,0 +1,68 @@
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,6 +11,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="AWSSDK.S3" Version="3.7.416.13" /> <PackageReference Include="AWSSDK.S3" Version="3.7.416.13" />
<PackageReference Include="MailKit" Version="4.9.0" /> <PackageReference Include="MailKit" Version="4.9.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.20" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.20" />
@ -44,6 +45,7 @@
<ProjectReference Include="..\Marco.Pms.DataAccess\Marco.Pms.DataAccess.csproj" /> <ProjectReference Include="..\Marco.Pms.DataAccess\Marco.Pms.DataAccess.csproj" />
<ProjectReference Include="..\Marco.Pms.Model\Marco.Pms.Model.csproj" /> <ProjectReference Include="..\Marco.Pms.Model\Marco.Pms.Model.csproj" />
<ProjectReference Include="..\Marco.Pms.Utility\Marco.Pms.Utility.csproj" /> <ProjectReference Include="..\Marco.Pms.Utility\Marco.Pms.Utility.csproj" />
<ProjectReference Include="..\Marco.Pms.CacheHelper\Marco.Pms.CacheHelper.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="EmailTemplates\**\*.html"> <Content Include="EmailTemplates\**\*.html">

View File

@ -24,7 +24,7 @@ namespace MarcoBMS.Services.Middleware
var response = context.Response; var response = context.Response;
var request = context.Request; var request = context.Request;
var tenantId = context.User.FindFirst("TenantId")?.Value; var tenantId = context.User.FindFirst("TenantId")?.Value;
string origin = request.Headers["Origin"].FirstOrDefault() ?? "";
using (LogContext.PushProperty("TenantId", tenantId)) using (LogContext.PushProperty("TenantId", tenantId))
using (LogContext.PushProperty("TraceId", context.TraceIdentifier)) using (LogContext.PushProperty("TraceId", context.TraceIdentifier))
@ -33,6 +33,8 @@ namespace MarcoBMS.Services.Middleware
using (LogContext.PushProperty("Timestamp", DateTime.UtcNow)) using (LogContext.PushProperty("Timestamp", DateTime.UtcNow))
using (LogContext.PushProperty("IpAddress", context.Connection.RemoteIpAddress?.ToString())) using (LogContext.PushProperty("IpAddress", context.Connection.RemoteIpAddress?.ToString()))
using (LogContext.PushProperty("RequestPath", request.Path)) using (LogContext.PushProperty("RequestPath", request.Path))
using (LogContext.PushProperty("Origin", origin))
try try

View File

@ -1,4 +1,4 @@
using System.Text; using Marco.Pms.CacheHelper;
using Marco.Pms.DataAccess.Data; using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Authentication; using Marco.Pms.Model.Authentication;
using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Entitlements;
@ -6,6 +6,7 @@ using Marco.Pms.Model.Utilities;
using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Helpers;
using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Hubs;
using Marco.Pms.Services.Service; using Marco.Pms.Services.Service;
using Marco.Pms.Services.Service.ServiceInterfaces;
using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Middleware; using MarcoBMS.Services.Middleware;
using MarcoBMS.Services.Service; using MarcoBMS.Services.Service;
@ -15,47 +16,35 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
using Serilog; using Serilog;
using System.Text;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
#region ======================= Service Configuration (Dependency Injection) =======================
#region Logging
// Add Serilog Configuration // Add Serilog Configuration
string? mongoConn = builder.Configuration["MongoDB:SerilogDatabaseUrl"]; string? mongoConn = builder.Configuration["MongoDB:SerilogDatabaseUrl"];
string timeString = "00:00:30"; string timeString = "00:00:30";
TimeSpan.TryParse(timeString, out TimeSpan timeSpan); TimeSpan.TryParse(timeString, out TimeSpan timeSpan);
// Add Serilog Configuration
builder.Host.UseSerilog((context, config) => builder.Host.UseSerilog((context, config) =>
{ {
config.ReadFrom.Configuration(context.Configuration) // Taking all configuration from appsetting.json config.ReadFrom.Configuration(context.Configuration)
.WriteTo.MongoDB( .WriteTo.MongoDB(
databaseUrl: mongoConn ?? string.Empty, databaseUrl: mongoConn ?? string.Empty,
collectionName: "api-logs", collectionName: "api-logs",
batchPostingLimit: 100, batchPostingLimit: 100,
period: timeSpan period: timeSpan
); );
}); });
#endregion
// Add services #region CORS (Cross-Origin Resource Sharing)
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 => builder.Services.AddCors(options =>
{ {
options.AddPolicy("Policy", policy => // A more permissive policy for development
{
if (allowedOrigins != null && allowedMethods != null && allowedHeaders != null)
{
policy.WithOrigins(allowedOrigins)
.WithMethods(allowedMethods)
.WithHeaders(allowedHeaders);
}
});
}).AddCors(options =>
{
options.AddPolicy("DevCorsPolicy", policy => options.AddPolicy("DevCorsPolicy", policy =>
{ {
policy.AllowAnyOrigin() policy.AllowAnyOrigin()
@ -63,89 +52,51 @@ builder.Services.AddCors(options =>
.AllowAnyHeader() .AllowAnyHeader()
.WithExposedHeaders("Authorization"); .WithExposedHeaders("Authorization");
}); });
});
// Add services to the container. // A stricter policy for production (loaded from config)
builder.Services.AddHostedService<StartupUserSeeder>(); var corsSettings = builder.Configuration.GetSection("Cors");
var allowedOrigins = corsSettings.GetValue<string>("AllowedOrigins")?.Split(',') ?? Array.Empty<string>();
options.AddPolicy("ProdCorsPolicy", policy =>
{
policy.WithOrigins(allowedOrigins)
.AllowAnyMethod()
.AllowAnyHeader();
});
});
#endregion
#region Core Web & Framework Services
builder.Services.AddControllers(); builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddSignalR();
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); builder.Services.AddHttpContextAccessor();
builder.Services.AddSwaggerGen(option => builder.Services.AddMemoryCache();
{ builder.Services.AddAutoMapper(typeof(Program));
option.SwaggerDoc("v1", new OpenApiInfo { Title = "Demo API", Version = "v1" }); builder.Services.AddHostedService<StartupUserSeeder>();
option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme #endregion
{
In = ParameterLocation.Header,
Description = "Please enter a valid token",
Name = "Authorization",
Type = SecuritySchemeType.Http,
BearerFormat = "JWT",
Scheme = "Bearer"
});
option.AddSecurityRequirement(new OpenApiSecurityRequirement #region Database & Identity
{ string? connString = builder.Configuration.GetConnectionString("DefaultConnectionString")
{ ?? throw new InvalidOperationException("Database connection string 'DefaultConnectionString' not found.");
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type=ReferenceType.SecurityScheme,
Id="Bearer"
}
},
new string[]{}
}
});
});
builder.Services.Configure<SmtpSettings>(builder.Configuration.GetSection("SmtpSettings")); // This single call correctly registers BOTH the DbContext (scoped) AND the IDbContextFactory (singleton).
builder.Services.AddTransient<IEmailSender, EmailSender>(); builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
options.UseMySql(connString, ServerVersion.AutoDetect(connString)));
builder.Services.Configure<AWSSettings>(builder.Configuration.GetSection("AWS")); // For uploading images to aws s3
builder.Services.AddTransient<S3UploadService>();
builder.Services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();
string? connString = builder.Configuration.GetConnectionString("DefaultConnectionString");
builder.Services.AddDbContext<ApplicationDbContext>(options => builder.Services.AddDbContext<ApplicationDbContext>(options =>
{ options.UseMySql(connString, ServerVersion.AutoDetect(connString)));
options.UseMySql(connString, ServerVersion.AutoDetect(connString));
});
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
#endregion
builder.Services.AddMemoryCache(); #region Authentication (JWT)
//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.AddSingleton<ILoggingService, LoggingService>();
builder.Services.AddHttpContextAccessor();
var jwtSettings = builder.Configuration.GetSection("Jwt").Get<JwtSettings>() var jwtSettings = builder.Configuration.GetSection("Jwt").Get<JwtSettings>()
?? throw new InvalidOperationException("JwtSettings section is missing or invalid."); ?? throw new InvalidOperationException("JwtSettings section is missing or invalid.");
if (jwtSettings != null && jwtSettings.Key != null) if (jwtSettings != null && jwtSettings.Key != null)
{ {
builder.Services.AddSingleton(jwtSettings);
builder.Services.AddAuthentication(options => builder.Services.AddAuthentication(options =>
{ {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
@ -163,71 +114,139 @@ if (jwtSettings != null && jwtSettings.Key != null)
ValidAudience = jwtSettings.Audience, ValidAudience = jwtSettings.Audience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key)) IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key))
}; };
// This event allows SignalR to get the token from the query string
options.Events = new JwtBearerEvents options.Events = new JwtBearerEvents
{ {
OnMessageReceived = context => OnMessageReceived = context =>
{ {
var accessToken = context.Request.Query["access_token"]; var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path; if (!string.IsNullOrEmpty(accessToken) && context.HttpContext.Request.Path.StartsWithSegments("/hubs/marco"))
// Match your hub route here
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs/marco"))
{ {
context.Token = accessToken; context.Token = accessToken;
} }
return Task.CompletedTask; return Task.CompletedTask;
} }
}; };
}); });
builder.Services.AddSingleton(jwtSettings);
} }
#endregion
builder.Services.AddSignalR(); #region API Documentation (Swagger)
builder.Services.AddSwaggerGen(option =>
{
option.SwaggerDoc("v1", new OpenApiInfo { Title = "Marco PMS API", Version = "v1" });
option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Description = "Please enter a valid token",
Name = "Authorization",
Type = SecuritySchemeType.Http,
BearerFormat = "JWT",
Scheme = "Bearer"
});
option.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" }
},
Array.Empty<string>()
}
});
});
#endregion
#region Application-Specific Services
// Configuration-bound services
builder.Services.Configure<SmtpSettings>(builder.Configuration.GetSection("SmtpSettings"));
builder.Services.Configure<AWSSettings>(builder.Configuration.GetSection("AWS"));
// Transient services (lightweight, created each time)
builder.Services.AddTransient<IEmailSender, EmailSender>();
builder.Services.AddTransient<S3UploadService>();
// Scoped services (one instance per HTTP request)
#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.WebHost.ConfigureKestrel(options => builder.WebHost.ConfigureKestrel(options =>
{ {
options.AddServerHeader = false; // Disable the "Server" header options.AddServerHeader = false; // Disable the "Server" header for security
}); });
#endregion
#endregion
var app = builder.Build(); 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<ExceptionHandlingMiddleware>();
app.UseMiddleware<TenantMiddleware>(); app.UseMiddleware<TenantMiddleware>();
app.UseMiddleware<LoggingMiddleware>(); 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()) if (app.Environment.IsDevelopment())
{ {
app.UseSwagger(); app.UseSwagger();
app.UseSwaggerUI(); app.UseSwaggerUI();
// Use CORS in the pipeline
app.UseCors("DevCorsPolicy");
} }
else #endregion
{
//if (app.Environment.IsProduction())
//{
// app.UseCors("ProdCorsPolicy");
//}
//app.UseCors("AllowAll"); #region Standard Middleware
app.UseCors("DevCorsPolicy"); // Common middleware for handling static content, security, and routing.
} app.UseStaticFiles(); // Enables serving static files (e.g., from wwwroot)
app.UseHttpsRedirection(); // Redirects HTTP requests to HTTPS
#endregion
app.UseStaticFiles(); // Enables serving static files #region Security (CORS, Authentication & Authorization)
// Security-related middleware must be in the correct order.
var corsPolicy = app.Environment.IsDevelopment() ? "DevCorsPolicy" : "ProdCorsPolicy";
app.UseCors(corsPolicy); // CORS must be applied before Authentication/Authorization.
//app.UseSerilogRequestLogging(); // This is Default Serilog Logging Middleware we are not using this because we're using custom logging middleware app.UseAuthentication(); // 1. Identifies who the user is.
app.UseAuthorization(); // 2. Determines what the identified user is allowed to do.
#endregion
#region Endpoint Routing (Run Last)
app.UseHttpsRedirection(); // These map incoming requests to the correct controller actions or SignalR hubs.
app.UseAuthorization();
app.MapHub<MarcoHub>("/hubs/marco");
app.MapControllers(); app.MapControllers();
app.MapHub<MarcoHub>("/hubs/marco");
#endregion
#endregion
app.Run(); app.Run();

View File

@ -150,18 +150,24 @@ namespace MarcoBMS.Services.Service
emailBody = emailBody.Replace("{{TEAM_ON_SITE}}", BuildTeamOnSiteHtml(report.TeamOnSite)); emailBody = emailBody.Replace("{{TEAM_ON_SITE}}", BuildTeamOnSiteHtml(report.TeamOnSite));
emailBody = emailBody.Replace("{{PERFORMED_TASK}}", BuildPerformedTaskHtml(report.PerformedTasks, report.Date)); emailBody = emailBody.Replace("{{PERFORMED_TASK}}", BuildPerformedTaskHtml(report.PerformedTasks, report.Date));
emailBody = emailBody.Replace("{{PERFORMED_ATTENDANCE}}", BuildPerformedAttendanceHtml(report.PerformedAttendance)); emailBody = emailBody.Replace("{{PERFORMED_ATTENDANCE}}", BuildPerformedAttendanceHtml(report.PerformedAttendance));
var subjectReplacements = new Dictionary<string, string> if (!string.IsNullOrWhiteSpace(subject))
{ {
{"DATE", date }, var subjectReplacements = new Dictionary<string, string>
{"PROJECT_NAME", report.ProjectName} {
}; {"DATE", date },
foreach (var item in subjectReplacements) {"PROJECT_NAME", report.ProjectName}
{ };
subject = subject.Replace($"{{{{{item.Key}}}}}", item.Value); foreach (var item in subjectReplacements)
{
subject = subject.Replace($"{{{{{item.Key}}}}}", item.Value);
}
string env = _configuration["environment:Title"] ?? string.Empty;
subject = CheckSubject(subject);
}
if (toEmails.Count > 0)
{
await SendEmailAsync(toEmails, subject, emailBody);
} }
string env = _configuration["environment:Title"] ?? string.Empty;
subject = CheckSubject(subject);
await SendEmailAsync(toEmails, subject, emailBody);
return emailBody; return emailBody;
} }
public async Task SendOTP(List<string> toEmails, string emailBody, string name, string otp, string subject) public async Task SendOTP(List<string> toEmails, string emailBody, string name, string otp, string subject)

View File

@ -1,12 +1,11 @@
using Serilog.Context; namespace MarcoBMS.Services.Service
namespace MarcoBMS.Services.Service
{ {
public interface ILoggingService public interface ILoggingService
{ {
void LogInfo(string? message, params object[]? args); void LogInfo(string? message, params object[]? args);
void LogDebug(string? message, params object[]? args);
void LogWarning(string? message, params object[]? args); void LogWarning(string? message, params object[]? args);
void LogError(string? message, params object[]? args); void LogError(Exception? ex, string? message, params object[]? args);
} }
} }

View File

@ -11,17 +11,18 @@ namespace MarcoBMS.Services.Service
_logger = logger; _logger = logger;
} }
public void LogError(string? message, params object[]? args) public void LogError(Exception? ex, string? message, params object[]? args)
{ {
using (LogContext.PushProperty("LogLevel", "Error")) using (LogContext.PushProperty("LogLevel", "Error"))
if (args != null) if (args != null)
{ {
_logger.LogError(message, args); _logger.LogError(ex, message, args);
} }
else { else
_logger.LogError(message); {
_logger.LogError(ex, message);
} }
} }
public void LogInfo(string? message, params object[]? args) public void LogInfo(string? message, params object[]? args)
{ {
@ -35,6 +36,18 @@ namespace MarcoBMS.Services.Service
_logger.LogInformation(message); _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) public void LogWarning(string? message, params object[]? args)
{ {
@ -49,6 +62,5 @@ namespace MarcoBMS.Services.Service
} }
} }
} }
} }

View File

@ -1,7 +1,7 @@
using Marco.Pms.DataAccess.Data; using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Employees; using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Projects; using Marco.Pms.Services.Helpers;
using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Helpers;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -11,42 +11,50 @@ namespace Marco.Pms.Services.Service
{ {
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
private readonly RolesHelper _rolesHelper; private readonly RolesHelper _rolesHelper;
private readonly ProjectsHelper _projectsHelper; private readonly CacheUpdateHelper _cache;
public PermissionServices(ApplicationDbContext context, RolesHelper rolesHelper, ProjectsHelper projectsHelper) public PermissionServices(ApplicationDbContext context, RolesHelper rolesHelper, CacheUpdateHelper cache)
{ {
_context = context; _context = context;
_rolesHelper = rolesHelper; _rolesHelper = rolesHelper;
_projectsHelper = projectsHelper; _cache = cache;
} }
public async Task<bool> HasPermission(Guid featurePermissionId, Guid employeeId) public async Task<bool> HasPermission(Guid featurePermissionId, Guid employeeId)
{ {
var hasPermission = await _context.EmployeeRoleMappings var featurePermissionIds = await _cache.GetPermissions(employeeId);
.Where(er => er.EmployeeId == employeeId) if (featurePermissionIds == null)
.Select(er => er.RoleId) {
.Distinct() List<FeaturePermission> featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeId(employeeId);
.AnyAsync(roleId => _context.RolePermissionMappings featurePermissionIds = featurePermission.Select(fp => fp.Id).ToList();
.Any(rp => rp.FeaturePermissionId == featurePermissionId && rp.ApplicationRoleId == roleId)); }
var hasPermission = featurePermissionIds.Contains(featurePermissionId);
return hasPermission; return hasPermission;
} }
public async Task<bool> HasProjectPermission(Employee emp, string projectId) public async Task<bool> HasProjectPermission(Employee LoggedInEmployee, Guid projectId)
{ {
List<FeaturePermission> featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(emp.Id); var employeeId = LoggedInEmployee.Id;
string[] projectsId = []; var projectIds = await _cache.GetProjects(employeeId);
/* User with permission manage project can see all projects */ if (projectIds == null)
if (featurePermission != null && featurePermission.Exists(c => c.Id.ToString() == "172fc9b6-755b-4f62-ab26-55c34a330614"))
{ {
List<Project> projects = await _projectsHelper.GetAllProjectByTanentID(emp.TenantId); var hasPermission = await HasPermission(PermissionsMaster.ManageProject, employeeId);
projectsId = projects.Select(c => c.Id.ToString()).ToArray(); 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);
} }
else return projectIds.Contains(projectId);
{
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 System.IdentityModel.Tokens.Jwt; using Marco.Pms.DataAccess.Data;
using System.Security.Claims;
using System.Text;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Authentication; using Marco.Pms.Model.Authentication;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
#nullable disable #nullable disable
namespace MarcoBMS.Services.Service namespace MarcoBMS.Services.Service
@ -94,7 +94,7 @@ namespace MarcoBMS.Services.Service
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError("{Error}", ex.Message); _logger.LogError(ex, "Error occured while creating new JWT token for user {UserId}", userId);
throw; throw;
} }
} }
@ -132,7 +132,7 @@ namespace MarcoBMS.Services.Service
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError("Error creating MPIN token for userId: {UserId}, tenantId: {TenantId}, error : {Error}", userId, tenantId, ex.Message); _logger.LogError(ex, "Error creating MPIN token for userId: {UserId}, tenantId: {TenantId}", userId, tenantId);
throw; throw;
} }
} }
@ -218,7 +218,7 @@ namespace MarcoBMS.Services.Service
catch (Exception ex) catch (Exception ex)
{ {
// Token is invalid // Token is invalid
Console.WriteLine($"Token validation failed: {ex.Message}"); _logger.LogError(ex, "Token validation failed");
return null; return null;
} }
} }

View File

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

View File

@ -0,0 +1,35 @@
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

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

View File

@ -0,0 +1,29 @@
using Marco.Pms.Services.Hubs;
using Marco.Pms.Services.Service.ServiceInterfaces;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.SignalR;
namespace Marco.Pms.Services.Service
{
public class SignalRService : ISignalRService
{
private readonly IHubContext<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");
}
}
}
}

View File

@ -47,6 +47,7 @@
"BucketName": "testenv-marco-pms-documents" "BucketName": "testenv-marco-pms-documents"
}, },
"MongoDB": { "MongoDB": {
"SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs" "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs",
"ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches?socketTimeoutMS=500&serverSelectionTimeoutMS=500&connectTimeoutMS=500"
} }
} }

View File

@ -6,7 +6,7 @@
}, },
"Environment": { "Environment": {
"Name": "Production", "Name": "Production",
"Title": "" "Title": ""
}, },
"ConnectionStrings": { "ConnectionStrings": {
"DefaultConnectionString": "Server=147.93.98.152;User ID=devuser;Password=AppUser@123$;Database=MarcoBMS1" "DefaultConnectionString": "Server=147.93.98.152;User ID=devuser;Password=AppUser@123$;Database=MarcoBMS1"
@ -40,6 +40,7 @@
"BucketName": "testenv-marco-pms-documents" "BucketName": "testenv-marco-pms-documents"
}, },
"MongoDB": { "MongoDB": {
"SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs" "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs",
"ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches"
} }
} }

View File

@ -11,6 +11,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Marco.Pms.Utility", "Marco.
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Marco.Pms.Services", "Marco.Pms.Services\Marco.Pms.Services.csproj", "{27A83653-5B7F-4135-9886-01594D54AFAE}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Marco.Pms.Services", "Marco.Pms.Services\Marco.Pms.Services.csproj", "{27A83653-5B7F-4135-9886-01594D54AFAE}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Marco.Pms.CacheHelper", "Marco.Pms.CacheHelper\Marco.Pms.CacheHelper.csproj", "{1A105C22-4ED7-4F54-8834-6923DDD96852}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -33,6 +35,10 @@ Global
{27A83653-5B7F-4135-9886-01594D54AFAE}.Debug|Any CPU.Build.0 = Debug|Any CPU {27A83653-5B7F-4135-9886-01594D54AFAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{27A83653-5B7F-4135-9886-01594D54AFAE}.Release|Any CPU.ActiveCfg = Release|Any CPU {27A83653-5B7F-4135-9886-01594D54AFAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{27A83653-5B7F-4135-9886-01594D54AFAE}.Release|Any CPU.Build.0 = Release|Any CPU {27A83653-5B7F-4135-9886-01594D54AFAE}.Release|Any CPU.Build.0 = Release|Any CPU
{1A105C22-4ED7-4F54-8834-6923DDD96852}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1A105C22-4ED7-4F54-8834-6923DDD96852}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1A105C22-4ED7-4F54-8834-6923DDD96852}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1A105C22-4ED7-4F54-8834-6923DDD96852}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE