Ashutosh_Refactor #107
3
.gitignore
vendored
3
.gitignore
vendored
@ -361,3 +361,6 @@ MigrationBackup/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# Sonar
|
||||
/.sonarqube
|
@ -1,5 +1,4 @@
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Model.MongoDBModels;
|
||||
using Marco.Pms.Model.MongoDBModels;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using MongoDB.Driver;
|
||||
@ -8,41 +7,21 @@ namespace Marco.Pms.CacheHelper
|
||||
{
|
||||
public class EmployeeCache
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
//private readonly IMongoDatabase _mongoDB;
|
||||
private readonly IMongoCollection<EmployeePermissionMongoDB> _collection;
|
||||
public EmployeeCache(ApplicationDbContext context, IConfiguration configuration)
|
||||
public EmployeeCache(IConfiguration configuration)
|
||||
{
|
||||
var connectionString = configuration["MongoDB:ConnectionString"];
|
||||
_context = context;
|
||||
var mongoUrl = new MongoUrl(connectionString);
|
||||
var client = new MongoClient(mongoUrl); // Your MongoDB connection string
|
||||
var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name
|
||||
_collection = mongoDB.GetCollection<EmployeePermissionMongoDB>("EmployeeProfile");
|
||||
}
|
||||
public async Task<bool> AddApplicationRoleToCache(Guid employeeId, List<Guid> roleIds)
|
||||
public async Task<bool> AddApplicationRoleToCache(Guid employeeId, List<string> newRoleIds, List<string> newPermissionIds)
|
||||
{
|
||||
// 1. Guard Clause: Avoid unnecessary database work if there are no roles to add.
|
||||
if (roleIds == null || !roleIds.Any())
|
||||
{
|
||||
return false; // Nothing to add, so the operation did not result in a change.
|
||||
}
|
||||
|
||||
// 2. Perform database queries concurrently for better performance.
|
||||
var employeeIdString = employeeId.ToString();
|
||||
|
||||
Task<List<string>> getPermissionIdsTask = _context.RolePermissionMappings
|
||||
.Where(rp => roleIds.Contains(rp.ApplicationRoleId))
|
||||
.Select(p => p.FeaturePermissionId.ToString())
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
// 3. Prepare role IDs in parallel with the database query.
|
||||
var newRoleIds = roleIds.Select(r => r.ToString()).ToList();
|
||||
|
||||
// 4. Await the database query result.
|
||||
var newPermissionIds = await getPermissionIdsTask;
|
||||
|
||||
// 5. Build a single, efficient update operation.
|
||||
var filter = Builders<EmployeePermissionMongoDB>.Filter.Eq(e => e.Id, employeeIdString);
|
||||
|
||||
@ -54,6 +33,8 @@ namespace Marco.Pms.CacheHelper
|
||||
|
||||
var result = await _collection.UpdateOneAsync(filter, update, options);
|
||||
|
||||
await InitializeCollectionAsync();
|
||||
|
||||
// 6. Return a more accurate result indicating success for both updates and upserts.
|
||||
// The operation is successful if an existing document was modified OR a new one was created.
|
||||
return result.IsAcknowledged && (result.ModifiedCount > 0 || result.UpsertedId != null);
|
||||
@ -72,6 +53,7 @@ namespace Marco.Pms.CacheHelper
|
||||
{
|
||||
return false;
|
||||
}
|
||||
await InitializeCollectionAsync();
|
||||
return true;
|
||||
}
|
||||
public async Task<List<Guid>> GetProjectsFromCache(Guid employeeId)
|
||||
@ -118,7 +100,7 @@ namespace Marco.Pms.CacheHelper
|
||||
|
||||
var result = await _collection.UpdateOneAsync(filter, update);
|
||||
|
||||
if (result.MatchedCount == 0)
|
||||
if (result.ModifiedCount == 0)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
@ -140,16 +122,10 @@ namespace Marco.Pms.CacheHelper
|
||||
public async Task<bool> ClearAllProjectIdsByPermissionIdFromCache(Guid permissionId)
|
||||
{
|
||||
var filter = Builders<EmployeePermissionMongoDB>.Filter.AnyEq(e => e.PermissionIds, permissionId.ToString());
|
||||
var update = Builders<EmployeePermissionMongoDB>.Update.Set(e => e.ProjectIds, new List<string>());
|
||||
|
||||
var 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;
|
||||
var result = await _collection.UpdateManyAsync(filter, update).ConfigureAwait(false);
|
||||
return result.IsAcknowledged && result.ModifiedCount > 0;
|
||||
}
|
||||
public async Task<bool> RemoveRoleIdFromCache(Guid employeeId, Guid roleId)
|
||||
{
|
||||
@ -198,5 +174,31 @@ namespace Marco.Pms.CacheHelper
|
||||
|
||||
return true;
|
||||
}
|
||||
public async Task<bool> ClearAllEmployeesFromCache()
|
||||
{
|
||||
var result = await _collection.DeleteManyAsync(FilterDefinition<EmployeePermissionMongoDB>.Empty);
|
||||
|
||||
if (result.DeletedCount == 0)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// A private method to handle the one-time setup of the collection's indexes.
|
||||
private async Task InitializeCollectionAsync()
|
||||
{
|
||||
// 1. Define the TTL (Time-To-Live) index on the 'ExpireAt' field.
|
||||
var indexKeys = Builders<EmployeePermissionMongoDB>.IndexKeys.Ascending(x => x.ExpireAt);
|
||||
var indexOptions = new CreateIndexOptions
|
||||
{
|
||||
// This tells MongoDB to automatically delete documents when their 'ExpireAt' time is reached.
|
||||
ExpireAfter = TimeSpan.FromSeconds(0)
|
||||
};
|
||||
var indexModel = new CreateIndexModel<EmployeePermissionMongoDB>(indexKeys, indexOptions);
|
||||
|
||||
// 2. Create the index. This is an idempotent operation if the index already exists.
|
||||
// Use CreateOneAsync since we are only creating a single index.
|
||||
await _collection.Indexes.CreateOneAsync(indexModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,159 +11,62 @@ namespace Marco.Pms.CacheHelper
|
||||
{
|
||||
public class ProjectCache
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly IMongoCollection<ProjectMongoDB> _projetCollection;
|
||||
private readonly IMongoCollection<ProjectMongoDB> _projectCollection;
|
||||
private readonly IMongoCollection<WorkItemMongoDB> _taskCollection;
|
||||
public ProjectCache(ApplicationDbContext context, IConfiguration configuration)
|
||||
{
|
||||
var connectionString = configuration["MongoDB:ConnectionString"];
|
||||
_context = context;
|
||||
var mongoUrl = new MongoUrl(connectionString);
|
||||
var client = new MongoClient(mongoUrl); // Your MongoDB connection string
|
||||
var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name
|
||||
_projetCollection = mongoDB.GetCollection<ProjectMongoDB>("ProjectDetails");
|
||||
_projectCollection = mongoDB.GetCollection<ProjectMongoDB>("ProjectDetails");
|
||||
_taskCollection = mongoDB.GetCollection<WorkItemMongoDB>("WorkItemDetails");
|
||||
}
|
||||
public async Task AddProjectDetailsToCache(Project project)
|
||||
{
|
||||
//_logger.LogInfo("[AddProjectDetails] Initiated for ProjectId: {ProjectId}", project.Id);
|
||||
|
||||
var projectDetails = new ProjectMongoDB
|
||||
#region=================================================================== Project Cache Helper ===================================================================
|
||||
|
||||
public async Task AddProjectDetailsToCache(ProjectMongoDB projectDetails)
|
||||
{
|
||||
Id = project.Id.ToString(),
|
||||
Name = project.Name,
|
||||
ShortName = project.ShortName,
|
||||
ProjectAddress = project.ProjectAddress,
|
||||
StartDate = project.StartDate,
|
||||
EndDate = project.EndDate,
|
||||
ContactPerson = project.ContactPerson
|
||||
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);
|
||||
|
||||
// Get project status
|
||||
var status = await _context.StatusMasters
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(s => s.Id == project.ProjectStatusId);
|
||||
|
||||
projectDetails.ProjectStatus = new StatusMasterMongoDB
|
||||
}
|
||||
public async Task AddProjectDetailsListToCache(List<ProjectMongoDB> projectDetailsList)
|
||||
{
|
||||
Id = status?.Id.ToString(),
|
||||
Status = status?.Status
|
||||
// 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);
|
||||
|
||||
// Get project team size
|
||||
var teamSize = await _context.ProjectAllocations
|
||||
.AsNoTracking()
|
||||
.CountAsync(pa => pa.ProjectId == project.Id && pa.IsActive);
|
||||
|
||||
projectDetails.TeamSize = teamSize;
|
||||
|
||||
// Fetch related infrastructure in parallel
|
||||
var buildings = await _context.Buildings
|
||||
.AsNoTracking()
|
||||
.Where(b => b.ProjectId == project.Id)
|
||||
.ToListAsync();
|
||||
var buildingIds = buildings.Select(b => b.Id).ToList();
|
||||
|
||||
var floors = await _context.Floor
|
||||
.AsNoTracking()
|
||||
.Where(f => buildingIds.Contains(f.BuildingId))
|
||||
.ToListAsync();
|
||||
|
||||
var floorIds = floors.Select(f => f.Id).ToList();
|
||||
|
||||
var workAreas = await _context.WorkAreas
|
||||
.AsNoTracking()
|
||||
.Where(wa => floorIds.Contains(wa.FloorId))
|
||||
.ToListAsync();
|
||||
var workAreaIds = workAreas.Select(wa => wa.Id).ToList();
|
||||
|
||||
var workItems = await _context.WorkItems
|
||||
.Where(wi => workAreaIds.Contains(wi.WorkAreaId))
|
||||
.ToListAsync();
|
||||
|
||||
double totalPlannedWork = 0, totalCompletedWork = 0;
|
||||
|
||||
var buildingMongoList = new List<BuildingMongoDB>();
|
||||
|
||||
foreach (var building in buildings)
|
||||
{
|
||||
double buildingPlanned = 0, buildingCompleted = 0;
|
||||
var buildingFloors = floors.Where(f => f.BuildingId == building.Id).ToList();
|
||||
|
||||
var floorMongoList = new List<FloorMongoDB>();
|
||||
foreach (var floor in buildingFloors)
|
||||
{
|
||||
double floorPlanned = 0, floorCompleted = 0;
|
||||
var floorWorkAreas = workAreas.Where(wa => wa.FloorId == floor.Id).ToList();
|
||||
|
||||
var workAreaMongoList = new List<WorkAreaMongoDB>();
|
||||
foreach (var wa in floorWorkAreas)
|
||||
{
|
||||
var items = workItems.Where(wi => wi.WorkAreaId == wa.Id).ToList();
|
||||
double waPlanned = items.Sum(wi => wi.PlannedWork);
|
||||
double waCompleted = items.Sum(wi => wi.CompletedWork);
|
||||
|
||||
workAreaMongoList.Add(new WorkAreaMongoDB
|
||||
{
|
||||
Id = wa.Id.ToString(),
|
||||
FloorId = wa.FloorId.ToString(),
|
||||
AreaName = wa.AreaName,
|
||||
PlannedWork = waPlanned,
|
||||
CompletedWork = waCompleted
|
||||
});
|
||||
|
||||
floorPlanned += waPlanned;
|
||||
floorCompleted += waCompleted;
|
||||
// 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);
|
||||
}
|
||||
|
||||
floorMongoList.Add(new FloorMongoDB
|
||||
public async Task<bool> UpdateProjectDetailsOnlyToCache(Project project, StatusMaster projectStatus)
|
||||
{
|
||||
Id = floor.Id.ToString(),
|
||||
BuildingId = floor.BuildingId.ToString(),
|
||||
FloorName = floor.FloorName,
|
||||
PlannedWork = floorPlanned,
|
||||
CompletedWork = floorCompleted,
|
||||
WorkAreas = workAreaMongoList
|
||||
});
|
||||
|
||||
buildingPlanned += floorPlanned;
|
||||
buildingCompleted += floorCompleted;
|
||||
}
|
||||
|
||||
buildingMongoList.Add(new BuildingMongoDB
|
||||
{
|
||||
Id = building.Id.ToString(),
|
||||
ProjectId = building.ProjectId.ToString(),
|
||||
BuildingName = building.Name,
|
||||
Description = building.Description,
|
||||
PlannedWork = buildingPlanned,
|
||||
CompletedWork = buildingCompleted,
|
||||
Floors = floorMongoList
|
||||
});
|
||||
|
||||
totalPlannedWork += buildingPlanned;
|
||||
totalCompletedWork += buildingCompleted;
|
||||
}
|
||||
|
||||
projectDetails.Buildings = buildingMongoList;
|
||||
projectDetails.PlannedWork = totalPlannedWork;
|
||||
projectDetails.CompletedWork = totalCompletedWork;
|
||||
|
||||
await _projetCollection.InsertOneAsync(projectDetails);
|
||||
//_logger.LogInfo("[AddProjectDetails] Project details inserted in MongoDB for ProjectId: {ProjectId}", project.Id);
|
||||
}
|
||||
public async Task<bool> UpdateProjectDetailsOnlyToCache(Project project)
|
||||
{
|
||||
//_logger.LogInfo("Starting update for project: {ProjectId}", project.Id);
|
||||
|
||||
var projectStatus = await _context.StatusMasters
|
||||
.FirstOrDefaultAsync(s => s.Id == project.ProjectStatusId);
|
||||
|
||||
if (projectStatus == null)
|
||||
{
|
||||
//_logger.LogWarning("StatusMaster not found for ProjectStatusId: {StatusId}", project.ProjectStatusId);
|
||||
}
|
||||
|
||||
// Build the update definition
|
||||
var updates = Builders<ProjectMongoDB>.Update.Combine(
|
||||
Builders<ProjectMongoDB>.Update.Set(r => r.Name, project.Name),
|
||||
@ -171,8 +74,8 @@ namespace Marco.Pms.CacheHelper
|
||||
Builders<ProjectMongoDB>.Update.Set(r => r.ShortName, project.ShortName),
|
||||
Builders<ProjectMongoDB>.Update.Set(r => r.ProjectStatus, new StatusMasterMongoDB
|
||||
{
|
||||
Id = projectStatus?.Id.ToString(),
|
||||
Status = projectStatus?.Status
|
||||
Id = projectStatus.Id.ToString(),
|
||||
Status = projectStatus.Status
|
||||
}),
|
||||
Builders<ProjectMongoDB>.Update.Set(r => r.StartDate, project.StartDate),
|
||||
Builders<ProjectMongoDB>.Update.Set(r => r.EndDate, project.EndDate),
|
||||
@ -180,18 +83,16 @@ namespace Marco.Pms.CacheHelper
|
||||
);
|
||||
|
||||
// Perform the update
|
||||
var result = await _projetCollection.UpdateOneAsync(
|
||||
var result = await _projectCollection.UpdateOneAsync(
|
||||
filter: r => r.Id == project.Id.ToString(),
|
||||
update: updates
|
||||
);
|
||||
|
||||
if (result.MatchedCount == 0)
|
||||
{
|
||||
//_logger.LogWarning("No project matched in MongoDB for update. ProjectId: {ProjectId}", project.Id);
|
||||
return false;
|
||||
}
|
||||
|
||||
//_logger.LogInfo("Project {ProjectId} successfully updated in MongoDB", project.Id);
|
||||
return true;
|
||||
}
|
||||
public async Task<ProjectMongoDB?> GetProjectDetailsFromCache(Guid projectId)
|
||||
@ -201,34 +102,56 @@ namespace Marco.Pms.CacheHelper
|
||||
var filter = Builders<ProjectMongoDB>.Filter.Eq(p => p.Id, projectId.ToString());
|
||||
var projection = Builders<ProjectMongoDB>.Projection.Exclude(p => p.Buildings);
|
||||
|
||||
//_logger.LogInfo("Fetching project details for ProjectId: {ProjectId} from MongoDB", projectId);
|
||||
|
||||
// Perform query
|
||||
var project = await _projetCollection
|
||||
var project = await _projectCollection
|
||||
.Find(filter)
|
||||
.Project<ProjectMongoDB>(projection)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (project == null)
|
||||
{
|
||||
//_logger.LogWarning("No project found in MongoDB for ProjectId: {ProjectId}", projectId);
|
||||
return null;
|
||||
}
|
||||
|
||||
//_logger.LogInfo("Successfully fetched project details (excluding Buildings) for ProjectId: {ProjectId}", projectId);
|
||||
return project;
|
||||
}
|
||||
public async Task<List<ProjectMongoDB>?> GetProjectDetailsListFromCache(List<Guid> projectIds)
|
||||
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 _projetCollection
|
||||
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();
|
||||
@ -249,15 +172,12 @@ namespace Marco.Pms.CacheHelper
|
||||
var filter = Builders<ProjectMongoDB>.Filter.Eq(p => p.Id, stringProjectId);
|
||||
var update = Builders<ProjectMongoDB>.Update.Push("Buildings", buildingMongo);
|
||||
|
||||
var result = await _projetCollection.UpdateOneAsync(filter, update);
|
||||
var result = await _projectCollection.UpdateOneAsync(filter, update);
|
||||
|
||||
if (result.MatchedCount == 0)
|
||||
{
|
||||
//_logger.LogWarning("Project not found while adding building. ProjectId: {ProjectId}", projectId);
|
||||
return;
|
||||
}
|
||||
|
||||
//_logger.LogInfo("Building {BuildingId} added to project {ProjectId}", building.Id, projectId);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -279,15 +199,12 @@ namespace Marco.Pms.CacheHelper
|
||||
);
|
||||
|
||||
var update = Builders<ProjectMongoDB>.Update.Push("Buildings.$.Floors", floorMongo);
|
||||
var result = await _projetCollection.UpdateOneAsync(filter, update);
|
||||
var result = await _projectCollection.UpdateOneAsync(filter, update);
|
||||
|
||||
if (result.MatchedCount == 0)
|
||||
{
|
||||
//_logger.LogWarning("Project or building not found while adding floor. ProjectId: {ProjectId}, BuildingId: {BuildingId}", projectId, floor.BuildingId);
|
||||
return;
|
||||
}
|
||||
|
||||
//_logger.LogInfo("Floor {FloorId} added to building {BuildingId} in project {ProjectId}", floor.Id, floor.BuildingId, projectId);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -313,20 +230,14 @@ namespace Marco.Pms.CacheHelper
|
||||
var update = Builders<ProjectMongoDB>.Update.Push("Buildings.$[b].Floors.$[f].WorkAreas", workAreaMongo);
|
||||
var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters };
|
||||
|
||||
var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions);
|
||||
var result = await _projectCollection.UpdateOneAsync(filter, update, updateOptions);
|
||||
|
||||
if (result.MatchedCount == 0)
|
||||
{
|
||||
//_logger.LogWarning("Project or nested structure not found while adding work area. ProjectId: {ProjectId}, BuildingId: {BuildingId}, FloorId: {FloorId}", projectId, buildingId, workArea.FloorId);
|
||||
return;
|
||||
}
|
||||
|
||||
//_logger.LogInfo("WorkArea {WorkAreaId} added to floor {FloorId} in building {BuildingId}, ProjectId: {ProjectId}", workArea.Id, workArea.FloorId, buildingId, projectId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback case when no valid data was passed
|
||||
//_logger.LogWarning("No valid infra data provided to add for ProjectId: {ProjectId}", projectId);
|
||||
}
|
||||
public async Task<bool> UpdateBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId)
|
||||
{
|
||||
@ -345,15 +256,13 @@ namespace Marco.Pms.CacheHelper
|
||||
Builders<ProjectMongoDB>.Update.Set("Buildings.$.Description", building.Description)
|
||||
);
|
||||
|
||||
var result = await _projetCollection.UpdateOneAsync(filter, update);
|
||||
var result = await _projectCollection.UpdateOneAsync(filter, update);
|
||||
|
||||
if (result.MatchedCount == 0)
|
||||
{
|
||||
//_logger.LogWarning("Update failed: Project or Building not found. ProjectId: {ProjectId}, BuildingId: {BuildingId}", projectId, building.Id);
|
||||
return false;
|
||||
}
|
||||
|
||||
//_logger.LogInfo("Building {BuildingId} updated successfully in project {ProjectId}", building.Id, projectId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -370,15 +279,12 @@ namespace Marco.Pms.CacheHelper
|
||||
var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters };
|
||||
var filter = Builders<ProjectMongoDB>.Filter.Eq(p => p.Id, stringProjectId);
|
||||
|
||||
var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions);
|
||||
var result = await _projectCollection.UpdateOneAsync(filter, update, updateOptions);
|
||||
|
||||
if (result.MatchedCount == 0)
|
||||
{
|
||||
//_logger.LogWarning("Update failed: Project or Floor not found. ProjectId: {ProjectId}, BuildingId: {BuildingId}, FloorId: {FloorId}", projectId, floor.BuildingId, floor.Id);
|
||||
return false;
|
||||
}
|
||||
|
||||
//_logger.LogInfo("Floor {FloorId} updated successfully in Building {BuildingId}, ProjectId: {ProjectId}", floor.Id, floor.BuildingId, projectId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -396,21 +302,14 @@ namespace Marco.Pms.CacheHelper
|
||||
var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters };
|
||||
var filter = Builders<ProjectMongoDB>.Filter.Eq(p => p.Id, stringProjectId);
|
||||
|
||||
var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions);
|
||||
var result = await _projectCollection.UpdateOneAsync(filter, update, updateOptions);
|
||||
|
||||
if (result.MatchedCount == 0)
|
||||
{
|
||||
//_logger.LogWarning("Update failed: Project or WorkArea not found. ProjectId: {ProjectId}, BuildingId: {BuildingId}, FloorId: {FloorId}, WorkAreaId: {WorkAreaId}",
|
||||
//projectId, buildingId, workArea.FloorId, workArea.Id);
|
||||
return false;
|
||||
}
|
||||
|
||||
//_logger.LogInfo("WorkArea {WorkAreaId} updated successfully in Floor {FloorId}, Building {BuildingId}, ProjectId: {ProjectId}",
|
||||
//workArea.Id, workArea.FloorId, buildingId, projectId);
|
||||
return true;
|
||||
}
|
||||
|
||||
//_logger.LogWarning("No update performed. Missing or invalid data for ProjectId: {ProjectId}", projectId);
|
||||
return false;
|
||||
}
|
||||
public async Task<List<BuildingMongoDB>?> GetBuildingInfraFromCache(Guid projectId)
|
||||
@ -420,26 +319,17 @@ namespace Marco.Pms.CacheHelper
|
||||
var filter = Builders<ProjectMongoDB>.Filter.Eq(p => p.Id, projectId.ToString());
|
||||
|
||||
// Project only the "Buildings" field from the document
|
||||
var buildings = await _projetCollection
|
||||
var buildings = await _projectCollection
|
||||
.Find(filter)
|
||||
.Project(p => p.Buildings)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
//if (buildings == null)
|
||||
//{
|
||||
// _logger.LogWarning("No building infrastructure found for ProjectId: {ProjectId}", projectId);
|
||||
//}
|
||||
//else
|
||||
//{
|
||||
// _logger.LogInfo("Fetched {Count} buildings for ProjectId: {ProjectId}", buildings.Count, projectId);
|
||||
//}
|
||||
|
||||
return buildings;
|
||||
}
|
||||
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 _projetCollection.Find(filter).FirstOrDefaultAsync();
|
||||
var project = await _projectCollection.Find(filter).FirstOrDefaultAsync();
|
||||
|
||||
string? selectedBuildingId = null;
|
||||
string? selectedFloorId = null;
|
||||
@ -477,7 +367,7 @@ namespace Marco.Pms.CacheHelper
|
||||
.Inc("Buildings.$[b].CompletedWork", completedWork)
|
||||
.Inc("PlannedWork", plannedWork)
|
||||
.Inc("CompletedWork", completedWork);
|
||||
var result = await _projetCollection.UpdateOneAsync(filter, update, updateOptions);
|
||||
var result = await _projectCollection.UpdateOneAsync(filter, update, updateOptions);
|
||||
|
||||
}
|
||||
public async Task<WorkAreaInfoMongoDB?> GetBuildingAndFloorByWorkAreaIdFromCache(Guid workAreaId)
|
||||
@ -517,11 +407,16 @@ namespace Marco.Pms.CacheHelper
|
||||
{ "WorkArea", "$Buildings.Floors.WorkAreas" }
|
||||
})
|
||||
};
|
||||
var result = await _projetCollection.Aggregate<WorkAreaInfoMongoDB>(pipeline).FirstOrDefaultAsync();
|
||||
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();
|
||||
@ -533,48 +428,22 @@ namespace Marco.Pms.CacheHelper
|
||||
|
||||
return workItems;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------- WorkItem -------------------------------------------------------
|
||||
|
||||
public async Task ManageWorkItemDetailsToCache(List<WorkItem> workItems)
|
||||
public async Task ManageWorkItemDetailsToCache(List<WorkItemMongoDB> workItems)
|
||||
{
|
||||
var activityIds = workItems.Select(wi => wi.ActivityId).ToList();
|
||||
var workCategoryIds = workItems.Select(wi => wi.WorkCategoryId).ToList();
|
||||
var workItemIds = workItems.Select(wi => wi.Id).ToList();
|
||||
// fetching Activity master
|
||||
var activities = await _context.ActivityMasters.Where(a => activityIds.Contains(a.Id)).ToListAsync() ?? new List<ActivityMaster>();
|
||||
|
||||
// Fetching Work Category
|
||||
var workCategories = await _context.WorkCategoryMasters.Where(wc => workCategoryIds.Contains(wc.Id)).ToListAsync() ?? new List<WorkCategoryMaster>();
|
||||
var task = await _context.TaskAllocations.Where(t => workItemIds.Contains(t.WorkItemId) && t.AssignmentDate == DateTime.UtcNow).ToListAsync();
|
||||
var todaysAssign = task.Sum(t => t.PlannedTask);
|
||||
foreach (WorkItem workItem in workItems)
|
||||
foreach (WorkItemMongoDB workItem in workItems)
|
||||
{
|
||||
var activity = activities.FirstOrDefault(a => a.Id == workItem.ActivityId) ?? new ActivityMaster();
|
||||
var workCategory = workCategories.FirstOrDefault(a => a.Id == workItem.WorkCategoryId) ?? new WorkCategoryMaster();
|
||||
|
||||
var filter = Builders<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, todaysAssign),
|
||||
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, new ActivityMasterMongoDB
|
||||
{
|
||||
Id = activity.Id.ToString(),
|
||||
ActivityName = activity.ActivityName,
|
||||
UnitOfMeasurement = activity.UnitOfMeasurement
|
||||
}),
|
||||
Builders<WorkItemMongoDB>.Update.Set(r => r.WorkCategoryMaster, new WorkCategoryMasterMongoDB
|
||||
{
|
||||
Id = workCategory.Id.ToString(),
|
||||
Name = workCategory.Name,
|
||||
Description = workCategory.Description,
|
||||
})
|
||||
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);
|
||||
@ -625,5 +494,13 @@ namespace Marco.Pms.CacheHelper
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public async Task<bool> DeleteWorkItemByIdFromCacheAsync(Guid workItemId)
|
||||
{
|
||||
var filter = Builders<WorkItemMongoDB>.Filter.Eq(e => e.Id, workItemId.ToString());
|
||||
var result = await _taskCollection.DeleteOneAsync(filter);
|
||||
return result.DeletedCount > 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
42
Marco.Pms.CacheHelper/ReportCache.cs
Normal file
42
Marco.Pms.CacheHelper/ReportCache.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Marco.Pms.Model.Dtos.Project
|
||||
{
|
||||
public class BuildingDot
|
||||
public class BuildingDto
|
||||
{
|
||||
[Key]
|
||||
public Guid? Id { get; set; }
|
@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Marco.Pms.Model.Dtos.Project
|
||||
{
|
||||
public class FloorDot
|
||||
public class FloorDto
|
||||
{
|
||||
public Guid? Id { get; set; }
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
9
Marco.Pms.Model/Dtos/Projects/InfraDto.cs
Normal file
9
Marco.Pms.Model/Dtos/Projects/InfraDto.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Marco.Pms.Model.Dtos.Project
|
||||
{
|
||||
public class WorkAreaDot
|
||||
public class WorkAreaDto
|
||||
{
|
||||
[Key]
|
||||
public Guid? Id { get; set; }
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace Marco.Pms.Model.Dtos.Project
|
||||
{
|
||||
public class WorkItemDot
|
||||
public class WorkItemDto
|
||||
{
|
||||
[Key]
|
||||
public Guid? Id { get; set; }
|
@ -5,7 +5,7 @@ namespace Marco.Pms.Model.Mapper
|
||||
{
|
||||
public static class BuildingMapper
|
||||
{
|
||||
public static Building ToBuildingFromBuildingDto(this BuildingDot model, Guid tenantId)
|
||||
public static Building ToBuildingFromBuildingDto(this BuildingDto model, Guid tenantId)
|
||||
{
|
||||
return new Building
|
||||
{
|
||||
@ -20,7 +20,7 @@ namespace Marco.Pms.Model.Mapper
|
||||
|
||||
public static class FloorMapper
|
||||
{
|
||||
public static Floor ToFloorFromFloorDto(this FloorDot model, Guid tenantId)
|
||||
public static Floor ToFloorFromFloorDto(this FloorDto model, Guid tenantId)
|
||||
{
|
||||
return new Floor
|
||||
{
|
||||
@ -34,7 +34,7 @@ namespace Marco.Pms.Model.Mapper
|
||||
|
||||
public static class WorAreaMapper
|
||||
{
|
||||
public static WorkArea ToWorkAreaFromWorkAreaDto(this WorkAreaDot model, Guid tenantId)
|
||||
public static WorkArea ToWorkAreaFromWorkAreaDto(this WorkAreaDto model, Guid tenantId)
|
||||
{
|
||||
return new WorkArea
|
||||
{
|
||||
@ -48,7 +48,7 @@ namespace Marco.Pms.Model.Mapper
|
||||
}
|
||||
public static class WorkItemMapper
|
||||
{
|
||||
public static WorkItem ToWorkItemFromWorkItemDto(this WorkItemDot model, Guid tenantId)
|
||||
public static WorkItem ToWorkItemFromWorkItemDto(this WorkItemDto model, Guid tenantId)
|
||||
{
|
||||
return new WorkItem
|
||||
{
|
||||
|
@ -9,5 +9,6 @@ namespace Marco.Pms.Model.MongoDBModels
|
||||
public List<string> ApplicationRoleIds { get; set; } = new List<string>();
|
||||
public List<string> PermissionIds { get; set; } = new List<string>();
|
||||
public List<string> ProjectIds { get; set; } = new List<string>();
|
||||
public DateTime ExpireAt { get; set; } = DateTime.UtcNow.Date.AddDays(1);
|
||||
}
|
||||
}
|
||||
|
@ -14,5 +14,6 @@
|
||||
public int TeamSize { get; set; }
|
||||
public double CompletedWork { get; set; }
|
||||
public double PlannedWork { get; set; }
|
||||
public DateTime ExpireAt { get; set; } = DateTime.UtcNow.Date.AddDays(1);
|
||||
}
|
||||
}
|
||||
|
16
Marco.Pms.Model/MongoDBModels/ProjectReportEmailMongoDB.cs
Normal file
16
Marco.Pms.Model/MongoDBModels/ProjectReportEmailMongoDB.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace Marco.Pms.Model.MongoDBModels
|
||||
{
|
||||
public class ProjectReportEmailMongoDB
|
||||
{
|
||||
[BsonId] // Tells MongoDB this is the primary key (_id)
|
||||
[BsonRepresentation(BsonType.ObjectId)] // Optional: if your _id is ObjectId
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string? Body { get; set; }
|
||||
public string? Subject { get; set; }
|
||||
public List<string>? Receivers { get; set; }
|
||||
public bool IsSent { get; set; } = false;
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
{
|
||||
public class StatusMasterMongoDB
|
||||
{
|
||||
public string? Id { get; set; }
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string? Status { get; set; }
|
||||
}
|
||||
}
|
||||
|
8
Marco.Pms.Model/Utilities/ServiceResponse.cs
Normal file
8
Marco.Pms.Model/Utilities/ServiceResponse.cs
Normal 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("");
|
||||
}
|
||||
}
|
13
Marco.Pms.Model/ViewModels/Projects/ProjectAllocationVM.cs
Normal file
13
Marco.Pms.Model/ViewModels/Projects/ProjectAllocationVM.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -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.Dtos.Attendance;
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Entitlements;
|
||||
using Marco.Pms.Model.Mapper;
|
||||
using Marco.Pms.Model.Projects;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Marco.Pms.Model.ViewModels.AttendanceVM;
|
||||
using Marco.Pms.Services.Hubs;
|
||||
using Marco.Pms.Services.Service;
|
||||
using Marco.Pms.Services.Service.ServiceInterfaces;
|
||||
using MarcoBMS.Services.Helpers;
|
||||
using MarcoBMS.Services.Service;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -16,6 +17,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Globalization;
|
||||
using Document = Marco.Pms.Model.DocumentManager.Document;
|
||||
|
||||
namespace MarcoBMS.Services.Controllers
|
||||
@ -27,7 +29,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly EmployeeHelper _employeeHelper;
|
||||
private readonly ProjectsHelper _projectsHelper;
|
||||
private readonly IProjectServices _projectServices;
|
||||
private readonly UserHelper _userHelper;
|
||||
private readonly S3UploadService _s3Service;
|
||||
private readonly PermissionServices _permission;
|
||||
@ -36,11 +38,11 @@ namespace MarcoBMS.Services.Controllers
|
||||
|
||||
|
||||
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;
|
||||
_employeeHelper = employeeHelper;
|
||||
_projectsHelper = projectsHelper;
|
||||
_projectServices = projectServices;
|
||||
_userHelper = userHelper;
|
||||
_s3Service = s3Service;
|
||||
_logger = logger;
|
||||
@ -61,7 +63,13 @@ namespace MarcoBMS.Services.Controllers
|
||||
{
|
||||
Guid TenantId = GetTenantId();
|
||||
|
||||
List<AttendanceLog> lstAttendance = await _context.AttendanceLogs.Include(a => a.Document).Include(a => a.Employee).Include(a => a.UpdatedByEmployee).Where(c => c.AttendanceId == attendanceid && c.TenantId == TenantId).ToListAsync();
|
||||
List<AttendanceLog> lstAttendance = await _context.AttendanceLogs
|
||||
.Include(a => a.Document)
|
||||
.Include(a => a.Employee)
|
||||
.Include(a => a.UpdatedByEmployee)
|
||||
.Where(c => c.AttendanceId == attendanceid && c.TenantId == TenantId)
|
||||
.ToListAsync();
|
||||
|
||||
List<AttendanceLogVM> attendanceLogVMs = new List<AttendanceLogVM>();
|
||||
foreach (var attendanceLog in lstAttendance)
|
||||
{
|
||||
@ -83,18 +91,18 @@ namespace MarcoBMS.Services.Controllers
|
||||
|
||||
if (dateFrom != null && DateTime.TryParse(dateFrom, out fromDate) == false)
|
||||
{
|
||||
_logger.LogError("User sent Invalid from Date while featching attendance logs");
|
||||
_logger.LogWarning("User sent Invalid from Date while featching attendance logs");
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
|
||||
}
|
||||
if (dateTo != null && DateTime.TryParse(dateTo, out toDate) == false)
|
||||
{
|
||||
_logger.LogError("User sent Invalid to Date while featching attendance logs");
|
||||
_logger.LogWarning("User sent Invalid to Date while featching attendance logs");
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
|
||||
}
|
||||
|
||||
if (employeeId == Guid.Empty)
|
||||
{
|
||||
_logger.LogError("The employee Id sent by user is empty");
|
||||
_logger.LogWarning("The employee Id sent by user is empty");
|
||||
return BadRequest(ApiResponse<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();
|
||||
@ -139,9 +147,9 @@ namespace MarcoBMS.Services.Controllers
|
||||
{
|
||||
Guid TenantId = GetTenantId();
|
||||
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var hasTeamAttendancePermission = await _permission.HasPermission(new Guid("915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"), LoggedInEmployee.Id);
|
||||
var hasSelfAttendancePermission = await _permission.HasPermission(new Guid("ccb0589f-712b-43de-92ed-5b6088e7dc4e"), LoggedInEmployee.Id);
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId.ToString());
|
||||
var hasTeamAttendancePermission = await _permission.HasPermission(PermissionsMaster.TeamAttendance, LoggedInEmployee.Id);
|
||||
var hasSelfAttendancePermission = await _permission.HasPermission(PermissionsMaster.SelfAttendance, LoggedInEmployee.Id);
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId);
|
||||
|
||||
if (!hasProjectPermission)
|
||||
{
|
||||
@ -154,18 +162,18 @@ namespace MarcoBMS.Services.Controllers
|
||||
|
||||
if (dateFrom != null && DateTime.TryParse(dateFrom, out fromDate) == false)
|
||||
{
|
||||
_logger.LogError("User sent Invalid fromDate while featching attendance logs");
|
||||
_logger.LogWarning("User sent Invalid fromDate while featching attendance logs");
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
|
||||
}
|
||||
if (dateTo != null && DateTime.TryParse(dateTo, out toDate) == false)
|
||||
{
|
||||
_logger.LogError("User sent Invalid toDate while featching attendance logs");
|
||||
_logger.LogWarning("User sent Invalid toDate while featching attendance logs");
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
|
||||
}
|
||||
|
||||
if (projectId == Guid.Empty)
|
||||
{
|
||||
_logger.LogError("The project Id sent by user is less than or equal to zero");
|
||||
_logger.LogWarning("The project Id sent by user is less than or equal to zero");
|
||||
return BadRequest(ApiResponse<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<ProjectAllocation> projectteam = await _projectsHelper.GetTeamByProject(TenantId, projectId, true);
|
||||
List<ProjectAllocation> projectteam = await _projectServices.GetTeamByProject(TenantId, projectId, true);
|
||||
var jobRole = await _context.JobRoles.ToListAsync();
|
||||
foreach (Attendance? attendance in lstAttendance)
|
||||
{
|
||||
@ -255,9 +263,9 @@ namespace MarcoBMS.Services.Controllers
|
||||
{
|
||||
Guid TenantId = GetTenantId();
|
||||
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var hasTeamAttendancePermission = await _permission.HasPermission(new Guid("915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"), LoggedInEmployee.Id);
|
||||
var hasSelfAttendancePermission = await _permission.HasPermission(new Guid("ccb0589f-712b-43de-92ed-5b6088e7dc4e"), LoggedInEmployee.Id);
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId.ToString());
|
||||
var hasTeamAttendancePermission = await _permission.HasPermission(PermissionsMaster.TeamAttendance, LoggedInEmployee.Id);
|
||||
var hasSelfAttendancePermission = await _permission.HasPermission(PermissionsMaster.SelfAttendance, LoggedInEmployee.Id);
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId);
|
||||
|
||||
if (!hasProjectPermission)
|
||||
{
|
||||
@ -269,13 +277,13 @@ namespace MarcoBMS.Services.Controllers
|
||||
|
||||
if (date != null && DateTime.TryParse(date, out forDate) == false)
|
||||
{
|
||||
_logger.LogError("User sent Invalid Date while featching attendance logs");
|
||||
_logger.LogWarning("User sent Invalid Date while featching attendance logs");
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
|
||||
|
||||
}
|
||||
if (projectId == Guid.Empty)
|
||||
{
|
||||
_logger.LogError("The project Id sent by user is less than or equal to zero");
|
||||
_logger.LogWarning("The project Id sent by user is less than or equal to zero");
|
||||
return BadRequest(ApiResponse<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<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 emp = await _context.Employees.Where(e => idList.Contains(e.Id)).Include(e => e.JobRole).ToListAsync();
|
||||
var jobRole = await _context.JobRoles.ToListAsync();
|
||||
@ -361,7 +369,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
Guid TenantId = GetTenantId();
|
||||
Employee LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var result = new List<EmployeeAttendanceVM>();
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId.ToString());
|
||||
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId);
|
||||
|
||||
if (!hasProjectPermission)
|
||||
{
|
||||
@ -371,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<ProjectAllocation> projectteam = await _projectsHelper.GetTeamByProject(TenantId, projectId, true);
|
||||
List<ProjectAllocation> projectteam = await _projectServices.GetTeamByProject(TenantId, projectId, true);
|
||||
var idList = projectteam.Select(p => p.EmployeeId).ToList();
|
||||
var jobRole = await _context.JobRoles.ToListAsync();
|
||||
|
||||
@ -419,7 +426,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
.SelectMany(v => v.Errors)
|
||||
.Select(e => e.ErrorMessage)
|
||||
.ToList();
|
||||
_logger.LogError("User sent Invalid Date while marking attendance");
|
||||
_logger.LogWarning("User sent Invalid Date while marking attendance \n {Error}", string.Join(",", errors));
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
|
||||
}
|
||||
|
||||
@ -433,14 +440,14 @@ namespace MarcoBMS.Services.Controllers
|
||||
|
||||
if (recordAttendanceDot.MarkTime == null)
|
||||
{
|
||||
_logger.LogError("User sent Invalid Mark Time while marking attendance");
|
||||
_logger.LogWarning("User sent Invalid Mark Time while marking attendance");
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Mark Time", "Invalid Mark Time", 400));
|
||||
}
|
||||
|
||||
DateTime finalDateTime = GetDateFromTimeStamp(recordAttendanceDot.Date, recordAttendanceDot.MarkTime);
|
||||
if (recordAttendanceDot.Comment == null)
|
||||
{
|
||||
_logger.LogError("User sent Invalid comment while marking attendance");
|
||||
_logger.LogWarning("User sent Invalid comment while marking attendance");
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Comment", "Invalid Comment", 400));
|
||||
}
|
||||
|
||||
@ -474,7 +481,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Employee {EmployeeId} sent regularization request but it check-out time is earlier than check-out");
|
||||
_logger.LogWarning("Employee {EmployeeId} sent regularization request but it check-out time is earlier than check-out");
|
||||
return BadRequest(ApiResponse<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
|
||||
@ -579,7 +586,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
catch (Exception ex)
|
||||
{
|
||||
await transaction.RollbackAsync(); // Rollback on failure
|
||||
_logger.LogError("{Error} while marking attendance", ex.Message);
|
||||
_logger.LogError(ex, "An Error occured while marking attendance");
|
||||
var response = new
|
||||
{
|
||||
message = ex.Message,
|
||||
@ -598,7 +605,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList();
|
||||
_logger.LogError("Invalid attendance model received.");
|
||||
_logger.LogWarning("Invalid attendance model received. \n {Error}", string.Join(",", errors));
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
|
||||
}
|
||||
|
||||
@ -774,7 +781,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
catch (Exception ex)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
_logger.LogError("Error while recording attendance : {Error}", ex.Message);
|
||||
_logger.LogError(ex, "Error while recording attendance");
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Something went wrong", ex.Message, 500));
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,4 @@
|
||||
using System.Net;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Model.Authentication;
|
||||
using Marco.Pms.Model.Dtos.Authentication;
|
||||
using Marco.Pms.Model.Dtos.Util;
|
||||
@ -15,6 +11,10 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Net;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace MarcoBMS.Services.Controllers
|
||||
{
|
||||
@ -110,7 +110,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Unexpected error during login : {Error}", ex.Message);
|
||||
_logger.LogError(ex, "Unexpected error during login");
|
||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("Unexpected error", ex.Message, 500));
|
||||
}
|
||||
}
|
||||
@ -270,7 +270,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Unexpected error occurred while verifying MPIN : {Error}", ex.Message);
|
||||
_logger.LogError(ex, "Unexpected error occurred while verifying MPIN");
|
||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("Unexpected error", ex.Message, 500));
|
||||
}
|
||||
}
|
||||
@ -307,7 +307,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Unexpected error during logout : {Error}", ex.Message);
|
||||
_logger.LogError(ex, "Unexpected error during logout");
|
||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("Unexpected error occurred", ex.Message, 500));
|
||||
}
|
||||
}
|
||||
@ -351,7 +351,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
|
||||
if (string.IsNullOrWhiteSpace(user.UserName))
|
||||
{
|
||||
_logger.LogError("Username missing for user ID: {UserId}", user.Id);
|
||||
_logger.LogWarning("Username missing for user ID: {UserId}", user.Id);
|
||||
return NotFound(ApiResponse<object>.ErrorResponse("Username not found.", "Username not found.", 404));
|
||||
}
|
||||
|
||||
@ -370,7 +370,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("An unexpected error occurred during token refresh. : {Error}", ex.Message);
|
||||
_logger.LogError(ex, "An unexpected error occurred during token refresh.");
|
||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("Unexpected error occurred.", ex.Message, 500));
|
||||
}
|
||||
}
|
||||
@ -406,7 +406,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Error while sending password reset email to: {Error}", ex.Message);
|
||||
_logger.LogError(ex, "Error while sending password reset email to");
|
||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("Error sending password reset email.", ex.Message, 500));
|
||||
}
|
||||
}
|
||||
@ -480,7 +480,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Error while sending reset password success email to user: {Error}", ex.Message);
|
||||
_logger.LogError(ex, "Error while sending reset password success email to user");
|
||||
// Continue, do not fail because of email issue
|
||||
}
|
||||
|
||||
@ -547,7 +547,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("An unexpected error occurred while sending OTP to {Email} : {Error}", generateOTP.Email ?? "", ex.Message);
|
||||
_logger.LogError(ex, "An unexpected error occurred while sending OTP to {Email}", generateOTP.Email ?? "");
|
||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("An unexpected error occurred.", ex.Message, 500));
|
||||
}
|
||||
}
|
||||
@ -638,7 +638,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("An unexpected error occurred during OTP login for email {Email} : {Error}", verifyOTP.Email ?? string.Empty, ex.Message);
|
||||
_logger.LogError(ex, "An unexpected error occurred during OTP login for email {Email}", verifyOTP.Email ?? string.Empty);
|
||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("Unexpected error", ex.Message, 500));
|
||||
}
|
||||
}
|
||||
@ -719,7 +719,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
var errors = result.Errors.Select(e => e.Description).ToList();
|
||||
_logger.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));
|
||||
}
|
||||
|
||||
@ -732,7 +732,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
}
|
||||
catch (Exception exp)
|
||||
{
|
||||
_logger.LogError("An unexpected error occurred while changing password : {Error}", exp.Message);
|
||||
_logger.LogError(exp, "An unexpected error occurred while changing password");
|
||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("An unexpected error occurred.", exp.Message, 500));
|
||||
}
|
||||
}
|
||||
@ -752,7 +752,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
// Validate employee and MPIN input
|
||||
if (requestEmployee == null || string.IsNullOrWhiteSpace(generateMPINDto.MPIN) || generateMPINDto.MPIN.Length != 6 || !generateMPINDto.MPIN.All(char.IsDigit))
|
||||
{
|
||||
_logger.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));
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ using Marco.Pms.Model.Projects;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Marco.Pms.Model.ViewModels.DashBoard;
|
||||
using Marco.Pms.Services.Service;
|
||||
using Marco.Pms.Services.Service.ServiceInterfaces;
|
||||
using MarcoBMS.Services.Helpers;
|
||||
using MarcoBMS.Services.Service;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -21,15 +22,15 @@ namespace Marco.Pms.Services.Controllers
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly UserHelper _userHelper;
|
||||
private readonly ProjectsHelper _projectsHelper;
|
||||
private readonly IProjectServices _projectServices;
|
||||
private readonly ILoggingService _logger;
|
||||
private readonly PermissionServices _permissionServices;
|
||||
public static readonly Guid ActiveId = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731");
|
||||
public DashboardController(ApplicationDbContext context, UserHelper userHelper, ProjectsHelper projectsHelper, ILoggingService logger, PermissionServices permissionServices)
|
||||
public DashboardController(ApplicationDbContext context, UserHelper userHelper, IProjectServices projectServices, ILoggingService logger, PermissionServices permissionServices)
|
||||
{
|
||||
_context = context;
|
||||
_userHelper = userHelper;
|
||||
_projectsHelper = projectsHelper;
|
||||
_projectServices = projectServices;
|
||||
_logger = logger;
|
||||
_permissionServices = permissionServices;
|
||||
}
|
||||
@ -182,11 +183,13 @@ namespace Marco.Pms.Services.Controllers
|
||||
|
||||
// --- Step 1: Get the list of projects the user can access ---
|
||||
// This query is more efficient as it only selects the IDs needed.
|
||||
var projects = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee);
|
||||
var accessibleActiveProjectIds = projects
|
||||
.Where(p => p.ProjectStatusId == ActiveId)
|
||||
var projects = await _projectServices.GetMyProjectIdsAsync(tenantId, loggedInEmployee);
|
||||
|
||||
var accessibleActiveProjectIds = await _context.Projects
|
||||
.Where(p => p.ProjectStatusId == ActiveId && projects.Contains(p.Id))
|
||||
.Select(p => p.Id)
|
||||
.ToList();
|
||||
.ToListAsync();
|
||||
|
||||
if (!accessibleActiveProjectIds.Any())
|
||||
{
|
||||
_logger.LogInfo("User {UserId} has no accessible active projects.", loggedInEmployee.Id);
|
||||
@ -199,7 +202,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
if (projectId.HasValue)
|
||||
{
|
||||
// Security Check: Ensure the requested project is in the user's accessible list.
|
||||
var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value.ToString());
|
||||
var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value);
|
||||
if (!hasPermission)
|
||||
{
|
||||
_logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId} (not active or not accessible).", loggedInEmployee.Id, projectId.Value);
|
||||
@ -250,7 +253,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("An unexpected error occurred in GetTotalEmployees for projectId {ProjectId} \n {Error}", projectId ?? Guid.Empty, ex.Message);
|
||||
_logger.LogError(ex, "An unexpected error occurred in GetTotalEmployees for projectId {ProjectId}", projectId ?? Guid.Empty);
|
||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred.", null, 500));
|
||||
}
|
||||
}
|
||||
@ -281,7 +284,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
// --- Logic for a SINGLE Project ---
|
||||
|
||||
// 2a. Security Check: Verify permission for the specific project.
|
||||
var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value.ToString());
|
||||
var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value);
|
||||
if (!hasPermission)
|
||||
{
|
||||
_logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, projectId.Value);
|
||||
@ -301,8 +304,8 @@ namespace Marco.Pms.Services.Controllers
|
||||
// --- Logic for ALL Accessible Projects ---
|
||||
|
||||
// 2c. Get a list of all projects the user is allowed to see.
|
||||
var accessibleProject = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee);
|
||||
var accessibleProjectIds = accessibleProject.Select(p => p.Id).ToList();
|
||||
var accessibleProjectIds = await _projectServices.GetMyProjectIdsAsync(tenantId, loggedInEmployee);
|
||||
|
||||
if (!accessibleProjectIds.Any())
|
||||
{
|
||||
_logger.LogInfo("User {UserId} has no accessible projects.", loggedInEmployee.Id);
|
||||
@ -341,7 +344,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("An unexpected error occurred in GetTotalTasks for projectId {ProjectId} \n {Error}", projectId ?? Guid.Empty, ex.Message);
|
||||
_logger.LogError(ex, "An unexpected error occurred in GetTotalTasks for projectId {ProjectId}", projectId ?? Guid.Empty);
|
||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred.", null, 500));
|
||||
}
|
||||
}
|
||||
@ -364,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);
|
||||
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));
|
||||
}
|
||||
|
||||
@ -378,14 +381,14 @@ namespace Marco.Pms.Services.Controllers
|
||||
List<ProjectProgressionVM>? projectProgressionVMs = new List<ProjectProgressionVM>();
|
||||
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));
|
||||
|
||||
}
|
||||
Project? project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == projectId);
|
||||
if (project == null)
|
||||
{
|
||||
_logger.LogError("Employee {EmployeeId} was attempted to get project attendance for date {Date}, but project not found in database", LoggedInEmployee.Id, currentDate);
|
||||
_logger.LogWarning("Employee {EmployeeId} was attempted to get project attendance for date {Date}, but project not found in database", LoggedInEmployee.Id, currentDate);
|
||||
return NotFound(ApiResponse<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();
|
||||
@ -431,14 +434,14 @@ namespace Marco.Pms.Services.Controllers
|
||||
DateTime currentDate = DateTime.UtcNow;
|
||||
if (date != null && DateTime.TryParse(date, out currentDate) == false)
|
||||
{
|
||||
_logger.LogError($"user send invalid date");
|
||||
_logger.LogWarning($"user send invalid date");
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid date.", "Invalid date.", 400));
|
||||
|
||||
}
|
||||
Project? project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == projectId);
|
||||
if (project == null)
|
||||
{
|
||||
_logger.LogError("Employee {EmployeeId} was attempted to get activities performed for date {Date}, but project not found in database", LoggedInEmployee.Id, currentDate);
|
||||
_logger.LogWarning("Employee {EmployeeId} was attempted to get activities performed for date {Date}, but project not found in database", LoggedInEmployee.Id, currentDate);
|
||||
return NotFound(ApiResponse<object>.ErrorResponse("Project not found", "Project not found", 404));
|
||||
}
|
||||
|
||||
@ -516,7 +519,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
|
||||
// Step 2: Permission check
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
bool hasAssigned = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.ToString());
|
||||
bool hasAssigned = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId);
|
||||
|
||||
if (!hasAssigned)
|
||||
{
|
||||
|
@ -77,7 +77,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
.SelectMany(v => v.Errors)
|
||||
.Select(e => e.ErrorMessage)
|
||||
.ToList();
|
||||
_logger.LogError("User sent Invalid Date while marking attendance");
|
||||
_logger.LogWarning("User sent Invalid Date while marking attendance");
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
|
||||
}
|
||||
var response = await _directoryHelper.CreateContact(createContact);
|
||||
@ -256,7 +256,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
.SelectMany(v => v.Errors)
|
||||
.Select(e => e.ErrorMessage)
|
||||
.ToList();
|
||||
_logger.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));
|
||||
}
|
||||
var response = await _directoryHelper.CreateBucket(bucketDto);
|
||||
|
@ -1,6 +1,4 @@
|
||||
using System.Data;
|
||||
using System.Net;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Model.Dtos.Attendance;
|
||||
using Marco.Pms.Model.Dtos.Employees;
|
||||
using Marco.Pms.Model.Employees;
|
||||
@ -11,6 +9,7 @@ using Marco.Pms.Model.Utilities;
|
||||
using Marco.Pms.Model.ViewModels.Employee;
|
||||
using Marco.Pms.Services.Hubs;
|
||||
using Marco.Pms.Services.Service;
|
||||
using Marco.Pms.Services.Service.ServiceInterfaces;
|
||||
using MarcoBMS.Services.Helpers;
|
||||
using MarcoBMS.Services.Service;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -18,6 +17,8 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Data;
|
||||
using System.Net;
|
||||
|
||||
namespace MarcoBMS.Services.Controllers
|
||||
{
|
||||
@ -37,13 +38,13 @@ namespace MarcoBMS.Services.Controllers
|
||||
private readonly ILoggingService _logger;
|
||||
private readonly IHubContext<MarcoHub> _signalR;
|
||||
private readonly PermissionServices _permission;
|
||||
private readonly ProjectsHelper _projectsHelper;
|
||||
private readonly IProjectServices _projectServices;
|
||||
private readonly Guid tenantId;
|
||||
|
||||
|
||||
public EmployeeController(UserManager<ApplicationUser> userManager, IEmailSender emailSender,
|
||||
ApplicationDbContext context, EmployeeHelper employeeHelper, UserHelper userHelper, IConfiguration configuration, ILoggingService logger,
|
||||
IHubContext<MarcoHub> signalR, PermissionServices permission, ProjectsHelper projectsHelper)
|
||||
IHubContext<MarcoHub> signalR, PermissionServices permission, IProjectServices projectServices)
|
||||
{
|
||||
_context = context;
|
||||
_userManager = userManager;
|
||||
@ -54,7 +55,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
_logger = logger;
|
||||
_signalR = signalR;
|
||||
_permission = permission;
|
||||
_projectsHelper = projectsHelper;
|
||||
_projectServices = projectServices;
|
||||
tenantId = _userHelper.GetTenantId();
|
||||
}
|
||||
|
||||
@ -119,8 +120,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
loggedInEmployee.Id, projectid ?? Guid.Empty, ShowInactive);
|
||||
|
||||
// Step 3: Fetch project access and permissions
|
||||
List<Project> projects = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee);
|
||||
var projectIds = projects.Select(p => p.Id).ToList();
|
||||
var projectIds = await _projectServices.GetMyProjectIdsAsync(tenantId, loggedInEmployee);
|
||||
|
||||
var hasViewAllEmployeesPermission = await _permission.HasPermission(PermissionsMaster.ViewAllEmployees, loggedInEmployee.Id);
|
||||
var hasViewTeamMembersPermission = await _permission.HasPermission(PermissionsMaster.ViewTeamMembers, loggedInEmployee.Id);
|
||||
@ -383,7 +383,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
Employee? existingEmployee = await _context.Employees.FirstOrDefaultAsync(e => e.Id == model.Id.Value);
|
||||
if (existingEmployee == null)
|
||||
{
|
||||
_logger.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));
|
||||
}
|
||||
byte[]? imageBytes = null;
|
||||
@ -496,7 +496,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Employee with ID {EmploueeId} not found in database", id);
|
||||
_logger.LogWarning("Employee with ID {EmploueeId} not found in database", id);
|
||||
}
|
||||
return Ok(ApiResponse<object>.SuccessResponse(new { }, "Employee Suspended successfully", 200));
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
.SelectMany(v => v.Errors)
|
||||
.Select(e => e.ErrorMessage)
|
||||
.ToList();
|
||||
_logger.LogError("{error}", errors);
|
||||
_logger.LogWarning("{error}", errors);
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
|
||||
}
|
||||
Guid tenantId = _userHelper.GetTenantId();
|
||||
@ -66,7 +66,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
var Image = attachmentDto;
|
||||
if (string.IsNullOrEmpty(Image.Base64Data))
|
||||
{
|
||||
_logger.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));
|
||||
}
|
||||
|
||||
@ -160,7 +160,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
.SelectMany(v => v.Errors)
|
||||
.Select(e => e.ErrorMessage)
|
||||
.ToList();
|
||||
_logger.LogError("{error}", errors);
|
||||
_logger.LogWarning("{error}", errors);
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
|
||||
}
|
||||
Guid tenantId = _userHelper.GetTenantId();
|
||||
@ -197,7 +197,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
var Image = attachmentDto;
|
||||
if (string.IsNullOrEmpty(Image.Base64Data))
|
||||
{
|
||||
_logger.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));
|
||||
}
|
||||
|
||||
@ -336,7 +336,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
_logger.LogInfo("Ticket {TicketId} updated", updateTicketDto.Id);
|
||||
return Ok(ApiResponse<object>.SuccessResponse(ticketVM, "Ticket Updated Successfully", 200));
|
||||
}
|
||||
_logger.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));
|
||||
}
|
||||
|
||||
@ -349,7 +349,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
.SelectMany(v => v.Errors)
|
||||
.Select(e => e.ErrorMessage)
|
||||
.ToList();
|
||||
_logger.LogError("{error}", errors);
|
||||
_logger.LogWarning("{error}", errors);
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
|
||||
}
|
||||
|
||||
@ -364,7 +364,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
|
||||
if (ticket == null)
|
||||
{
|
||||
_logger.LogError("Ticket {TicketId} not Found in database", addCommentDto.TicketId);
|
||||
_logger.LogWarning("Ticket {TicketId} not Found in database", addCommentDto.TicketId);
|
||||
return NotFound(ApiResponse<object>.ErrorResponse("Ticket not Found", "Ticket not Found", 404));
|
||||
}
|
||||
|
||||
@ -379,7 +379,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
var Image = attachmentDto;
|
||||
if (string.IsNullOrEmpty(Image.Base64Data))
|
||||
{
|
||||
_logger.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));
|
||||
}
|
||||
|
||||
@ -437,7 +437,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
.SelectMany(v => v.Errors)
|
||||
.Select(e => e.ErrorMessage)
|
||||
.ToList();
|
||||
_logger.LogError("{error}", errors);
|
||||
_logger.LogWarning("{error}", errors);
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
|
||||
}
|
||||
|
||||
@ -451,7 +451,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
|
||||
if (ticket == null)
|
||||
{
|
||||
_logger.LogError("Ticket {TicketId} not Found in database", updateCommentDto.TicketId);
|
||||
_logger.LogWarning("Ticket {TicketId} not Found in database", updateCommentDto.TicketId);
|
||||
return NotFound(ApiResponse<object>.ErrorResponse("Ticket not Found", "Ticket not Found", 404));
|
||||
}
|
||||
|
||||
@ -474,7 +474,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
var Image = attachmentDto;
|
||||
if (string.IsNullOrEmpty(Image.Base64Data))
|
||||
{
|
||||
_logger.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));
|
||||
}
|
||||
|
||||
@ -552,7 +552,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
.SelectMany(v => v.Errors)
|
||||
.Select(e => e.ErrorMessage)
|
||||
.ToList();
|
||||
_logger.LogError("{error}", errors);
|
||||
_logger.LogWarning("{error}", errors);
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
|
||||
}
|
||||
|
||||
@ -568,7 +568,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
|
||||
if (tickets == null || tickets.Count > 0)
|
||||
{
|
||||
_logger.LogError("Tickets not Found in database");
|
||||
_logger.LogWarning("Tickets not Found in database");
|
||||
return NotFound(ApiResponse<object>.ErrorResponse("Ticket not Found", "Ticket not Found", 404));
|
||||
}
|
||||
|
||||
@ -578,12 +578,12 @@ namespace Marco.Pms.Services.Controllers
|
||||
{
|
||||
if (string.IsNullOrEmpty(forumAttachmentDto.Base64Data))
|
||||
{
|
||||
_logger.LogError("Base64 data is missing");
|
||||
_logger.LogWarning("Base64 data is missing");
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
|
||||
}
|
||||
if (forumAttachmentDto.TicketId == null)
|
||||
{
|
||||
_logger.LogError("ticket ID is missing");
|
||||
_logger.LogWarning("ticket ID is missing");
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("ticket ID is missing", "ticket ID is missing", 400));
|
||||
}
|
||||
var ticket = tickets.FirstOrDefault(t => t.Id == forumAttachmentDto.TicketId);
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Model.Activities;
|
||||
using Marco.Pms.Model.Dtos.DocumentManager;
|
||||
using Marco.Pms.Model.Employees;
|
||||
@ -13,6 +12,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Marco.Pms.Services.Controllers
|
||||
{
|
||||
@ -54,7 +54,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
}
|
||||
|
||||
// Step 2: Check project access permission
|
||||
var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.ToString());
|
||||
var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId);
|
||||
if (!hasPermission)
|
||||
{
|
||||
_logger.LogWarning("[GetImageList] Access denied for EmployeeId: {EmployeeId} on ProjectId: {ProjectId}", loggedInEmployee.Id, projectId);
|
||||
|
@ -168,7 +168,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
_logger.LogInfo("activity updated successfully from tenant {tenantId}", tenantId);
|
||||
return Ok(ApiResponse<object>.SuccessResponse(activityVM, "activity updated successfully", 200));
|
||||
}
|
||||
_logger.LogError("Activity {ActivityId} not found", id);
|
||||
_logger.LogWarning("Activity {ActivityId} not found", id);
|
||||
return NotFound(ApiResponse<object>.ErrorResponse("Activity not found", "Activity not found", 404));
|
||||
}
|
||||
|
||||
@ -230,7 +230,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
_logger.LogInfo("Ticket Status master {TicketStatusId} added successfully from tenant {tenantId}", statusMaster.Id, tenantId);
|
||||
return Ok(ApiResponse<object>.SuccessResponse(statusVM, "Ticket Status master added successfully", 200));
|
||||
}
|
||||
_logger.LogError("User sent empyt payload");
|
||||
_logger.LogWarning("User sent empyt payload");
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Sent Empty payload", "Sent Empty payload", 400));
|
||||
}
|
||||
|
||||
@ -251,10 +251,10 @@ namespace Marco.Pms.Services.Controllers
|
||||
_logger.LogInfo("Ticket Status master {TicketStatusId} updated successfully from tenant {tenantId}", statusMaster.Id, tenantId);
|
||||
return Ok(ApiResponse<object>.SuccessResponse(statusVM, "Ticket Status master updated successfully", 200));
|
||||
}
|
||||
_logger.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));
|
||||
}
|
||||
_logger.LogError("User sent empyt payload");
|
||||
_logger.LogWarning("User sent empyt payload");
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Sent Empty payload", "Sent Empty payload", 400));
|
||||
}
|
||||
|
||||
@ -281,7 +281,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.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));
|
||||
}
|
||||
}
|
||||
@ -318,7 +318,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
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));
|
||||
}
|
||||
|
||||
@ -339,10 +339,10 @@ namespace Marco.Pms.Services.Controllers
|
||||
_logger.LogInfo("Ticket Type master {TicketTypeId} updated successfully from tenant {tenantId}", typeMaster.Id, tenantId);
|
||||
return Ok(ApiResponse<object>.SuccessResponse(typeVM, "Ticket type master updated successfully", 200));
|
||||
}
|
||||
_logger.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));
|
||||
}
|
||||
_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));
|
||||
}
|
||||
|
||||
@ -369,7 +369,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Ticket Type {TickeTypeId} not found in database", id);
|
||||
_logger.LogWarning("Ticket Type {TickeTypeId} not found in database", id);
|
||||
return NotFound(ApiResponse<object>.ErrorResponse("Ticket Type not found", "Ticket Type not found", 404));
|
||||
}
|
||||
}
|
||||
@ -407,7 +407,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
return Ok(ApiResponse<object>.SuccessResponse(typeVM, "Ticket Priority master added successfully", 200));
|
||||
}
|
||||
|
||||
_logger.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));
|
||||
}
|
||||
[HttpPost("ticket-priorities/edit/{id}")]
|
||||
@ -427,10 +427,10 @@ namespace Marco.Pms.Services.Controllers
|
||||
_logger.LogInfo("Ticket Priority master {TicketPriorityId} updated successfully from tenant {tenantId}", typeMaster.Id, tenantId);
|
||||
return Ok(ApiResponse<object>.SuccessResponse(typeVM, "Ticket Priority master updated successfully", 200));
|
||||
}
|
||||
_logger.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));
|
||||
}
|
||||
_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));
|
||||
}
|
||||
|
||||
@ -457,7 +457,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Ticket Priority {TickePriorityId} not found in database", id);
|
||||
_logger.LogWarning("Ticket Priority {TickePriorityId} not found in database", id);
|
||||
return NotFound(ApiResponse<object>.ErrorResponse("Ticket Priority not found", "Ticket Priority not found", 404));
|
||||
}
|
||||
}
|
||||
@ -494,7 +494,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
return Ok(ApiResponse<object>.SuccessResponse(typeVM, "Ticket tag master added successfully", 200));
|
||||
|
||||
}
|
||||
_logger.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));
|
||||
}
|
||||
|
||||
@ -515,10 +515,10 @@ namespace Marco.Pms.Services.Controllers
|
||||
_logger.LogInfo("Ticket Tag master {TicketTypeId} updated successfully from tenant {tenantId}", tagMaster.Id, tenantId);
|
||||
return Ok(ApiResponse<object>.SuccessResponse(typeVM, "Ticket tag master updated successfully", 200));
|
||||
}
|
||||
_logger.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));
|
||||
}
|
||||
_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));
|
||||
}
|
||||
|
||||
@ -545,7 +545,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Ticket Tag {TickeTagId} not found in database", id);
|
||||
_logger.LogWarning("Ticket Tag {TickeTagId} not found in database", id);
|
||||
return NotFound(ApiResponse<object>.ErrorResponse("Ticket tag not found", "Ticket tag not found", 404));
|
||||
}
|
||||
}
|
||||
@ -609,7 +609,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
return Ok(ApiResponse<object>.SuccessResponse(workCategoryMasterVM, "Work category master added successfully", 200));
|
||||
|
||||
}
|
||||
_logger.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));
|
||||
}
|
||||
|
||||
@ -624,7 +624,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
{
|
||||
if (workCategory.IsSystem)
|
||||
{
|
||||
_logger.LogError("User tries to update system-defined work category");
|
||||
_logger.LogWarning("User tries to update system-defined work category");
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Cannot update system-defined work", "Cannot update system-defined work", 400));
|
||||
}
|
||||
workCategory = workCategoryMasterDto.ToWorkCategoryMasterFromWorkCategoryMasterDto(tenantId);
|
||||
@ -635,10 +635,10 @@ namespace Marco.Pms.Services.Controllers
|
||||
_logger.LogInfo("Work category master {WorkCategoryId} updated successfully from tenant {tenantId}", workCategory.Id, tenantId);
|
||||
return Ok(ApiResponse<object>.SuccessResponse(workCategoryMasterVM, "Work category master updated successfully", 200));
|
||||
}
|
||||
_logger.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));
|
||||
}
|
||||
_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));
|
||||
}
|
||||
|
||||
@ -666,7 +666,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Work category {WorkCategoryId} not found in database", id);
|
||||
_logger.LogWarning("Work category {WorkCategoryId} not found in database", id);
|
||||
return NotFound(ApiResponse<object>.ErrorResponse("Work category not found", "Work category not found", 404));
|
||||
}
|
||||
}
|
||||
@ -689,7 +689,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
.SelectMany(v => v.Errors)
|
||||
.Select(e => e.ErrorMessage)
|
||||
.ToList();
|
||||
_logger.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));
|
||||
}
|
||||
var response = await _masterHelper.CreateWorkStatus(createWorkStatusDto);
|
||||
@ -803,7 +803,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
.SelectMany(v => v.Errors)
|
||||
.Select(e => e.ErrorMessage)
|
||||
.ToList();
|
||||
_logger.LogError("User sent Invalid Date while marking attendance");
|
||||
_logger.LogWarning("User sent Invalid Date while marking attendance");
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
|
||||
}
|
||||
var response = await _masterHelper.CreateContactTag(contactTagDto);
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,16 +1,19 @@
|
||||
using System.Data;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Model.Dtos.Mail;
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Mail;
|
||||
using Marco.Pms.Model.MongoDBModels;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Marco.Pms.Services.Helpers;
|
||||
using MarcoBMS.Services.Helpers;
|
||||
using MarcoBMS.Services.Service;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MongoDB.Driver;
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using System.Net.Mail;
|
||||
|
||||
namespace Marco.Pms.Services.Controllers
|
||||
{
|
||||
@ -25,7 +28,11 @@ namespace Marco.Pms.Services.Controllers
|
||||
private readonly UserHelper _userHelper;
|
||||
private readonly IWebHostEnvironment _env;
|
||||
private readonly ReportHelper _reportHelper;
|
||||
public ReportController(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, UserHelper userHelper, IWebHostEnvironment env, ReportHelper reportHelper)
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly CacheUpdateHelper _cache;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
public ReportController(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, UserHelper userHelper,
|
||||
IWebHostEnvironment env, ReportHelper reportHelper, IConfiguration configuration, CacheUpdateHelper cache, IServiceScopeFactory serviceScopeFactory)
|
||||
{
|
||||
_context = context;
|
||||
_emailSender = emailSender;
|
||||
@ -33,27 +40,122 @@ namespace Marco.Pms.Services.Controllers
|
||||
_userHelper = userHelper;
|
||||
_env = env;
|
||||
_reportHelper = reportHelper;
|
||||
_configuration = configuration;
|
||||
_cache = cache;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
[HttpPost("set-mail")]
|
||||
/// <summary>
|
||||
/// Adds new mail details for a project report.
|
||||
/// </summary>
|
||||
/// <param name="mailDetailsDto">The mail details data.</param>
|
||||
/// <returns>An API response indicating success or failure.</returns>
|
||||
[HttpPost("mail-details")] // More specific route for adding mail details
|
||||
public async Task<IActionResult> AddMailDetails([FromBody] MailDetailsDto mailDetailsDto)
|
||||
{
|
||||
// 1. Get Tenant ID and Basic Authorization Check
|
||||
Guid tenantId = _userHelper.GetTenantId();
|
||||
MailDetails mailDetails = new MailDetails
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("Authorization Error: Attempt to add mail details with an empty or invalid tenant ID.");
|
||||
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized", "Tenant ID not found or invalid.", 401));
|
||||
}
|
||||
|
||||
// 2. Input Validation (Leverage Model Validation attributes on DTO)
|
||||
if (mailDetailsDto == null)
|
||||
{
|
||||
_logger.LogWarning("Validation Error: MailDetails DTO is null. TenantId: {TenantId}", tenantId);
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Data", "Request body is empty.", 400));
|
||||
}
|
||||
|
||||
// Ensure ProjectId and Recipient are not empty
|
||||
if (mailDetailsDto.ProjectId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("Validation Error: Project ID is empty. TenantId: {TenantId}", tenantId);
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Validation Failed", "Project ID cannot be empty.", 400));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(mailDetailsDto.Recipient))
|
||||
{
|
||||
_logger.LogWarning("Validation Error: Recipient email is empty. ProjectId: {ProjectId}, TenantId: {TenantId}", mailDetailsDto.ProjectId, tenantId);
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Validation Failed", "Recipient email cannot be empty.", 400));
|
||||
}
|
||||
|
||||
// Optional: Validate email format using regex or System.Net.Mail.MailAddress
|
||||
try
|
||||
{
|
||||
var mailAddress = new MailAddress(mailDetailsDto.Recipient);
|
||||
_logger.LogInfo("nothing");
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
_logger.LogWarning("Validation Error: Invalid recipient email format '{Recipient}'. ProjectId: {ProjectId}, TenantId: {TenantId}", mailDetailsDto.Recipient, mailDetailsDto.ProjectId, tenantId);
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Validation Failed", "Invalid recipient email format.", 400));
|
||||
}
|
||||
|
||||
// 3. Validate MailListId (Foreign Key Check)
|
||||
// Ensure the MailListId refers to an existing MailBody (template) within the same tenant.
|
||||
if (mailDetailsDto.MailListId != Guid.Empty) // Only validate if a MailListId is provided
|
||||
{
|
||||
bool mailTemplateExists;
|
||||
try
|
||||
{
|
||||
mailTemplateExists = await _context.MailingList
|
||||
.AsNoTracking()
|
||||
.AnyAsync(m => m.Id == mailDetailsDto.MailListId && m.TenantId == tenantId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(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,
|
||||
Recipient = mailDetailsDto.Recipient,
|
||||
Schedule = mailDetailsDto.Schedule,
|
||||
MailListId = mailDetailsDto.MailListId,
|
||||
TenantId = tenantId
|
||||
TenantId = tenantId,
|
||||
};
|
||||
_context.MailDetails.Add(mailDetails);
|
||||
|
||||
try
|
||||
{
|
||||
_context.MailDetails.Add(newMailDetails);
|
||||
await _context.SaveChangesAsync();
|
||||
return Ok("Success");
|
||||
_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")]
|
||||
public async Task<IActionResult> AddMailTemplate([FromBody] MailTemeplateDto mailTemeplateDto)
|
||||
[HttpPost("mail-template1")]
|
||||
public async Task<IActionResult> AddMailTemplate1([FromBody] MailTemeplateDto mailTemeplateDto)
|
||||
{
|
||||
Guid tenantId = _userHelper.GetTenantId();
|
||||
if (string.IsNullOrWhiteSpace(mailTemeplateDto.Body) && string.IsNullOrWhiteSpace(mailTemeplateDto.Title))
|
||||
@ -80,116 +182,298 @@ namespace Marco.Pms.Services.Controllers
|
||||
return Ok("Success");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new mail template.
|
||||
/// </summary>
|
||||
/// <param name="mailTemplateDto">The mail template data.</param>
|
||||
/// <returns>An API response indicating success or failure.</returns>
|
||||
[HttpPost("mail-template")] // More specific route for adding a template
|
||||
public async Task<IActionResult> AddMailTemplate([FromBody] MailTemeplateDto mailTemplateDto) // Renamed parameter for consistency
|
||||
{
|
||||
// 1. Get Tenant ID and Basic Authorization Check
|
||||
Guid tenantId = _userHelper.GetTenantId();
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("Authorization Error: Attempt to add mail template with an empty or invalid tenant ID.");
|
||||
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized", "Tenant ID not found or invalid.", 401));
|
||||
}
|
||||
|
||||
// 2. Input Validation (Moved to model validation if possible, or keep explicit)
|
||||
// Use proper model validation attributes ([Required], [StringLength]) on MailTemeplateDto
|
||||
// and rely on ASP.NET Core's automatic model validation if possible.
|
||||
// If not, these checks are good.
|
||||
if (mailTemplateDto == null)
|
||||
{
|
||||
_logger.LogWarning("Validation Error: Mail template DTO is null.");
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Data", "Request body is empty.", 400));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(mailTemplateDto.Title))
|
||||
{
|
||||
_logger.LogWarning("Validation Error: Mail template title is empty or whitespace. TenantId: {TenantId}", tenantId);
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Validation Failed", "Mail template title cannot be empty.", 400));
|
||||
}
|
||||
|
||||
// The original logic checked both body and title, but often a template needs at least a title.
|
||||
// Re-evalute if body can be empty. If so, remove the body check. Assuming title is always mandatory.
|
||||
// If both body and title are empty, it's definitely invalid.
|
||||
if (string.IsNullOrWhiteSpace(mailTemplateDto.Body) && string.IsNullOrWhiteSpace(mailTemplateDto.Subject))
|
||||
{
|
||||
_logger.LogWarning("Validation Error: Mail template body and subject are both empty or whitespace for title '{Title}'. TenantId: {TenantId}", mailTemplateDto.Title, tenantId);
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Validation Failed", "Mail template body or subject must be provided.", 400));
|
||||
}
|
||||
|
||||
// 3. Check for Existing Template Title (Case-Insensitive)
|
||||
// Use AsNoTracking() for read-only query
|
||||
MailingList? existingTemplate;
|
||||
try
|
||||
{
|
||||
existingTemplate = await _context.MailingList
|
||||
.AsNoTracking() // Important for read-only checks
|
||||
.FirstOrDefaultAsync(t => t.Title.ToLower() == mailTemplateDto.Title.ToLower() && t.TenantId == tenantId); // IMPORTANT: Filter by TenantId!
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Database Error: Failed to check for existing mail template with title '{Title}' for TenantId: {TenantId}.", mailTemplateDto.Title, tenantId);
|
||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while checking for existing templates.", 500));
|
||||
}
|
||||
|
||||
|
||||
if (existingTemplate != null)
|
||||
{
|
||||
_logger.LogWarning("Conflict Error: User tries to add email template with title '{Title}' which already exists for TenantId: {TenantId}.", mailTemplateDto.Title, tenantId);
|
||||
return Conflict(ApiResponse<object>.ErrorResponse("Conflict", $"Email template with title '{mailTemplateDto.Title}' already exists.", 409));
|
||||
}
|
||||
|
||||
// 4. Create and Add New Template
|
||||
var newMailingList = new MailingList
|
||||
{
|
||||
Title = mailTemplateDto.Title,
|
||||
Body = mailTemplateDto.Body,
|
||||
Subject = mailTemplateDto.Subject,
|
||||
Keywords = mailTemplateDto.Keywords,
|
||||
TenantId = tenantId,
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
_context.MailingList.Add(newMailingList);
|
||||
await _context.SaveChangesAsync();
|
||||
_logger.LogInfo("Successfully added new mail template with ID {TemplateId} and title '{Title}' for TenantId: {TenantId}.", newMailingList.Id, newMailingList.Title, tenantId);
|
||||
|
||||
// 5. Return Success Response (201 Created is ideal for resource creation)
|
||||
// It's good practice to return the created resource or its ID.
|
||||
return StatusCode(201, ApiResponse<MailingList>.SuccessResponse(
|
||||
newMailingList, // Return the newly created object (or a DTO of it)
|
||||
"Mail template added successfully.",
|
||||
201));
|
||||
}
|
||||
catch (DbUpdateException dbEx)
|
||||
{
|
||||
_logger.LogError(dbEx, "Database Error: Failed to save new mail template '{Title}' for TenantId: {TenantId}. : {Error}", mailTemplateDto.Title, tenantId);
|
||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while saving the mail template.", 500));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unexpected Error: An unhandled exception occurred while adding mail template '{Title}' for TenantId: {TenantId}.", mailTemplateDto.Title, tenantId);
|
||||
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500));
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("project-statistics")]
|
||||
public async Task<IActionResult> SendProjectReport()
|
||||
{
|
||||
Guid tenantId = _userHelper.GetTenantId();
|
||||
|
||||
// Use AsNoTracking() for read-only queries to improve performance
|
||||
List<MailDetails> mailDetails = await _context.MailDetails
|
||||
// 1. OPTIMIZATION: Perform grouping and projection on the database server.
|
||||
// This is far more efficient than loading all entities into memory.
|
||||
var projectMailGroups = await _context.MailDetails
|
||||
.AsNoTracking()
|
||||
.Include(m => m.MailBody)
|
||||
.Where(m => m.TenantId == tenantId)
|
||||
.ToListAsync();
|
||||
|
||||
int successCount = 0;
|
||||
int notFoundCount = 0;
|
||||
int invalidIdCount = 0;
|
||||
|
||||
var groupedMails = mailDetails
|
||||
.GroupBy(m => new { m.ProjectId, m.MailListId })
|
||||
.Select(g => new
|
||||
{
|
||||
ProjectId = g.Key.ProjectId,
|
||||
MailListId = g.Key.MailListId,
|
||||
Recipients = g.Select(m => m.Recipient).Distinct().ToList(),
|
||||
MailBody = g.FirstOrDefault()?.MailBody?.Body ?? "",
|
||||
Subject = g.FirstOrDefault()?.MailBody?.Subject ?? string.Empty,
|
||||
// Project the mail body and subject from the first record in the group
|
||||
MailInfo = g.Select(m => new { Body = m.MailBody != null ? m.MailBody.Body : "", Subject = m.MailBody != null ? m.MailBody.Subject : "" }).FirstOrDefault()
|
||||
})
|
||||
.ToList();
|
||||
.ToListAsync();
|
||||
|
||||
var semaphore = new SemaphoreSlim(1);
|
||||
|
||||
// Using Task.WhenAll to send reports concurrently for better performance
|
||||
var sendTasks = groupedMails.Select(async mailDetail =>
|
||||
if (!projectMailGroups.Any())
|
||||
{
|
||||
await semaphore.WaitAsync();
|
||||
return Ok(ApiResponse<object>.SuccessResponse(new { }, "No projects found to send reports for.", 200));
|
||||
}
|
||||
|
||||
int successCount = 0;
|
||||
int notFoundCount = 0;
|
||||
int invalidIdCount = 0;
|
||||
int failureCount = 0;
|
||||
|
||||
// 2. OPTIMIZATION: Use true concurrency by removing SemaphoreSlim(1)
|
||||
// and giving each task its own isolated set of services (including DbContext).
|
||||
var sendTasks = projectMailGroups.Select(async mailGroup =>
|
||||
{
|
||||
// SOLUTION: Create a new Dependency Injection scope for each parallel task.
|
||||
using (var scope = _serviceScopeFactory.CreateScope())
|
||||
{
|
||||
// Resolve a new instance of the helper from this isolated scope.
|
||||
// This ensures each task gets its own thread-safe DbContext.
|
||||
var reportHelper = scope.ServiceProvider.GetRequiredService<ReportHelper>();
|
||||
|
||||
try
|
||||
{
|
||||
var response = await GetProjectStatistics(mailDetail.ProjectId, mailDetail.Recipients, mailDetail.MailBody, mailDetail.Subject, tenantId);
|
||||
if (response.StatusCode == 200)
|
||||
Interlocked.Increment(ref successCount);
|
||||
else if (response.StatusCode == 404)
|
||||
Interlocked.Increment(ref notFoundCount);
|
||||
else if (response.StatusCode == 400)
|
||||
Interlocked.Increment(ref invalidIdCount);
|
||||
}
|
||||
finally
|
||||
// Ensure MailInfo and ProjectId are valid before proceeding
|
||||
if (mailGroup.MailInfo == null || mailGroup.ProjectId == Guid.Empty)
|
||||
{
|
||||
semaphore.Release();
|
||||
Interlocked.Increment(ref invalidIdCount);
|
||||
return;
|
||||
}
|
||||
|
||||
var response = await reportHelper.GetProjectStatistics(
|
||||
mailGroup.ProjectId,
|
||||
mailGroup.Recipients,
|
||||
mailGroup.MailInfo.Body,
|
||||
mailGroup.MailInfo.Subject,
|
||||
tenantId);
|
||||
|
||||
// Use a switch expression for cleaner counting
|
||||
switch (response.StatusCode)
|
||||
{
|
||||
case 200: Interlocked.Increment(ref successCount); break;
|
||||
case 404: Interlocked.Increment(ref notFoundCount); break;
|
||||
case 400: Interlocked.Increment(ref invalidIdCount); break;
|
||||
default: Interlocked.Increment(ref failureCount); break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 3. OPTIMIZATION: Make the process resilient.
|
||||
// If one task fails unexpectedly, log it and continue with others.
|
||||
_logger.LogError(ex, "Failed to send report for project {ProjectId}", mailGroup.ProjectId);
|
||||
Interlocked.Increment(ref failureCount);
|
||||
}
|
||||
}
|
||||
}).ToList();
|
||||
|
||||
await Task.WhenAll(sendTasks);
|
||||
//var response = await GetProjectStatistics(Guid.Parse("2618eb89-2823-11f0-9d9e-bc241163f504"), "ashutosh.nehete@marcoaiot.com", tenantId);
|
||||
|
||||
var summaryMessage = $"Processing complete. Success: {successCount}, Not Found: {notFoundCount}, Invalid ID: {invalidIdCount}, Failures: {failureCount}.";
|
||||
|
||||
_logger.LogInfo(
|
||||
"Emails of project reports sent for tenant {TenantId}. Successfully sent: {SuccessCount}, Projects not found: {NotFoundCount}, Invalid IDs: {InvalidIdsCount}",
|
||||
tenantId, successCount, notFoundCount, invalidIdCount);
|
||||
"Project report sending complete for tenant {TenantId}. Success: {SuccessCount}, Not Found: {NotFoundCount}, Invalid ID: {InvalidIdCount}, Failures: {FailureCount}",
|
||||
tenantId, successCount, notFoundCount, invalidIdCount, failureCount);
|
||||
|
||||
return Ok(ApiResponse<object>.SuccessResponse(
|
||||
new { },
|
||||
$"Reports sent successfully: {successCount}. Projects not found: {notFoundCount}. Invalid IDs: {invalidIdCount}.",
|
||||
new { successCount, notFoundCount, invalidIdCount, failureCount },
|
||||
summaryMessage,
|
||||
200));
|
||||
}
|
||||
/// <summary>
|
||||
/// Retrieves project statistics for a given project ID and sends an email report.
|
||||
/// </summary>
|
||||
/// <param name="projectId">The ID of the project.</param>
|
||||
/// <param name="recipientEmail">The email address of the recipient.</param>
|
||||
/// <returns>An ApiResponse indicating the success or failure of retrieving statistics and sending the email.</returns>
|
||||
private async Task<ApiResponse<object>> GetProjectStatistics(Guid projectId, List<string> recipientEmails, string body, string subject, Guid tenantId)
|
||||
{
|
||||
|
||||
if (projectId == Guid.Empty)
|
||||
[HttpPost("add-report-mail")]
|
||||
public async Task<IActionResult> StoreProjectStatistics()
|
||||
{
|
||||
_logger.LogError("Provided empty project ID while fetching project report.");
|
||||
return ApiResponse<object>.ErrorResponse("Provided empty Project ID.", "Provided empty Project ID.", 400);
|
||||
Guid tenantId = _userHelper.GetTenantId();
|
||||
|
||||
// 1. Database-Side Grouping (Still the most efficient way to get initial data)
|
||||
var projectMailGroups = await _context.MailDetails
|
||||
.AsNoTracking()
|
||||
.Where(m => m.TenantId == tenantId && m.ProjectId != Guid.Empty)
|
||||
.GroupBy(m => new { m.ProjectId, m.MailListId })
|
||||
.Select(g => new
|
||||
{
|
||||
g.Key.ProjectId,
|
||||
Recipients = g.Select(m => m.Recipient).Distinct().ToList(),
|
||||
MailInfo = g.Select(m => new { Body = m.MailBody != null ? m.MailBody.Body : "", Subject = m.MailBody != null ? m.MailBody.Subject : "" }).FirstOrDefault()
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
if (!projectMailGroups.Any())
|
||||
{
|
||||
_logger.LogInfo("No project mail details found for tenant {TenantId} to process.", tenantId);
|
||||
return Ok(ApiResponse<object>.SuccessResponse("No project reports to generate.", "No project reports to generate.", 200));
|
||||
}
|
||||
|
||||
string env = _configuration["environment:Title"] ?? string.Empty;
|
||||
|
||||
var statisticReport = await _reportHelper.GetDailyProjectReport(projectId, tenantId);
|
||||
// 2. Process each group concurrently, but with isolated DBContexts.
|
||||
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("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);
|
||||
_logger.LogWarning("Statistic report for project ID {ProjectId} not found. Skipping.", projectId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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();
|
||||
if (group.MailInfo == null)
|
||||
{
|
||||
_logger.LogWarning("MailBody info for project ID {ProjectId} not found. Skipping.", projectId);
|
||||
return;
|
||||
}
|
||||
|
||||
List<MailLog> mailLogs = new List<MailLog>();
|
||||
foreach (var recipientEmail in recipientEmails)
|
||||
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
|
||||
{
|
||||
mailLogs.Add(
|
||||
new MailLog
|
||||
{
|
||||
ProjectId = projectId,
|
||||
EmailId = recipientEmail,
|
||||
IsSent = false,
|
||||
Body = emailBody,
|
||||
EmployeeId = employee.Id,
|
||||
TimeStamp = DateTime.UtcNow,
|
||||
TenantId = tenantId
|
||||
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));
|
||||
}
|
||||
|
||||
_context.MailLogs.AddRange(mailLogs);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return ApiResponse<object>.SuccessResponse(statisticReport, "Email sent successfully", 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ using Marco.Pms.Model.Mapper;
|
||||
using Marco.Pms.Model.Projects;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Marco.Pms.Model.ViewModels.Employee;
|
||||
using Marco.Pms.Services.Service.ServiceInterfaces;
|
||||
using MarcoBMS.Services.Helpers;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -19,14 +20,14 @@ namespace MarcoBMS.Services.Controllers
|
||||
private readonly UserHelper _userHelper;
|
||||
private readonly EmployeeHelper _employeeHelper;
|
||||
|
||||
private readonly ProjectsHelper _projectsHelper;
|
||||
private readonly IProjectServices _projectServices;
|
||||
private readonly RolesHelper _rolesHelper;
|
||||
|
||||
public UserController(EmployeeHelper employeeHelper, ProjectsHelper projectsHelper, UserHelper userHelper, RolesHelper rolesHelper)
|
||||
public UserController(EmployeeHelper employeeHelper, IProjectServices projectServices, UserHelper userHelper, RolesHelper rolesHelper)
|
||||
{
|
||||
_userHelper = userHelper;
|
||||
_employeeHelper = employeeHelper;
|
||||
_projectsHelper = projectsHelper;
|
||||
_projectServices = projectServices;
|
||||
_rolesHelper = rolesHelper;
|
||||
|
||||
}
|
||||
@ -50,18 +51,18 @@ namespace MarcoBMS.Services.Controllers
|
||||
emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id);
|
||||
}
|
||||
|
||||
List<FeaturePermission> featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(emp.Id);
|
||||
List<FeaturePermission> featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeId(emp.Id);
|
||||
string[] projectsId = [];
|
||||
|
||||
/* User with permission manage project can see all projects */
|
||||
if (featurePermission != null && featurePermission.Exists(c => c.Id.ToString() == "172fc9b6-755b-4f62-ab26-55c34a330614"))
|
||||
{
|
||||
List<Project> projects = await _projectsHelper.GetAllProjectByTanentID(emp.TenantId);
|
||||
List<Project> projects = await _projectServices.GetAllProjectByTanentID(emp.TenantId);
|
||||
projectsId = projects.Select(c => c.Id.ToString()).ToArray();
|
||||
}
|
||||
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();
|
||||
}
|
||||
EmployeeProfile profile = new EmployeeProfile() { };
|
||||
|
@ -1,7 +1,10 @@
|
||||
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
|
||||
@ -10,37 +13,423 @@ namespace Marco.Pms.Services.Helpers
|
||||
{
|
||||
private readonly ProjectCache _projectCache;
|
||||
private readonly EmployeeCache _employeeCache;
|
||||
private readonly ReportCache _reportCache;
|
||||
private readonly ILoggingService _logger;
|
||||
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly GeneralHelper _generalHelper;
|
||||
|
||||
public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache, ILoggingService logger)
|
||||
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 and Infrastructure Cache ---------------------------------------
|
||||
// ------------------------------------ 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(project);
|
||||
await _projectCache.AddProjectDetailsToCache(projectDetails);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning("Error occured while adding project {ProjectId} to Cache : {Error}", project.Id, ex.Message);
|
||||
_logger.LogError(ex, "Error occurred while adding project {ProjectId} to Cache", project.Id);
|
||||
}
|
||||
}
|
||||
public async Task AddProjectDetailsList(List<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);
|
||||
bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project, projectStatus);
|
||||
return response;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning("Error occured while updating project {ProjectId} to Cache: {Error}", project.Id, ex.Message);
|
||||
_logger.LogError(ex, "Error occured while updating project {ProjectId} to Cache", project.Id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -53,7 +442,20 @@ namespace Marco.Pms.Services.Helpers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning("Error occured while getting project {ProjectId} to Cache: {Error}", ex.Message);
|
||||
_logger.LogError(ex, "Error occured while getting project {ProjectId} to Cache");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public async Task<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;
|
||||
}
|
||||
}
|
||||
@ -62,14 +464,48 @@ namespace Marco.Pms.Services.Helpers
|
||||
try
|
||||
{
|
||||
var response = await _projectCache.GetProjectDetailsListFromCache(projectIds);
|
||||
if (response.Any())
|
||||
{
|
||||
return response;
|
||||
}
|
||||
catch (Exception ex)
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Error occured while getting list od project details from to Cache: {Error}", ex.Message);
|
||||
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
|
||||
@ -133,6 +569,9 @@ namespace Marco.Pms.Services.Helpers
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------- WorkItem -------------------------------------------------------
|
||||
|
||||
public async Task<List<WorkItemMongoDB>?> GetWorkItemsByWorkAreaIds(List<Guid> workAreaIds)
|
||||
{
|
||||
try
|
||||
@ -150,10 +589,20 @@ namespace Marco.Pms.Services.Helpers
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------- WorkItem -------------------------------------------------------
|
||||
|
||||
public async Task ManageWorkItemDetails(List<WorkItem> workItems)
|
||||
{
|
||||
try
|
||||
{
|
||||
var workAreaId = workItems.First().WorkAreaId;
|
||||
var workItemDB = await _generalHelper.GetWorkItemsListFromDB(workAreaId);
|
||||
await _projectCache.ManageWorkItemDetailsToCache(workItemDB);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning("Error occured while saving workItems form Cache: {Error}", ex.Message);
|
||||
}
|
||||
}
|
||||
public async Task ManageWorkItemDetailsByVM(List<WorkItemMongoDB> workItems)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -215,14 +664,47 @@ namespace Marco.Pms.Services.Helpers
|
||||
_logger.LogWarning("Error occured while updating planned work, completed work, and today's assigned work in workItems in Cache: {Error}", ex.Message);
|
||||
}
|
||||
}
|
||||
public async Task DeleteWorkItemByIdAsync(Guid workItemId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _projectCache.DeleteWorkItemByIdFromCacheAsync(workItemId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning("Error occured while deleting work item from to Cache: {Error}", ex.Message);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------ Employee Profile Cache ---------------------------------------
|
||||
public async Task AddApplicationRole(Guid employeeId, List<Guid> roleIds)
|
||||
{
|
||||
// 1. Guard Clause: Avoid unnecessary database work if there are no roles to add.
|
||||
if (roleIds == null || !roleIds.Any())
|
||||
{
|
||||
return; // Nothing to add, so the operation did not result in a change.
|
||||
}
|
||||
Task<List<string>> getPermissionIdsTask = Task.Run(async () =>
|
||||
{
|
||||
using var context = _dbContextFactory.CreateDbContext();
|
||||
|
||||
return await context.RolePermissionMappings
|
||||
.Where(rp => roleIds.Contains(rp.ApplicationRoleId))
|
||||
.Select(p => p.FeaturePermissionId.ToString())
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
});
|
||||
|
||||
// 3. Prepare role IDs in parallel with the database query.
|
||||
var newRoleIds = roleIds.Select(r => r.ToString()).ToList();
|
||||
|
||||
// 4. Await the database query result.
|
||||
var newPermissionIds = await getPermissionIdsTask;
|
||||
try
|
||||
{
|
||||
var response = await _employeeCache.AddApplicationRoleToCache(employeeId, roleIds);
|
||||
var response = await _employeeCache.AddApplicationRoleToCache(employeeId, newRoleIds, newPermissionIds);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -342,5 +824,44 @@ namespace Marco.Pms.Services.Helpers
|
||||
_logger.LogWarning("Error occured while deleting Application role {RoleId} from Cache for employee {EmployeeId}: {Error}", roleId, employeeId, ex.Message);
|
||||
}
|
||||
}
|
||||
public async Task ClearAllEmployees()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _employeeCache.ClearAllEmployeesFromCache();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error occured while deleting all employees from Cache");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------ Report Cache ---------------------------------------
|
||||
|
||||
public async Task<List<ProjectReportEmailMongoDB>?> GetProjectReportMail(bool IsSend)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _reportCache.GetProjectReportMailFromCache(IsSend);
|
||||
return response;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error occured while fetching project report mail bodys");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public async Task AddProjectReportMail(ProjectReportEmailMongoDB report)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _reportCache.AddProjectReportMailToCache(report);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error occured while adding project report mail bodys");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ namespace Marco.Pms.Services.Helpers
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Employee {EmployeeId} attemped to access a contacts, but do not have permission", LoggedInEmployee.Id);
|
||||
_logger.LogWarning("Employee {EmployeeId} attemped to access a contacts, but do not have permission", LoggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("You don't have permission", "You don't have permission", 401);
|
||||
}
|
||||
|
||||
@ -202,7 +202,7 @@ namespace Marco.Pms.Services.Helpers
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Employee {EmployeeId} attemped to access a contacts with in bucket {BucketId}, but do not have permission", LoggedInEmployee.Id, id);
|
||||
_logger.LogWarning("Employee {EmployeeId} attemped to access a contacts with in bucket {BucketId}, but do not have permission", LoggedInEmployee.Id, id);
|
||||
return ApiResponse<object>.ErrorResponse("You don't have permission", "You don't have permission", 401);
|
||||
}
|
||||
|
||||
@ -490,7 +490,7 @@ namespace Marco.Pms.Services.Helpers
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Employee {EmployeeId} attemped to update a contact, but do not have permission", LoggedInEmployee.Id);
|
||||
_logger.LogWarning("Employee {EmployeeId} attemped to update a contact, but do not have permission", LoggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("You don't have permission", "You don't have permission", 401);
|
||||
}
|
||||
|
||||
@ -1157,11 +1157,12 @@ namespace Marco.Pms.Services.Helpers
|
||||
|
||||
List<EmployeeBucketMapping> employeeBuckets = await _context.EmployeeBucketMappings.Where(b => b.EmployeeId == LoggedInEmployee.Id).ToListAsync();
|
||||
var bucketIds = employeeBuckets.Select(b => b.BucketId).ToList();
|
||||
List<EmployeeBucketMapping> employeeBucketVM = await _context.EmployeeBucketMappings.Where(b => bucketIds.Contains(b.BucketId)).ToListAsync();
|
||||
|
||||
List<Bucket> bucketList = new List<Bucket>();
|
||||
if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin))
|
||||
{
|
||||
bucketList = await _context.Buckets.Include(b => b.CreatedBy).Where(b => b.TenantId == tenantId).ToListAsync();
|
||||
bucketIds = bucketList.Select(b => b.Id).ToList();
|
||||
}
|
||||
else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) || permissionIds.Contains(PermissionsMaster.DirectoryUser))
|
||||
{
|
||||
@ -1169,10 +1170,12 @@ namespace Marco.Pms.Services.Helpers
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Employee {EmployeeId} attemped to access a buckets list, but do not have permission", LoggedInEmployee.Id);
|
||||
_logger.LogWarning("Employee {EmployeeId} attemped to access a buckets list, but do not have permission", LoggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("You don't have permission", "You don't have permission", 401);
|
||||
}
|
||||
|
||||
List<EmployeeBucketMapping> employeeBucketVM = await _context.EmployeeBucketMappings.Where(b => bucketIds.Contains(b.BucketId)).ToListAsync();
|
||||
|
||||
List<AssignBucketVM> bucketVMs = new List<AssignBucketVM>();
|
||||
if (bucketList.Any())
|
||||
{
|
||||
@ -1184,7 +1187,11 @@ namespace Marco.Pms.Services.Helpers
|
||||
var emplyeeIds = employeeBucketMappings.Select(eb => eb.EmployeeId).ToList();
|
||||
List<ContactBucketMapping>? contactBuckets = contactBucketMappings.Where(cb => cb.BucketId == bucket.Id).ToList();
|
||||
AssignBucketVM bucketVM = bucket.ToAssignBucketVMFromBucket();
|
||||
bucketVM.EmployeeIds = emplyeeIds;
|
||||
if (bucketVM.CreatedBy != null)
|
||||
{
|
||||
emplyeeIds.Add(bucketVM.CreatedBy.Id);
|
||||
}
|
||||
bucketVM.EmployeeIds = emplyeeIds.Distinct().ToList();
|
||||
bucketVM.NumberOfContacts = contactBuckets.Count;
|
||||
bucketVMs.Add(bucketVM);
|
||||
}
|
||||
@ -1204,7 +1211,7 @@ namespace Marco.Pms.Services.Helpers
|
||||
var demo = !permissionIds.Contains(PermissionsMaster.DirectoryUser);
|
||||
if (!permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && !permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && !permissionIds.Contains(PermissionsMaster.DirectoryUser))
|
||||
{
|
||||
_logger.LogError("Employee {EmployeeId} attemped to create a bucket, but do not have permission", LoggedInEmployee.Id);
|
||||
_logger.LogWarning("Employee {EmployeeId} attemped to create a bucket, but do not have permission", LoggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("You don't have permission", "You don't have permission", 401);
|
||||
}
|
||||
|
||||
@ -1276,7 +1283,7 @@ namespace Marco.Pms.Services.Helpers
|
||||
}
|
||||
if (accessableBucket == null)
|
||||
{
|
||||
_logger.LogError("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id);
|
||||
_logger.LogWarning("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id);
|
||||
return ApiResponse<object>.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 401);
|
||||
}
|
||||
|
||||
@ -1342,7 +1349,7 @@ namespace Marco.Pms.Services.Helpers
|
||||
}
|
||||
if (accessableBucket == null)
|
||||
{
|
||||
_logger.LogError("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id);
|
||||
_logger.LogWarning("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id);
|
||||
return ApiResponse<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();
|
||||
@ -1362,7 +1369,7 @@ namespace Marco.Pms.Services.Helpers
|
||||
_context.EmployeeBucketMappings.Add(employeeBucketMapping);
|
||||
assignedEmployee += 1;
|
||||
}
|
||||
else
|
||||
else if (!assignBucket.IsActive)
|
||||
{
|
||||
EmployeeBucketMapping? employeeBucketMapping = employeeBuckets.FirstOrDefault(eb => eb.BucketId == bucketId && eb.EmployeeId == assignBucket.EmployeeId);
|
||||
if (employeeBucketMapping != null)
|
||||
@ -1396,7 +1403,7 @@ namespace Marco.Pms.Services.Helpers
|
||||
}
|
||||
if (removededEmployee > 0)
|
||||
{
|
||||
_logger.LogError("Employee {EmployeeId} removed {conut} number of employees from bucket {BucketId}", LoggedInEmployee.Id, removededEmployee, bucketId);
|
||||
_logger.LogWarning("Employee {EmployeeId} removed {conut} number of employees from bucket {BucketId}", LoggedInEmployee.Id, removededEmployee, bucketId);
|
||||
}
|
||||
return ApiResponse<object>.SuccessResponse(bucketVM, "Details updated successfully", 200);
|
||||
}
|
||||
@ -1443,7 +1450,7 @@ namespace Marco.Pms.Services.Helpers
|
||||
}
|
||||
if (accessableBucket == null)
|
||||
{
|
||||
_logger.LogError("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id);
|
||||
_logger.LogWarning("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id);
|
||||
return ApiResponse<object>.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 401);
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,7 @@ namespace MarcoBMS.Services.Helpers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("{Error}", ex.Message);
|
||||
_logger.LogError(ex, "Error occured while fetching employee by application user ID {ApplicationUserId}", ApplicationUserID);
|
||||
return new Employee();
|
||||
}
|
||||
}
|
||||
@ -66,7 +66,7 @@ namespace MarcoBMS.Services.Helpers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("{Error}", ex.Message);
|
||||
_logger.LogError(ex, "Error occoured while filtering employees by string {SearchString} or project {ProjectId}", searchString, ProjectId ?? Guid.Empty);
|
||||
return new List<EmployeeVM>();
|
||||
}
|
||||
}
|
||||
@ -102,7 +102,7 @@ namespace MarcoBMS.Services.Helpers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("{Error}", ex.Message);
|
||||
_logger.LogError(ex, "Error occured while featching list of employee by project ID {ProjectId}", ProjectId ?? Guid.Empty);
|
||||
return new List<EmployeeVM>();
|
||||
}
|
||||
}
|
||||
|
214
Marco.Pms.Services/Helpers/GeneralHelper.cs
Normal file
214
Marco.Pms.Services/Helpers/GeneralHelper.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -218,7 +218,7 @@ namespace Marco.Pms.Services.Helpers
|
||||
_logger.LogInfo("Contact tag master {ConatctTagId} updated successfully by employee {EmployeeId}", contactTagVm.Id, LoggedInEmployee.Id);
|
||||
return ApiResponse<object>.SuccessResponse(contactTagVm, "Contact Tag master updated successfully", 200);
|
||||
}
|
||||
_logger.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);
|
||||
}
|
||||
_logger.LogWarning("Employee with ID {LoggedInEmployeeId} sended empty payload", LoggedInEmployee.Id);
|
||||
@ -294,7 +294,7 @@ namespace Marco.Pms.Services.Helpers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Error occurred while fetching work status list : {Error}", ex.Message);
|
||||
_logger.LogWarning("Error occurred while fetching work status list : {Error}", ex.Message);
|
||||
return ApiResponse<object>.ErrorResponse("An error occurred", "Unable to fetch work status list", 500);
|
||||
}
|
||||
}
|
||||
@ -343,7 +343,7 @@ namespace Marco.Pms.Services.Helpers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.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);
|
||||
}
|
||||
}
|
||||
@ -403,7 +403,7 @@ namespace Marco.Pms.Services.Helpers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Error occurred while updating work status ID: {Id} : {Error}", id, ex.Message);
|
||||
_logger.LogError(ex, "Error occurred while updating work status ID: {Id}", id);
|
||||
return ApiResponse<object>.ErrorResponse("An error occurred", "Unable to update the work status at this time", 500);
|
||||
}
|
||||
}
|
||||
@ -458,7 +458,7 @@ namespace Marco.Pms.Services.Helpers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -1,130 +0,0 @@
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Entitlements;
|
||||
using Marco.Pms.Model.MongoDBModels;
|
||||
using Marco.Pms.Model.Projects;
|
||||
using Marco.Pms.Services.Helpers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MarcoBMS.Services.Helpers
|
||||
{
|
||||
public class ProjectsHelper
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly RolesHelper _rolesHelper;
|
||||
private readonly CacheUpdateHelper _cache;
|
||||
|
||||
|
||||
public ProjectsHelper(ApplicationDbContext context, RolesHelper rolesHelper, CacheUpdateHelper cache)
|
||||
{
|
||||
_context = context;
|
||||
_rolesHelper = rolesHelper;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
public async Task<List<Project>> GetAllProjectByTanentID(Guid tanentID)
|
||||
{
|
||||
List<Project> alloc = await _context.Projects.Where(c => c.TenantId == tanentID).ToListAsync();
|
||||
return alloc;
|
||||
}
|
||||
|
||||
public async Task<List<ProjectAllocation>> GetProjectByEmployeeID(Guid employeeID)
|
||||
{
|
||||
List<ProjectAllocation> alloc = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeID && c.IsActive == true).Include(c => c.Project).ToListAsync();
|
||||
return alloc;
|
||||
}
|
||||
|
||||
public async Task<List<ProjectAllocation>> GetTeamByProject(Guid TenantId, Guid ProjectId, bool IncludeInactive)
|
||||
{
|
||||
if (IncludeInactive)
|
||||
{
|
||||
|
||||
var employees = await _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == ProjectId).Include(e => e.Employee).ToListAsync();
|
||||
|
||||
return employees;
|
||||
}
|
||||
else
|
||||
{
|
||||
var employees = await _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == ProjectId && c.IsActive == true).Include(e => e.Employee).ToListAsync();
|
||||
|
||||
return employees;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<Project>> GetMyProjects(Guid tenantId, Employee LoggedInEmployee)
|
||||
{
|
||||
string[] projectsId = [];
|
||||
List<Project> projects = new List<Project>();
|
||||
|
||||
var projectIds = await _cache.GetProjects(LoggedInEmployee.Id);
|
||||
|
||||
if (projectIds != null)
|
||||
{
|
||||
|
||||
List<ProjectMongoDB> projectdetails = await _cache.GetProjectDetailsList(projectIds) ?? new List<ProjectMongoDB>();
|
||||
projects = projectdetails.Select(p => new Project
|
||||
{
|
||||
Id = Guid.Parse(p.Id),
|
||||
Name = p.Name,
|
||||
ShortName = p.ShortName,
|
||||
ProjectAddress = p.ProjectAddress,
|
||||
ProjectStatusId = Guid.Parse(p.ProjectStatus?.Id ?? ""),
|
||||
ContactPerson = p.ContactPerson,
|
||||
StartDate = p.StartDate,
|
||||
EndDate = p.EndDate,
|
||||
TenantId = tenantId
|
||||
}).ToList();
|
||||
|
||||
if (projects.Count != projectIds.Count)
|
||||
{
|
||||
projects = await _context.Projects.Where(p => projectIds.Contains(p.Id)).ToListAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var featurePermissionIds = await _cache.GetPermissions(LoggedInEmployee.Id);
|
||||
if (featurePermissionIds == null)
|
||||
{
|
||||
List<FeaturePermission> featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(LoggedInEmployee.Id);
|
||||
featurePermissionIds = featurePermission.Select(fp => fp.Id).ToList();
|
||||
}
|
||||
// Define a common queryable base for projects
|
||||
IQueryable<Project> projectQuery = _context.Projects.Where(c => c.TenantId == tenantId);
|
||||
|
||||
// 2. Optimized Project Retrieval Logic
|
||||
// User with permission 'manage project' can see all projects
|
||||
if (featurePermissionIds != null && featurePermissionIds.Contains(Guid.Parse("172fc9b6-755b-4f62-ab26-55c34a330614")))
|
||||
{
|
||||
// If GetAllProjectByTanentID is already optimized and directly returns IQueryable or
|
||||
// directly executes with ToListAsync(), keep it.
|
||||
// If it does more complex logic or extra trips, consider inlining here.
|
||||
projects = await projectQuery.ToListAsync(); // Directly query the context
|
||||
}
|
||||
else
|
||||
{
|
||||
// 3. Efficiently get project allocations and then filter projects
|
||||
// Load allocations only once
|
||||
var allocation = await GetProjectByEmployeeID(LoggedInEmployee.Id);
|
||||
|
||||
// If there are no allocations, return an empty list early
|
||||
if (allocation == null || !allocation.Any())
|
||||
{
|
||||
return new List<Project>();
|
||||
}
|
||||
|
||||
// Use LINQ's Contains for efficient filtering by ProjectId
|
||||
projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); // Get distinct Guids
|
||||
|
||||
// Filter projects based on the retrieved ProjectIds
|
||||
projects = await projectQuery.Where(c => projectIds.Contains(c.Id)).ToListAsync();
|
||||
|
||||
}
|
||||
projectIds = projects.Select(p => p.Id).ToList();
|
||||
await _cache.AddProjects(LoggedInEmployee.Id, projectIds);
|
||||
}
|
||||
|
||||
return projects;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,26 +1,34 @@
|
||||
using System.Globalization;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Model.Dtos.Attendance;
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Mail;
|
||||
using Marco.Pms.Model.MongoDBModels;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Marco.Pms.Model.ViewModels.Report;
|
||||
using MarcoBMS.Services.Service;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Marco.Pms.Services.Helpers
|
||||
{
|
||||
public class ReportHelper
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly IEmailSender _emailSender;
|
||||
private readonly ILoggingService _logger;
|
||||
private readonly CacheUpdateHelper _cache;
|
||||
public ReportHelper(CacheUpdateHelper cache, ApplicationDbContext context)
|
||||
public ReportHelper(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, CacheUpdateHelper cache)
|
||||
{
|
||||
_cache = cache;
|
||||
_context = context;
|
||||
_emailSender = emailSender;
|
||||
_logger = logger;
|
||||
_cache = cache;
|
||||
}
|
||||
public async Task<ProjectStatisticReport?> GetDailyProjectReport(Guid projectId, Guid tenantId)
|
||||
{
|
||||
// await _cache.GetBuildingAndFloorByWorkAreaId();
|
||||
DateTime reportDate = DateTime.UtcNow.AddDays(-1).Date;
|
||||
var project = await _cache.GetProjectDetails(projectId);
|
||||
var project = await _cache.GetProjectDetailsWithBuildings(projectId);
|
||||
if (project == null)
|
||||
{
|
||||
var projectSQL = await _context.Projects
|
||||
@ -83,7 +91,7 @@ namespace Marco.Pms.Services.Helpers
|
||||
BuildingName = b.BuildingName,
|
||||
Description = b.Description
|
||||
}).ToList();
|
||||
if (buildings == null)
|
||||
if (!buildings.Any())
|
||||
{
|
||||
buildings = await _context.Buildings
|
||||
.Where(b => b.ProjectId == projectId)
|
||||
@ -105,7 +113,7 @@ namespace Marco.Pms.Services.Helpers
|
||||
BuildingId = f.BuildingId,
|
||||
FloorName = f.FloorName
|
||||
})).ToList();
|
||||
if (floors == null)
|
||||
if (!floors.Any())
|
||||
{
|
||||
var buildingIds = buildings.Select(b => Guid.Parse(b.Id)).ToList();
|
||||
floors = await _context.Floor
|
||||
@ -123,7 +131,7 @@ namespace Marco.Pms.Services.Helpers
|
||||
areas = project.Buildings
|
||||
.SelectMany(b => b.Floors)
|
||||
.SelectMany(f => f.WorkAreas).ToList();
|
||||
if (areas == null)
|
||||
if (!areas.Any())
|
||||
{
|
||||
var floorIds = floors.Select(f => Guid.Parse(f.Id)).ToList();
|
||||
areas = await _context.WorkAreas
|
||||
@ -141,7 +149,7 @@ namespace Marco.Pms.Services.Helpers
|
||||
|
||||
// fetch Work Items
|
||||
workItems = await _cache.GetWorkItemsByWorkAreaIds(areaIds);
|
||||
if (workItems == null)
|
||||
if (workItems == null || !workItems.Any())
|
||||
{
|
||||
workItems = await _context.WorkItems
|
||||
.Include(w => w.ActivityMaster)
|
||||
@ -270,5 +278,88 @@ namespace Marco.Pms.Services.Helpers
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/// <summary>
|
||||
/// Retrieves project statistics for a given project ID and sends an email report.
|
||||
/// </summary>
|
||||
/// <param name="projectId">The ID of the project.</param>
|
||||
/// <param name="recipientEmail">The email address of the recipient.</param>
|
||||
/// <returns>An ApiResponse indicating the success or failure of retrieving statistics and sending the email.</returns>
|
||||
public async Task<ApiResponse<object>> GetProjectStatistics(Guid projectId, List<string> recipientEmails, string body, string subject, Guid tenantId)
|
||||
{
|
||||
// --- Input Validation ---
|
||||
if (projectId == Guid.Empty)
|
||||
{
|
||||
_logger.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,41 +3,93 @@
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Model.Entitlements;
|
||||
using Marco.Pms.Services.Helpers;
|
||||
using MarcoBMS.Services.Service;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MarcoBMS.Services.Helpers
|
||||
{
|
||||
public class RolesHelper
|
||||
{
|
||||
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly CacheUpdateHelper _cache;
|
||||
public RolesHelper(ApplicationDbContext context, CacheUpdateHelper cache)
|
||||
private readonly ILoggingService _logger;
|
||||
public RolesHelper(ApplicationDbContext context, CacheUpdateHelper cache, ILoggingService logger, IDbContextFactory<ApplicationDbContext> dbContextFactory)
|
||||
{
|
||||
_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);
|
||||
|
||||
await _cache.AddApplicationRole(EmployeeID, roleMappings);
|
||||
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);
|
||||
|
||||
// _context.RolePermissionMappings
|
||||
// --- Step 2: Asynchronously update the cache using the DbContextFactory ---
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// Create a NEW, short-lived DbContext instance for this background task.
|
||||
await using var contextForCache = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
var result = await (from rpm in _context.RolePermissionMappings
|
||||
join fp in _context.FeaturePermissions.Where(c => c.IsEnabled == true).Include(fp => fp.Feature) // Include Feature
|
||||
on rpm.FeaturePermissionId equals fp.Id
|
||||
where roleMappings.Contains(rpm.ApplicationRoleId)
|
||||
select fp)
|
||||
// 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 result;
|
||||
if (roleIds.Any())
|
||||
{
|
||||
// The cache service might also need its own context, or you can pass the data directly.
|
||||
// Assuming AddApplicationRole takes the data, not a context.
|
||||
await _cache.AddApplicationRole(EmployeeId, roleIds);
|
||||
_logger.LogInfo("Successfully queued cache update for EmployeeId: {EmployeeId}", EmployeeId);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning("Background cache update failed for EmployeeId {EmployeeId} : {Error}", EmployeeId, ex.Message);
|
||||
}
|
||||
});
|
||||
|
||||
// return null;
|
||||
// --- 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();
|
||||
|
||||
@ -54,5 +106,49 @@ namespace MarcoBMS.Services.Helpers
|
||||
// return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a unique list of enabled feature permissions for a given role.
|
||||
/// This method is optimized to fetch all data in a single, efficient database query.
|
||||
/// </summary>
|
||||
/// <param name="roleId">The ID of the role.</param>
|
||||
/// <returns>A distinct list of FeaturePermission objects granted to the role.</returns>
|
||||
public async Task<List<FeaturePermission>> GetFeaturePermissionByRoleID(Guid roleId)
|
||||
{
|
||||
_logger.LogInfo("Fetching feature permissions for RoleID: {RoleId}", roleId);
|
||||
|
||||
try
|
||||
{
|
||||
// This single, efficient query gets all the required data at once.
|
||||
// It joins the mapping table to the permissions table and filters by the given roleId.
|
||||
var permissions = await (
|
||||
// 1. Start with the linking table.
|
||||
from rpm in _context.RolePermissionMappings
|
||||
|
||||
// 2. Join to the FeaturePermissions table on the foreign key.
|
||||
join fp in _context.FeaturePermissions on rpm.FeaturePermissionId equals fp.Id
|
||||
|
||||
// 3. Apply all filters in one 'where' clause for clarity and efficiency.
|
||||
where
|
||||
rpm.ApplicationRoleId == roleId // Filter by the specific role
|
||||
&& fp.IsEnabled == true // And only get enabled permissions
|
||||
|
||||
// 4. Select the final FeaturePermission object.
|
||||
select fp)
|
||||
.Include(fp => fp.Feature)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
_logger.LogInfo("Successfully retrieved {PermissionCount} unique permissions for RoleID: {RoleId}", permissions.Count, roleId);
|
||||
|
||||
return permissions;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "An error occurred while fetching permissions for RoleId {RoleId}", roleId);
|
||||
// Return an empty list as a safe default to prevent downstream failures.
|
||||
return new List<FeaturePermission>();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
68
Marco.Pms.Services/MappingProfiles/MappingProfile.cs
Normal file
68
Marco.Pms.Services/MappingProfiles/MappingProfile.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="13.0.1" />
|
||||
<PackageReference Include="AWSSDK.S3" Version="3.7.416.13" />
|
||||
<PackageReference Include="MailKit" Version="4.9.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.20" />
|
||||
|
@ -1,4 +1,3 @@
|
||||
using System.Text;
|
||||
using Marco.Pms.CacheHelper;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Model.Authentication;
|
||||
@ -7,6 +6,7 @@ using Marco.Pms.Model.Utilities;
|
||||
using Marco.Pms.Services.Helpers;
|
||||
using Marco.Pms.Services.Hubs;
|
||||
using Marco.Pms.Services.Service;
|
||||
using Marco.Pms.Services.Service.ServiceInterfaces;
|
||||
using MarcoBMS.Services.Helpers;
|
||||
using MarcoBMS.Services.Middleware;
|
||||
using MarcoBMS.Services.Service;
|
||||
@ -16,47 +16,35 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Serilog;
|
||||
|
||||
using System.Text;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
#region ======================= Service Configuration (Dependency Injection) =======================
|
||||
|
||||
#region Logging
|
||||
|
||||
// Add Serilog Configuration
|
||||
string? mongoConn = builder.Configuration["MongoDB:SerilogDatabaseUrl"];
|
||||
string timeString = "00:00:30";
|
||||
TimeSpan.TryParse(timeString, out TimeSpan timeSpan);
|
||||
|
||||
// Add Serilog Configuration
|
||||
builder.Host.UseSerilog((context, config) =>
|
||||
{
|
||||
config.ReadFrom.Configuration(context.Configuration) // Taking all configuration from appsetting.json
|
||||
config.ReadFrom.Configuration(context.Configuration)
|
||||
.WriteTo.MongoDB(
|
||||
databaseUrl: mongoConn ?? string.Empty,
|
||||
collectionName: "api-logs",
|
||||
batchPostingLimit: 100,
|
||||
period: timeSpan
|
||||
);
|
||||
|
||||
});
|
||||
#endregion
|
||||
|
||||
// Add services
|
||||
var corsSettings = builder.Configuration.GetSection("Cors");
|
||||
var allowedOrigins = corsSettings.GetValue<string>("AllowedOrigins")?.Split(',');
|
||||
var allowedMethods = corsSettings.GetValue<string>("AllowedMethods")?.Split(',');
|
||||
var allowedHeaders = corsSettings.GetValue<string>("AllowedHeaders")?.Split(',');
|
||||
|
||||
#region CORS (Cross-Origin Resource Sharing)
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("Policy", policy =>
|
||||
{
|
||||
if (allowedOrigins != null && allowedMethods != null && allowedHeaders != null)
|
||||
{
|
||||
policy.WithOrigins(allowedOrigins)
|
||||
.WithMethods(allowedMethods)
|
||||
.WithHeaders(allowedHeaders);
|
||||
}
|
||||
});
|
||||
}).AddCors(options =>
|
||||
{
|
||||
// A more permissive policy for development
|
||||
options.AddPolicy("DevCorsPolicy", policy =>
|
||||
{
|
||||
policy.AllowAnyOrigin()
|
||||
@ -64,93 +52,51 @@ builder.Services.AddCors(options =>
|
||||
.AllowAnyHeader()
|
||||
.WithExposedHeaders("Authorization");
|
||||
});
|
||||
});
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddHostedService<StartupUserSeeder>();
|
||||
// A stricter policy for production (loaded from config)
|
||||
var corsSettings = builder.Configuration.GetSection("Cors");
|
||||
var allowedOrigins = corsSettings.GetValue<string>("AllowedOrigins")?.Split(',') ?? Array.Empty<string>();
|
||||
options.AddPolicy("ProdCorsPolicy", policy =>
|
||||
{
|
||||
policy.WithOrigins(allowedOrigins)
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader();
|
||||
});
|
||||
});
|
||||
#endregion
|
||||
|
||||
#region Core Web & Framework Services
|
||||
builder.Services.AddControllers();
|
||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||
builder.Services.AddSignalR();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
builder.Services.AddSwaggerGen(option =>
|
||||
{
|
||||
option.SwaggerDoc("v1", new OpenApiInfo { Title = "Demo API", Version = "v1" });
|
||||
option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||
{
|
||||
In = ParameterLocation.Header,
|
||||
Description = "Please enter a valid token",
|
||||
Name = "Authorization",
|
||||
Type = SecuritySchemeType.Http,
|
||||
BearerFormat = "JWT",
|
||||
Scheme = "Bearer"
|
||||
});
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddMemoryCache();
|
||||
builder.Services.AddAutoMapper(typeof(Program));
|
||||
builder.Services.AddHostedService<StartupUserSeeder>();
|
||||
#endregion
|
||||
|
||||
option.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type=ReferenceType.SecurityScheme,
|
||||
Id="Bearer"
|
||||
}
|
||||
},
|
||||
new string[]{}
|
||||
}
|
||||
});
|
||||
});
|
||||
#region Database & Identity
|
||||
string? connString = builder.Configuration.GetConnectionString("DefaultConnectionString")
|
||||
?? throw new InvalidOperationException("Database connection string 'DefaultConnectionString' not found.");
|
||||
|
||||
builder.Services.Configure<SmtpSettings>(builder.Configuration.GetSection("SmtpSettings"));
|
||||
builder.Services.AddTransient<IEmailSender, EmailSender>();
|
||||
|
||||
builder.Services.Configure<AWSSettings>(builder.Configuration.GetSection("AWS")); // For uploading images to aws s3
|
||||
builder.Services.AddTransient<S3UploadService>();
|
||||
|
||||
builder.Services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();
|
||||
|
||||
|
||||
string? connString = builder.Configuration.GetConnectionString("DefaultConnectionString");
|
||||
// This single call correctly registers BOTH the DbContext (scoped) AND the IDbContextFactory (singleton).
|
||||
builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
|
||||
options.UseMySql(connString, ServerVersion.AutoDetect(connString)));
|
||||
|
||||
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
||||
{
|
||||
options.UseMySql(connString, ServerVersion.AutoDetect(connString));
|
||||
});
|
||||
options.UseMySql(connString, ServerVersion.AutoDetect(connString)));
|
||||
|
||||
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||
.AddDefaultTokenProviders();
|
||||
#endregion
|
||||
|
||||
builder.Services.AddMemoryCache();
|
||||
|
||||
|
||||
//builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
//builder.Services.AddScoped<IProjectRepository, ProjectRepository>();
|
||||
//builder.Services.AddScoped<IEmployeeRepository, EmployeeRepository>();
|
||||
//builder.Services.AddScoped<IActivityMasterRepository, ActivityMasterRepository>();
|
||||
//builder.Services.AddScoped<IAttendenceRepository, AttendenceRepository>();
|
||||
//builder.Services.AddScoped<IProjectAllocationRepository, ProjectAllocationRepository>();
|
||||
|
||||
builder.Services.AddScoped<RefreshTokenService>();
|
||||
builder.Services.AddScoped<PermissionServices>();
|
||||
|
||||
builder.Services.AddScoped<UserHelper>();
|
||||
builder.Services.AddScoped<RolesHelper>();
|
||||
builder.Services.AddScoped<EmployeeHelper>();
|
||||
builder.Services.AddScoped<ProjectsHelper>();
|
||||
builder.Services.AddScoped<DirectoryHelper>();
|
||||
builder.Services.AddScoped<MasterHelper>();
|
||||
builder.Services.AddScoped<ReportHelper>();
|
||||
builder.Services.AddScoped<CacheUpdateHelper>();
|
||||
builder.Services.AddScoped<ProjectCache>();
|
||||
builder.Services.AddScoped<EmployeeCache>();
|
||||
builder.Services.AddSingleton<ILoggingService, LoggingService>();
|
||||
|
||||
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
|
||||
#region Authentication (JWT)
|
||||
var jwtSettings = builder.Configuration.GetSection("Jwt").Get<JwtSettings>()
|
||||
?? throw new InvalidOperationException("JwtSettings section is missing or invalid.");
|
||||
|
||||
if (jwtSettings != null && jwtSettings.Key != null)
|
||||
{
|
||||
builder.Services.AddSingleton(jwtSettings);
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
@ -168,71 +114,139 @@ if (jwtSettings != null && jwtSettings.Key != null)
|
||||
ValidAudience = jwtSettings.Audience,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key))
|
||||
};
|
||||
|
||||
// This event allows SignalR to get the token from the query string
|
||||
options.Events = new JwtBearerEvents
|
||||
{
|
||||
OnMessageReceived = context =>
|
||||
{
|
||||
var accessToken = context.Request.Query["access_token"];
|
||||
var path = context.HttpContext.Request.Path;
|
||||
|
||||
// Match your hub route here
|
||||
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs/marco"))
|
||||
if (!string.IsNullOrEmpty(accessToken) && context.HttpContext.Request.Path.StartsWithSegments("/hubs/marco"))
|
||||
{
|
||||
context.Token = accessToken;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
});
|
||||
builder.Services.AddSingleton(jwtSettings);
|
||||
}
|
||||
#endregion
|
||||
|
||||
builder.Services.AddSignalR();
|
||||
#region API Documentation (Swagger)
|
||||
builder.Services.AddSwaggerGen(option =>
|
||||
{
|
||||
option.SwaggerDoc("v1", new OpenApiInfo { Title = "Marco PMS API", Version = "v1" });
|
||||
option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||
{
|
||||
In = ParameterLocation.Header,
|
||||
Description = "Please enter a valid token",
|
||||
Name = "Authorization",
|
||||
Type = SecuritySchemeType.Http,
|
||||
BearerFormat = "JWT",
|
||||
Scheme = "Bearer"
|
||||
});
|
||||
option.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" }
|
||||
},
|
||||
Array.Empty<string>()
|
||||
}
|
||||
});
|
||||
});
|
||||
#endregion
|
||||
|
||||
#region Application-Specific Services
|
||||
// Configuration-bound services
|
||||
builder.Services.Configure<SmtpSettings>(builder.Configuration.GetSection("SmtpSettings"));
|
||||
builder.Services.Configure<AWSSettings>(builder.Configuration.GetSection("AWS"));
|
||||
|
||||
// Transient services (lightweight, created each time)
|
||||
builder.Services.AddTransient<IEmailSender, EmailSender>();
|
||||
builder.Services.AddTransient<S3UploadService>();
|
||||
|
||||
// Scoped services (one instance per HTTP request)
|
||||
#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 =>
|
||||
{
|
||||
options.AddServerHeader = false; // Disable the "Server" header
|
||||
options.AddServerHeader = false; // Disable the "Server" header for security
|
||||
});
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
#region ===================== HTTP Request Pipeline Configuration =====================
|
||||
|
||||
// The order of middleware registration is critical for correct application behavior.
|
||||
|
||||
#region Global Middleware (Run First)
|
||||
// These custom middleware components run at the beginning of the pipeline to handle cross-cutting concerns.
|
||||
app.UseMiddleware<ExceptionHandlingMiddleware>();
|
||||
app.UseMiddleware<TenantMiddleware>();
|
||||
app.UseMiddleware<LoggingMiddleware>();
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
#region Development Environment Configuration
|
||||
// These tools are only enabled in the Development environment for debugging and API testing.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
// Use CORS in the pipeline
|
||||
app.UseCors("DevCorsPolicy");
|
||||
}
|
||||
else
|
||||
{
|
||||
//if (app.Environment.IsProduction())
|
||||
//{
|
||||
// app.UseCors("ProdCorsPolicy");
|
||||
//}
|
||||
#endregion
|
||||
|
||||
//app.UseCors("AllowAll");
|
||||
app.UseCors("DevCorsPolicy");
|
||||
}
|
||||
#region Standard Middleware
|
||||
// Common middleware for handling static content, security, and routing.
|
||||
app.UseStaticFiles(); // Enables serving static files (e.g., from wwwroot)
|
||||
app.UseHttpsRedirection(); // Redirects HTTP requests to HTTPS
|
||||
#endregion
|
||||
|
||||
app.UseStaticFiles(); // Enables serving static files
|
||||
#region Security (CORS, Authentication & Authorization)
|
||||
// Security-related middleware must be in the correct order.
|
||||
var corsPolicy = app.Environment.IsDevelopment() ? "DevCorsPolicy" : "ProdCorsPolicy";
|
||||
app.UseCors(corsPolicy); // CORS must be applied before Authentication/Authorization.
|
||||
|
||||
//app.UseSerilogRequestLogging(); // This is Default Serilog Logging Middleware we are not using this because we're using custom logging middleware
|
||||
app.UseAuthentication(); // 1. Identifies who the user is.
|
||||
app.UseAuthorization(); // 2. Determines what the identified user is allowed to do.
|
||||
#endregion
|
||||
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.MapHub<MarcoHub>("/hubs/marco");
|
||||
#region Endpoint Routing (Run Last)
|
||||
// These map incoming requests to the correct controller actions or SignalR hubs.
|
||||
app.MapControllers();
|
||||
app.MapHub<MarcoHub>("/hubs/marco");
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
app.Run();
|
@ -150,6 +150,8 @@ namespace MarcoBMS.Services.Service
|
||||
emailBody = emailBody.Replace("{{TEAM_ON_SITE}}", BuildTeamOnSiteHtml(report.TeamOnSite));
|
||||
emailBody = emailBody.Replace("{{PERFORMED_TASK}}", BuildPerformedTaskHtml(report.PerformedTasks, report.Date));
|
||||
emailBody = emailBody.Replace("{{PERFORMED_ATTENDANCE}}", BuildPerformedAttendanceHtml(report.PerformedAttendance));
|
||||
if (!string.IsNullOrWhiteSpace(subject))
|
||||
{
|
||||
var subjectReplacements = new Dictionary<string, string>
|
||||
{
|
||||
{"DATE", date },
|
||||
@ -161,7 +163,11 @@ namespace MarcoBMS.Services.Service
|
||||
}
|
||||
string env = _configuration["environment:Title"] ?? string.Empty;
|
||||
subject = CheckSubject(subject);
|
||||
}
|
||||
if (toEmails.Count > 0)
|
||||
{
|
||||
await SendEmailAsync(toEmails, subject, emailBody);
|
||||
}
|
||||
return emailBody;
|
||||
}
|
||||
public async Task SendOTP(List<string> toEmails, string emailBody, string name, string otp, string subject)
|
||||
|
@ -1,12 +1,11 @@
|
||||
using Serilog.Context;
|
||||
|
||||
namespace MarcoBMS.Services.Service
|
||||
namespace MarcoBMS.Services.Service
|
||||
{
|
||||
public interface ILoggingService
|
||||
{
|
||||
void LogInfo(string? message, params object[]? args);
|
||||
void LogDebug(string? message, params object[]? args);
|
||||
void LogWarning(string? message, params object[]? args);
|
||||
void LogError(string? message, params object[]? args);
|
||||
void LogError(Exception? ex, string? message, params object[]? args);
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -11,15 +11,16 @@ namespace MarcoBMS.Services.Service
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void LogError(string? message, params object[]? args)
|
||||
public void LogError(Exception? ex, string? message, params object[]? args)
|
||||
{
|
||||
using (LogContext.PushProperty("LogLevel", "Error"))
|
||||
if (args != null)
|
||||
{
|
||||
_logger.LogError(message, args);
|
||||
_logger.LogError(ex, message, args);
|
||||
}
|
||||
else {
|
||||
_logger.LogError(message);
|
||||
else
|
||||
{
|
||||
_logger.LogError(ex, message);
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,6 +36,18 @@ namespace MarcoBMS.Services.Service
|
||||
_logger.LogInformation(message);
|
||||
}
|
||||
}
|
||||
public void LogDebug(string? message, params object[]? args)
|
||||
{
|
||||
using (LogContext.PushProperty("LogLevel", "Information"))
|
||||
if (args != null)
|
||||
{
|
||||
_logger.LogDebug(message, args);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug(message);
|
||||
}
|
||||
}
|
||||
|
||||
public void LogWarning(string? message, params object[]? args)
|
||||
{
|
||||
@ -49,6 +62,5 @@ namespace MarcoBMS.Services.Service
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Entitlements;
|
||||
using Marco.Pms.Model.Projects;
|
||||
using Marco.Pms.Services.Helpers;
|
||||
using MarcoBMS.Services.Helpers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -12,13 +11,11 @@ namespace Marco.Pms.Services.Service
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly RolesHelper _rolesHelper;
|
||||
private readonly ProjectsHelper _projectsHelper;
|
||||
private readonly CacheUpdateHelper _cache;
|
||||
public PermissionServices(ApplicationDbContext context, RolesHelper rolesHelper, ProjectsHelper projectsHelper, CacheUpdateHelper cache)
|
||||
public PermissionServices(ApplicationDbContext context, RolesHelper rolesHelper, CacheUpdateHelper cache)
|
||||
{
|
||||
_context = context;
|
||||
_rolesHelper = rolesHelper;
|
||||
_projectsHelper = projectsHelper;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
@ -27,30 +24,37 @@ namespace Marco.Pms.Services.Service
|
||||
var featurePermissionIds = await _cache.GetPermissions(employeeId);
|
||||
if (featurePermissionIds == null)
|
||||
{
|
||||
List<FeaturePermission> featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(employeeId);
|
||||
List<FeaturePermission> featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeId(employeeId);
|
||||
featurePermissionIds = featurePermission.Select(fp => fp.Id).ToList();
|
||||
}
|
||||
var hasPermission = featurePermissionIds.Contains(featurePermissionId);
|
||||
return hasPermission;
|
||||
}
|
||||
public async Task<bool> HasProjectPermission(Employee emp, string projectId)
|
||||
public async Task<bool> HasProjectPermission(Employee LoggedInEmployee, Guid projectId)
|
||||
{
|
||||
List<FeaturePermission> featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(emp.Id);
|
||||
string[] projectsId = [];
|
||||
var employeeId = LoggedInEmployee.Id;
|
||||
var projectIds = await _cache.GetProjects(employeeId);
|
||||
|
||||
/* User with permission manage project can see all projects */
|
||||
if (featurePermission != null && featurePermission.Exists(c => c.Id.ToString() == "172fc9b6-755b-4f62-ab26-55c34a330614"))
|
||||
if (projectIds == null)
|
||||
{
|
||||
List<Project> projects = await _projectsHelper.GetAllProjectByTanentID(emp.TenantId);
|
||||
projectsId = projects.Select(c => c.Id.ToString()).ToArray();
|
||||
var hasPermission = await HasPermission(PermissionsMaster.ManageProject, employeeId);
|
||||
if (hasPermission)
|
||||
{
|
||||
var projects = await _context.Projects.Where(c => c.TenantId == LoggedInEmployee.TenantId).ToListAsync();
|
||||
projectIds = projects.Select(p => p.Id).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
List<ProjectAllocation> allocation = await _projectsHelper.GetProjectByEmployeeID(emp.Id);
|
||||
projectsId = allocation.Select(c => c.ProjectId.ToString()).ToArray();
|
||||
var allocation = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.IsActive).ToListAsync();
|
||||
if (!allocation.Any())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool response = projectsId.Contains(projectId);
|
||||
return response;
|
||||
projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList();
|
||||
}
|
||||
await _cache.AddProjects(LoggedInEmployee.Id, projectIds);
|
||||
}
|
||||
return projectIds.Contains(projectId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
1751
Marco.Pms.Services/Service/ProjectServices.cs
Normal file
1751
Marco.Pms.Services/Service/ProjectServices.cs
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,11 +1,11 @@
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Model.Authentication;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
|
||||
#nullable disable
|
||||
namespace MarcoBMS.Services.Service
|
||||
@ -94,7 +94,7 @@ namespace MarcoBMS.Services.Service
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("{Error}", ex.Message);
|
||||
_logger.LogError(ex, "Error occured while creating new JWT token for user {UserId}", userId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@ -132,7 +132,7 @@ namespace MarcoBMS.Services.Service
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Error creating MPIN token for userId: {UserId}, tenantId: {TenantId}, error : {Error}", userId, tenantId, ex.Message);
|
||||
_logger.LogError(ex, "Error creating MPIN token for userId: {UserId}, tenantId: {TenantId}", userId, tenantId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@ -218,7 +218,7 @@ namespace MarcoBMS.Services.Service
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Token is invalid
|
||||
_logger.LogError($"Token validation failed: {ex.Message}");
|
||||
_logger.LogError(ex, "Token validation failed");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("{error} while uploading file to S3", ex.Message);
|
||||
_logger.LogError(ex, "error occured while uploading file to S3");
|
||||
}
|
||||
|
||||
|
||||
@ -87,7 +87,7 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("{error} while requesting presigned url from Amazon S3", ex.Message);
|
||||
_logger.LogError(ex, "error occured while requesting presigned url from Amazon S3", ex.Message);
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
@ -107,7 +107,7 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("{error} while deleting from Amazon S3", ex.Message);
|
||||
_logger.LogError(ex, "error ocured while deleting from Amazon S3");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -202,7 +202,7 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Warning: Could not find MimeType, Type, or ContentType property in Definition.");
|
||||
_logger.LogWarning("Warning: Could not find MimeType, Type, or ContentType property in Definition.");
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
@ -211,16 +211,16 @@ namespace Marco.Pms.Services.Service
|
||||
return "application/octet-stream"; // Default if type cannot be determined
|
||||
}
|
||||
}
|
||||
catch (FormatException)
|
||||
catch (FormatException fEx)
|
||||
{
|
||||
// Handle cases where the input string is not valid Base64
|
||||
_logger.LogError("Invalid Base64 string.");
|
||||
_logger.LogError(fEx, "Invalid Base64 string.");
|
||||
return string.Empty;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle other potential errors during decoding or inspection
|
||||
_logger.LogError($"An error occurred: {ex.Message}");
|
||||
_logger.LogError(ex, "errors during decoding or inspection");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace Marco.Pms.Services.Service.ServiceInterfaces
|
||||
{
|
||||
public interface ISignalRService
|
||||
{
|
||||
Task SendNotificationAsync(object notification);
|
||||
}
|
||||
}
|
29
Marco.Pms.Services/Service/SignalRService.cs
Normal file
29
Marco.Pms.Services/Service/SignalRService.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user