diff --git a/.gitignore b/.gitignore index 9491a2f..a6a47c3 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,7 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd + +# Sonar +/.sonarqube \ No newline at end of file diff --git a/Marco.Pms.CacheHelper/EmployeeCache.cs b/Marco.Pms.CacheHelper/EmployeeCache.cs new file mode 100644 index 0000000..7c7f4b4 --- /dev/null +++ b/Marco.Pms.CacheHelper/EmployeeCache.cs @@ -0,0 +1,204 @@ +using Marco.Pms.Model.MongoDBModels; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using MongoDB.Driver; + +namespace Marco.Pms.CacheHelper +{ + public class EmployeeCache + { + private readonly IMongoCollection _collection; + public EmployeeCache(IConfiguration configuration) + { + var connectionString = configuration["MongoDB:ConnectionString"]; + var mongoUrl = new MongoUrl(connectionString); + var client = new MongoClient(mongoUrl); // Your MongoDB connection string + var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name + _collection = mongoDB.GetCollection("EmployeeProfile"); + } + public async Task AddApplicationRoleToCache(Guid employeeId, List newRoleIds, List newPermissionIds) + { + + // 2. Perform database queries concurrently for better performance. + var employeeIdString = employeeId.ToString(); + + // 5. Build a single, efficient update operation. + var filter = Builders.Filter.Eq(e => e.Id, employeeIdString); + + var update = Builders.Update + .AddToSetEach(e => e.ApplicationRoleIds, newRoleIds) + .AddToSetEach(e => e.PermissionIds, newPermissionIds); + + var options = new UpdateOptions { IsUpsert = true }; + + var result = await _collection.UpdateOneAsync(filter, update, options); + + await InitializeCollectionAsync(); + + // 6. Return a more accurate result indicating success for both updates and upserts. + // The operation is successful if an existing document was modified OR a new one was created. + return result.IsAcknowledged && (result.ModifiedCount > 0 || result.UpsertedId != null); + } + public async Task AddProjectsToCache(Guid employeeId, List projectIds) + { + var newprojectIds = projectIds.Select(p => p.ToString()).ToList(); + + var filter = Builders.Filter.Eq(e => e.Id, employeeId.ToString()); + + var update = Builders.Update + .AddToSetEach(e => e.ProjectIds, newprojectIds); + + var result = await _collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true }); + if (result.MatchedCount == 0) + { + return false; + } + await InitializeCollectionAsync(); + return true; + } + public async Task> GetProjectsFromCache(Guid employeeId) + { + var filter = Builders.Filter.Eq(e => e.Id, employeeId.ToString()); + + + var result = await _collection + .Find(filter) + .FirstOrDefaultAsync(); + + var projectIds = new List(); + if (result != null) + { + projectIds = result.ProjectIds.Select(Guid.Parse).ToList(); + } + + return projectIds; + } + public async Task> GetPermissionsFromCache(Guid employeeId) + { + var filter = Builders.Filter.Eq(e => e.Id, employeeId.ToString()); + + + var result = await _collection + .Find(filter) + .FirstOrDefaultAsync(); + + var permissionIds = new List(); + if (result != null) + { + permissionIds = result.PermissionIds.Select(Guid.Parse).ToList(); + } + + return permissionIds; + } + public async Task ClearAllProjectIdsFromCache(Guid employeeId) + { + var filter = Builders.Filter + .Eq(e => e.Id, employeeId.ToString()); + + var update = Builders.Update + .Set(e => e.ProjectIds, new List()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.ModifiedCount == 0) + return false; + + return true; + } + public async Task ClearAllProjectIdsByRoleIdFromCache(Guid roleId) + { + var filter = Builders.Filter.AnyEq(e => e.ApplicationRoleIds, roleId.ToString()); + + var update = Builders.Update + .Set(e => e.ProjectIds, new List()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + return true; + } + public async Task ClearAllProjectIdsByPermissionIdFromCache(Guid permissionId) + { + var filter = Builders.Filter.AnyEq(e => e.PermissionIds, permissionId.ToString()); + var update = Builders.Update.Set(e => e.ProjectIds, new List()); + + var result = await _collection.UpdateManyAsync(filter, update).ConfigureAwait(false); + return result.IsAcknowledged && result.ModifiedCount > 0; + } + public async Task RemoveRoleIdFromCache(Guid employeeId, Guid roleId) + { + var filter = Builders.Filter + .Eq(e => e.Id, employeeId.ToString()); + + var update = Builders.Update + .Pull(e => e.ApplicationRoleIds, roleId.ToString()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + if (result.ModifiedCount == 0) + return false; + + return true; + } + public async Task ClearAllPermissionIdsByEmployeeIDFromCache(Guid employeeId) + { + var filter = Builders.Filter + .Eq(e => e.Id, employeeId.ToString()); + + var update = Builders.Update + .Set(e => e.PermissionIds, new List()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + return true; + } + public async Task ClearAllPermissionIdsByRoleIdFromCache(Guid roleId) + { + var filter = Builders.Filter.AnyEq(e => e.ApplicationRoleIds, roleId.ToString()); + + var update = Builders.Update + .Set(e => e.PermissionIds, new List()); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + return false; + + return true; + } + public async Task ClearAllEmployeesFromCache() + { + var result = await _collection.DeleteManyAsync(FilterDefinition.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.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(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); + } + } +} diff --git a/Marco.Pms.CacheHelper/Marco.Pms.CacheHelper.csproj b/Marco.Pms.CacheHelper/Marco.Pms.CacheHelper.csproj new file mode 100644 index 0000000..e12ac6c --- /dev/null +++ b/Marco.Pms.CacheHelper/Marco.Pms.CacheHelper.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs new file mode 100644 index 0000000..10eb623 --- /dev/null +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -0,0 +1,506 @@ +using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.Master; +using Marco.Pms.Model.MongoDBModels; +using Marco.Pms.Model.Projects; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Marco.Pms.CacheHelper +{ + public class ProjectCache + { + private readonly IMongoCollection _projectCollection; + private readonly IMongoCollection _taskCollection; + public ProjectCache(ApplicationDbContext context, IConfiguration configuration) + { + var connectionString = configuration["MongoDB:ConnectionString"]; + var mongoUrl = new MongoUrl(connectionString); + var client = new MongoClient(mongoUrl); // Your MongoDB connection string + var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name + _projectCollection = mongoDB.GetCollection("ProjectDetails"); + _taskCollection = mongoDB.GetCollection("WorkItemDetails"); + } + + #region=================================================================== Project Cache Helper =================================================================== + + public async Task AddProjectDetailsToCache(ProjectMongoDB projectDetails) + { + await _projectCollection.InsertOneAsync(projectDetails); + + var indexKeys = Builders.IndexKeys.Ascending(x => x.ExpireAt); + var indexOptions = new CreateIndexOptions + { + ExpireAfter = TimeSpan.Zero // required for fixed expiration time + }; + var indexModel = new CreateIndexModel(indexKeys, indexOptions); + await _projectCollection.Indexes.CreateOneAsync(indexModel); + + } + public async Task AddProjectDetailsListToCache(List projectDetailsList) + { + // 1. Add a guard clause to avoid an unnecessary database call for an empty list. + if (projectDetailsList == null || !projectDetailsList.Any()) + { + return; + } + + // 2. Perform the insert operation. This is the only responsibility of this method. + await _projectCollection.InsertManyAsync(projectDetailsList); + await InitializeCollectionAsync(); + } + private async Task InitializeCollectionAsync() + { + // 1. Define the TTL (Time-To-Live) index on the 'ExpireAt' field. + var indexKeys = Builders.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(indexKeys, indexOptions); + + // 2. Create the index. This is an idempotent operation if the index already exists. + // Use CreateOneAsync since we are only creating a single index. + await _projectCollection.Indexes.CreateOneAsync(indexModel); + } + public async Task UpdateProjectDetailsOnlyToCache(Project project, StatusMaster projectStatus) + { + // Build the update definition + var updates = Builders.Update.Combine( + Builders.Update.Set(r => r.Name, project.Name), + Builders.Update.Set(r => r.ProjectAddress, project.ProjectAddress), + Builders.Update.Set(r => r.ShortName, project.ShortName), + Builders.Update.Set(r => r.ProjectStatus, new StatusMasterMongoDB + { + Id = projectStatus.Id.ToString(), + Status = projectStatus.Status + }), + Builders.Update.Set(r => r.StartDate, project.StartDate), + Builders.Update.Set(r => r.EndDate, project.EndDate), + Builders.Update.Set(r => r.ContactPerson, project.ContactPerson) + ); + + // Perform the update + var result = await _projectCollection.UpdateOneAsync( + filter: r => r.Id == project.Id.ToString(), + update: updates + ); + + if (result.MatchedCount == 0) + { + return false; + } + + return true; + } + public async Task GetProjectDetailsFromCache(Guid projectId) + { + + // Build filter and projection to exclude large 'Buildings' list + var filter = Builders.Filter.Eq(p => p.Id, projectId.ToString()); + var projection = Builders.Projection.Exclude(p => p.Buildings); + + // Perform query + var project = await _projectCollection + .Find(filter) + .Project(projection) + .FirstOrDefaultAsync(); + + return project; + } + public async Task GetProjectDetailsWithBuildingsFromCache(Guid projectId) + { + + // Build filter and projection to exclude large 'Buildings' list + var filter = Builders.Filter.Eq(p => p.Id, projectId.ToString()); + + // Perform query + var project = await _projectCollection + .Find(filter) + .FirstOrDefaultAsync(); + + return project; + } + public async Task> GetProjectDetailsListFromCache(List projectIds) + { + List stringProjectIds = projectIds.Select(p => p.ToString()).ToList(); + var filter = Builders.Filter.In(p => p.Id, stringProjectIds); + var projection = Builders.Projection.Exclude(p => p.Buildings); + var projects = await _projectCollection + .Find(filter) + .Project(projection) + .ToListAsync(); + return projects; + } + public async Task DeleteProjectByIdFromCacheAsync(Guid projectId) + { + var filter = Builders.Filter.Eq(e => e.Id, projectId.ToString()); + var result = await _projectCollection.DeleteOneAsync(filter); + return result.DeletedCount > 0; + } + public async Task RemoveProjectsFromCacheAsync(List projectIds) + { + var stringIds = projectIds.Select(id => id.ToString()).ToList(); + var filter = Builders.Filter.In(p => p.Id, stringIds); + var result = await _projectCollection.DeleteManyAsync(filter); + return result.DeletedCount > 0; + } + + #endregion + + #region=================================================================== Project infrastructure Cache Helper =================================================================== + + public async Task AddBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId) + { + var stringProjectId = projectId.ToString(); + + // Add Building + if (building != null) + { + var buildingMongo = new BuildingMongoDB + { + Id = building.Id.ToString(), + BuildingName = building.Name, + Description = building.Description, + PlannedWork = 0, + CompletedWork = 0, + Floors = new List() + }; + + var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); + var update = Builders.Update.Push("Buildings", buildingMongo); + + var result = await _projectCollection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + { + return; + } + return; + } + + // Add Floor + if (floor != null) + { + var floorMongo = new FloorMongoDB + { + Id = floor.Id.ToString(), + FloorName = floor.FloorName, + PlannedWork = 0, + CompletedWork = 0, + WorkAreas = new List() + }; + + var filter = Builders.Filter.And( + Builders.Filter.Eq(p => p.Id, stringProjectId), + Builders.Filter.Eq("Buildings._id", floor.BuildingId.ToString()) + ); + + var update = Builders.Update.Push("Buildings.$.Floors", floorMongo); + var result = await _projectCollection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + { + return; + } + return; + } + + // Add WorkArea + if (workArea != null && buildingId != null) + { + var workAreaMongo = new WorkAreaMongoDB + { + Id = workArea.Id.ToString(), + AreaName = workArea.AreaName, + PlannedWork = 0, + CompletedWork = 0 + }; + + var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); + + var arrayFilters = new List + { + new JsonArrayFilterDefinition("{ 'b._id': '" + buildingId + "' }"), + new JsonArrayFilterDefinition("{ 'f._id': '" + workArea.FloorId + "' }") + }; + + var update = Builders.Update.Push("Buildings.$[b].Floors.$[f].WorkAreas", workAreaMongo); + var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; + + var result = await _projectCollection.UpdateOneAsync(filter, update, updateOptions); + + if (result.MatchedCount == 0) + { + return; + } + return; + } + } + public async Task UpdateBuildngInfraToCache(Guid projectId, Building? building, Floor? floor, WorkArea? workArea, Guid? buildingId) + { + var stringProjectId = projectId.ToString(); + + // Update Building + if (building != null) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(p => p.Id, stringProjectId), + Builders.Filter.Eq("Buildings._id", building.Id.ToString()) + ); + + var update = Builders.Update.Combine( + Builders.Update.Set("Buildings.$.BuildingName", building.Name), + Builders.Update.Set("Buildings.$.Description", building.Description) + ); + + var result = await _projectCollection.UpdateOneAsync(filter, update); + + if (result.MatchedCount == 0) + { + return false; + } + + return true; + } + + // Update Floor + if (floor != null) + { + var arrayFilters = new List + { + new JsonArrayFilterDefinition("{ 'b._id': '" + floor.BuildingId + "' }"), + new JsonArrayFilterDefinition("{ 'f._id': '" + floor.Id + "' }") + }; + + var update = Builders.Update.Set("Buildings.$[b].Floors.$[f].FloorName", floor.FloorName); + var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; + var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); + + var result = await _projectCollection.UpdateOneAsync(filter, update, updateOptions); + + if (result.MatchedCount == 0) + { + return false; + } + return true; + } + + // Update WorkArea + if (workArea != null && buildingId != null) + { + var arrayFilters = new List + { + new JsonArrayFilterDefinition("{ 'b._id': '" + buildingId + "' }"), + new JsonArrayFilterDefinition("{ 'f._id': '" + workArea.FloorId + "' }"), + new JsonArrayFilterDefinition("{ 'a._id': '" + workArea.Id + "' }") + }; + + var update = Builders.Update.Set("Buildings.$[b].Floors.$[f].WorkAreas.$[a].AreaName", workArea.AreaName); + var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; + var filter = Builders.Filter.Eq(p => p.Id, stringProjectId); + + var result = await _projectCollection.UpdateOneAsync(filter, update, updateOptions); + + if (result.MatchedCount == 0) + { + return false; + } + return true; + } + return false; + } + public async Task?> GetBuildingInfraFromCache(Guid projectId) + { + + // Filter by project ID + var filter = Builders.Filter.Eq(p => p.Id, projectId.ToString()); + + // Project only the "Buildings" field from the document + var buildings = await _projectCollection + .Find(filter) + .Project(p => p.Buildings) + .FirstOrDefaultAsync(); + + return buildings; + } + public async Task UpdatePlannedAndCompleteWorksInBuildingFromCache(Guid workAreaId, double plannedWork, double completedWork) + { + var filter = Builders.Filter.Eq("Buildings.Floors.WorkAreas._id", workAreaId.ToString()); + var project = await _projectCollection.Find(filter).FirstOrDefaultAsync(); + + string? selectedBuildingId = null; + string? selectedFloorId = null; + string? selectedWorkAreaId = null; + + foreach (var building in project.Buildings) + { + foreach (var floor in building.Floors) + { + foreach (var area in floor.WorkAreas) + { + if (area.Id == workAreaId.ToString()) + { + selectedWorkAreaId = area.Id; + selectedFloorId = floor.Id; + selectedBuildingId = building.Id; + } + } + } + } + + var arrayFilters = new List + { + new JsonArrayFilterDefinition("{ 'b._id': '" + selectedBuildingId + "' }"), + new JsonArrayFilterDefinition("{ 'f._id': '" + selectedFloorId + "' }"), + new JsonArrayFilterDefinition("{ 'a._id': '" + selectedWorkAreaId + "' }") + }; + var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; + var update = Builders.Update + .Inc("Buildings.$[b].Floors.$[f].WorkAreas.$[a].PlannedWork", plannedWork) + .Inc("Buildings.$[b].Floors.$[f].WorkAreas.$[a].CompletedWork", completedWork) + .Inc("Buildings.$[b].Floors.$[f].PlannedWork", plannedWork) + .Inc("Buildings.$[b].Floors.$[f].CompletedWork", completedWork) + .Inc("Buildings.$[b].PlannedWork", plannedWork) + .Inc("Buildings.$[b].CompletedWork", completedWork) + .Inc("PlannedWork", plannedWork) + .Inc("CompletedWork", completedWork); + var result = await _projectCollection.UpdateOneAsync(filter, update, updateOptions); + + } + public async Task GetBuildingAndFloorByWorkAreaIdFromCache(Guid workAreaId) + { + var pipeline = new[] + { + new BsonDocument("$unwind", "$Buildings"), + new BsonDocument("$unwind", "$Buildings.Floors"), + new BsonDocument("$unwind", "$Buildings.Floors.WorkAreas"), + new BsonDocument("$match", new BsonDocument("Buildings.Floors.WorkAreas._id", workAreaId.ToString())), + new BsonDocument("$project", new BsonDocument + { + { "_id", 0 }, + { "ProjectId", "$_id" }, + { "ProjectName", "$Name" }, + { "PlannedWork", "$PlannedWork" }, + { "CompletedWork", "$CompletedWork" }, + { + "Building", new BsonDocument + { + { "_id", "$Buildings._id" }, + { "BuildingName", "$Buildings.BuildingName" }, + { "Description", "$Buildings.Description" }, + { "PlannedWork", "$Buildings.PlannedWork" }, + { "CompletedWork", "$Buildings.CompletedWork" } + } + }, + { + "Floor", new BsonDocument + { + { "_id", "$Buildings.Floors._id" }, + { "FloorName", "$Buildings.Floors.FloorName" }, + { "PlannedWork", "$Buildings.Floors.PlannedWork" }, + { "CompletedWork", "$Buildings.Floors.CompletedWork" } + } + }, + { "WorkArea", "$Buildings.Floors.WorkAreas" } + }) + }; + var result = await _projectCollection.Aggregate(pipeline).FirstOrDefaultAsync(); + if (result == null) + return null; + return result; + } + + #endregion + + #region=================================================================== WorkItem Cache Helper =================================================================== + + public async Task> GetWorkItemsByWorkAreaIdsFromCache(List workAreaIds) + { + var stringWorkAreaIds = workAreaIds.Select(wa => wa.ToString()).ToList(); + var filter = Builders.Filter.In(w => w.WorkAreaId, stringWorkAreaIds); + + var workItems = await _taskCollection // replace with your actual collection name + .Find(filter) + .ToListAsync(); + + return workItems; + } + public async Task ManageWorkItemDetailsToCache(List workItems) + { + foreach (WorkItemMongoDB workItem in workItems) + { + var filter = Builders.Filter.Eq(p => p.Id, workItem.Id.ToString()); + var updates = Builders.Update.Combine( + Builders.Update.Set(r => r.WorkAreaId, workItem.WorkAreaId.ToString()), + Builders.Update.Set(r => r.ParentTaskId, (workItem.ParentTaskId != null ? workItem.ParentTaskId.ToString() : null)), + Builders.Update.Set(r => r.PlannedWork, workItem.PlannedWork), + Builders.Update.Set(r => r.TodaysAssigned, workItem.TodaysAssigned), + Builders.Update.Set(r => r.CompletedWork, workItem.CompletedWork), + Builders.Update.Set(r => r.Description, workItem.Description), + Builders.Update.Set(r => r.TaskDate, workItem.TaskDate), + Builders.Update.Set(r => r.ExpireAt, DateTime.UtcNow.Date.AddDays(1)), + Builders.Update.Set(r => r.ActivityMaster, workItem.ActivityMaster), + Builders.Update.Set(r => r.WorkCategoryMaster, workItem.WorkCategoryMaster) + ); + var options = new UpdateOptions { IsUpsert = true }; + var result = await _taskCollection.UpdateOneAsync(filter, updates, options); + if (result.UpsertedId != null) + { + var indexKeys = Builders.IndexKeys.Ascending(x => x.ExpireAt); + var indexOptions = new CreateIndexOptions + { + ExpireAfter = TimeSpan.Zero // required for fixed expiration time + }; + var indexModel = new CreateIndexModel(indexKeys, indexOptions); + await _taskCollection.Indexes.CreateOneAsync(indexModel); + } + } + } + public async Task> GetWorkItemDetailsByWorkAreaFromCache(Guid workAreaId) + { + var filter = Builders.Filter.Eq(p => p.WorkAreaId, workAreaId.ToString()); + + var options = new UpdateOptions { IsUpsert = true }; + var workItems = await _taskCollection + .Find(filter) + .ToListAsync(); + return workItems; + } + public async Task GetWorkItemDetailsByIdFromCache(Guid id) + { + var filter = Builders.Filter.Eq(p => p.Id, id.ToString()); + + var options = new UpdateOptions { IsUpsert = true }; + var workItem = await _taskCollection + .Find(filter) + .FirstOrDefaultAsync(); + return workItem; + } + public async Task UpdatePlannedAndCompleteWorksInWorkItemToCache(Guid id, double plannedWork, double completedWork, double todaysAssigned) + { + var filter = Builders.Filter.Eq(p => p.Id, id.ToString()); + var updates = Builders.Update + .Inc("PlannedWork", plannedWork) + .Inc("CompletedWork", completedWork) + .Inc("TodaysAssigned", todaysAssigned); + + var result = await _taskCollection.UpdateOneAsync(filter, updates); + if (result.ModifiedCount > 0) + { + return true; + } + return false; + } + public async Task DeleteWorkItemByIdFromCacheAsync(Guid workItemId) + { + var filter = Builders.Filter.Eq(e => e.Id, workItemId.ToString()); + var result = await _taskCollection.DeleteOneAsync(filter); + return result.DeletedCount > 0; + } + + #endregion + } +} diff --git a/Marco.Pms.CacheHelper/ReportCache.cs b/Marco.Pms.CacheHelper/ReportCache.cs new file mode 100644 index 0000000..66611a8 --- /dev/null +++ b/Marco.Pms.CacheHelper/ReportCache.cs @@ -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 _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("ProjectReportMail"); + } + + /// + /// Retrieves project report emails from the cache based on their sent status. + /// + /// True to get sent reports, false to get unsent reports. + /// A list of ProjectReportEmailMongoDB objects. + public async Task> GetProjectReportMailFromCache(bool isSent) + { + var filter = Builders.Filter.Eq(p => p.IsSent, isSent); + var reports = await _projectReportCollection.Find(filter).ToListAsync(); + return reports; + } + + /// + /// Adds a project report email to the cache. + /// + /// The ProjectReportEmailMongoDB object to add. + /// A Task representing the asynchronous operation. + public async Task AddProjectReportMailToCache(ProjectReportEmailMongoDB report) + { + // Consider adding validation or logging here. + await _projectReportCollection.InsertOneAsync(report); + } + } +} diff --git a/Marco.Pms.DataAccess/Migrations/20250702042830_Added_UploadedBy_ForeginKey_In_Decuments_Table.Designer.cs b/Marco.Pms.DataAccess/Migrations/20250702042830_Added_UploadedBy_ForeginKey_In_Decuments_Table.Designer.cs new file mode 100644 index 0000000..c0c77b8 --- /dev/null +++ b/Marco.Pms.DataAccess/Migrations/20250702042830_Added_UploadedBy_ForeginKey_In_Decuments_Table.Designer.cs @@ -0,0 +1,3426 @@ +// +using System; +using Marco.Pms.DataAccess.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Marco.Pms.DataAccess.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250702042830_Added_UploadedBy_ForeginKey_In_Decuments_Table")] + partial class Added_UploadedBy_ForeginKey_In_Decuments_Table + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + //MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskAllocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApprovedById") + .HasColumnType("char(36)"); + + b.Property("ApprovedDate") + .HasColumnType("datetime(6)"); + + b.Property("AssignedBy") + .HasColumnType("char(36)"); + + b.Property("AssignmentDate") + .HasColumnType("datetime(6)"); + + b.Property("CompletedTask") + .HasColumnType("double"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ParentTaskId") + .HasColumnType("char(36)"); + + b.Property("PlannedTask") + .HasColumnType("double"); + + b.Property("ReportedById") + .HasColumnType("char(36)"); + + b.Property("ReportedDate") + .HasColumnType("datetime(6)"); + + b.Property("ReportedTask") + .HasColumnType("double"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("WorkItemId") + .HasColumnType("char(36)"); + + b.Property("WorkStatusId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedById"); + + b.HasIndex("AssignedBy"); + + b.HasIndex("ReportedById"); + + b.HasIndex("TenantId"); + + b.HasIndex("WorkItemId"); + + b.HasIndex("WorkStatusId"); + + b.ToTable("TaskAllocations"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("DocumentId") + .HasColumnType("char(36)"); + + b.Property("ReferenceId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("TaskAttachments"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CommentDate") + .HasColumnType("datetime(6)"); + + b.Property("CommentedBy") + .HasColumnType("char(36)"); + + b.Property("TaskAllocationId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("CommentedBy"); + + b.HasIndex("TaskAllocationId"); + + b.HasIndex("TenantId"); + + b.ToTable("TaskComments"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskMembers", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("EmployeeId") + .HasColumnType("char(36)"); + + b.Property("TaskAllocationId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("TaskAllocationId"); + + b.HasIndex("TenantId"); + + b.ToTable("TaskMembers"); + }); + + modelBuilder.Entity("Marco.Pms.Model.AttendanceModule.Attendance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Activity") + .HasColumnType("int"); + + b.Property("ApprovedBy") + .HasColumnType("char(36)"); + + b.Property("AttendanceDate") + .HasColumnType("datetime(6)"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("EmployeeID") + .HasColumnType("char(36)"); + + b.Property("InTime") + .HasColumnType("datetime(6)"); + + b.Property("IsApproved") + .HasColumnType("tinyint(1)"); + + b.Property("OutTime") + .HasColumnType("datetime(6)"); + + b.Property("ProjectID") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeID"); + + b.HasIndex("TenantId"); + + b.ToTable("Attendes"); + }); + + modelBuilder.Entity("Marco.Pms.Model.AttendanceModule.AttendanceLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Activity") + .HasColumnType("int"); + + b.Property("ActivityTime") + .HasColumnType("datetime(6)"); + + b.Property("AttendanceId") + .HasColumnType("char(36)"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("char(36)"); + + b.Property("EmployeeID") + .HasColumnType("char(36)"); + + b.Property("Latitude") + .HasColumnType("longtext"); + + b.Property("Longitude") + .HasColumnType("longtext"); + + b.Property("Photo") + .HasColumnType("longblob"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("UpdatedBy") + .HasColumnType("char(36)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AttendanceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("EmployeeID"); + + b.HasIndex("TenantId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("AttendanceLogs"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Authentication.MPINDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("MPIN") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MPINToken") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("TimeStamp") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("MPINDetails"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Authentication.OTPDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ExpriesInSec") + .HasColumnType("int"); + + b.Property("IsUsed") + .HasColumnType("tinyint(1)"); + + b.Property("OTP") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("TimeStamp") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("OTPDetails"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Authentication.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExpiryDate") + .HasColumnType("datetime(6)"); + + b.Property("IsRevoked") + .HasColumnType("tinyint(1)"); + + b.Property("IsUsed") + .HasColumnType("tinyint(1)"); + + b.Property("RevokedAt") + .HasColumnType("datetime(6)"); + + b.Property("Token") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.Bucket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedByID") + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByID"); + + b.HasIndex("TenantId"); + + b.ToTable("Buckets"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Address") + .HasColumnType("longtext"); + + b.Property("ContactCategoryId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedById") + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Organization") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedById") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ContactCategoryId"); + + b.HasIndex("CreatedById"); + + b.HasIndex("TenantId"); + + b.HasIndex("UpdatedById"); + + b.ToTable("Contacts"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactBucketMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BucketId") + .HasColumnType("char(36)"); + + b.Property("ContactId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("BucketId"); + + b.HasIndex("ContactId"); + + b.ToTable("ContactBucketMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactCategoryMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("ContactCategoryMasters"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactEmail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactId") + .HasColumnType("char(36)"); + + b.Property("EmailAddress") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)"); + + b.Property("Label") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.ToTable("ContactsEmails"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedById") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Note") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedById") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.HasIndex("CreatedById"); + + b.HasIndex("TenantId"); + + b.HasIndex("UpdatedById"); + + b.ToTable("ContactNotes"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactPhone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactId") + .HasColumnType("char(36)"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)"); + + b.Property("Label") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.ToTable("ContactsPhones"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactProjectMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactId") + .HasColumnType("char(36)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("TenantId"); + + b.ToTable("ContactProjectMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactTagMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactId") + .HasColumnType("char(36)"); + + b.Property("ContactTagId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.HasIndex("ContactTagId"); + + b.ToTable("ContactTagMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactTagMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("ContactTagMasters"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.DirectoryUpdateLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("RefereanceId") + .HasColumnType("char(36)"); + + b.Property("UpdateAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedById") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UpdatedById"); + + b.ToTable("DirectoryUpdateLogs"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.EmployeeBucketMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BucketId") + .HasColumnType("char(36)"); + + b.Property("EmployeeId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("BucketId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("EmployeeBucketMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.DocumentManager.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Base64Data") + .HasColumnType("longtext"); + + b.Property("BatchId") + .HasColumnType("char(36)"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("S3Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("ThumbS3Key") + .HasColumnType("longtext"); + + b.Property("UploadedAt") + .HasColumnType("datetime(6)"); + + b.Property("UploadedById") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("UploadedById"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Employees.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AadharNumber") + .HasColumnType("longtext"); + + b.Property("ApplicationUserId") + .HasColumnType("varchar(255)"); + + b.Property("BirthDate") + .HasColumnType("datetime(6)"); + + b.Property("CurrentAddress") + .HasColumnType("longtext"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("EmergencyContactPerson") + .HasColumnType("longtext"); + + b.Property("EmergencyPhoneNumber") + .HasColumnType("longtext"); + + b.Property("FirstName") + .HasColumnType("longtext"); + + b.Property("Gender") + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("JobRoleId") + .HasColumnType("char(36)"); + + b.Property("JoiningDate") + .HasColumnType("datetime(6)"); + + b.Property("LastName") + .HasColumnType("longtext"); + + b.Property("MiddleName") + .HasColumnType("longtext"); + + b.Property("PanNumber") + .HasColumnType("longtext"); + + b.Property("PermanentAddress") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("Photo") + .HasColumnType("longblob"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.HasIndex("JobRoleId"); + + b.HasIndex("TenantId"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Employees.EmployeeRoleMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("EmployeeId") + .HasColumnType("char(36)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("RoleId"); + + b.HasIndex("TenantId"); + + b.ToTable("EmployeeRoleMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Employees.WorkShift", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("EndTime") + .HasColumnType("time(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("StartTime") + .HasColumnType("time(6)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("WorkShifts"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.ActivityCheckList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ActivityId") + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IsChecked") + .HasColumnType("tinyint(1)"); + + b.Property("IsMandatory") + .HasColumnType("tinyint(1)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("ActivityCheckLists"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.CheckListMappings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CheckListId") + .HasColumnType("char(36)"); + + b.Property("TaskAllocationId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("CheckListMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.FeaturePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("FeatureId") + .HasColumnType("char(36)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FeatureId"); + + b.ToTable("FeaturePermissions"); + + b.HasData( + new + { + Id = new Guid("6ea44136-987e-44ba-9e5d-1cf8f5837ebc"), + Description = "Access all information related to the project.", + FeatureId = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), + IsEnabled = true, + Name = "View Project" + }, + new + { + Id = new Guid("172fc9b6-755b-4f62-ab26-55c34a330614"), + Description = "Potentially edit the project name, description, start/end dates, or status.", + FeatureId = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), + IsEnabled = true, + Name = "Manage Project" + }, + new + { + Id = new Guid("b94802ce-0689-4643-9e1d-11c86950c35b"), + Description = "The \"Manage Team\" feature allows authorized users to organize project personnel by adding, removing, and assigning employee to projects.", + FeatureId = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), + IsEnabled = true, + Name = "Manage Team" + }, + new + { + Id = new Guid("8d7cc6e3-9147-41f7-aaa7-fa507e450bd4"), + Description = "Grants a user comprehensive read-only access to all details concerning the project's underlying systems, technologies, resources, and configurations", + FeatureId = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), + IsEnabled = true, + Name = "View Project Infra" + }, + new + { + Id = new Guid("cf2825ad-453b-46aa-91d9-27c124d63373"), + Description = "This allows them to create, modify, and manage all aspects of the supporting infrastructure.", + FeatureId = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), + IsEnabled = true, + Name = "Manage Project Infra" + }, + new + { + Id = new Guid("9fcc5f87-25e3-4846-90ac-67a71ab92e3c"), + Description = "Grants a user comprehensive read-only access to all details associated with tasks within a project. This includes task descriptions, statuses, assignees, due dates, dependencies, progress, history, and any related attachments or discussions.", + FeatureId = new Guid("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"), + IsEnabled = true, + Name = "View Task" + }, + new + { + Id = new Guid("08752f33-3b29-4816-b76b-ea8a968ed3c5"), + Description = "This allows them to create new tasks, modify existing task attributes (description, status, assignee, due date, etc.),", + FeatureId = new Guid("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"), + IsEnabled = true, + Name = "Add/Edit Task" + }, + new + { + Id = new Guid("6a32379b-8b3f-49a6-8c48-4b7ac1b55dc2"), + Description = "Grants a user the ability to designate team members responsible for specific tasks and to update the completion status or provide progress updates for those tasks", + FeatureId = new Guid("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"), + IsEnabled = true, + Name = "Assign/Report Progress" + }, + new + { + Id = new Guid("db4e40c5-2ba9-4b6d-b8a6-a16a250ff99c"), + Description = "Grants a user the authority to officially confirm the completion or acceptance of a task, often signifying that it meets the required standards or criteria", + FeatureId = new Guid("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"), + IsEnabled = true, + Name = "Approve Task" + }, + new + { + Id = new Guid("60611762-7f8a-4fb5-b53f-b1139918796b"), + Description = "Grants a user read-only access to details about the all individuals within the system. This typically includes names, contact information, roles, departments, and potentially other relevant employee data", + FeatureId = new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), + IsEnabled = true, + Name = "View All Employees" + }, + new + { + Id = new Guid("b82d2b7e-0d52-45f3-997b-c008ea460e7f"), + Description = "Grants a user read-only access to details about the individuals within the system which are is assigned to same projects as user. This typically includes names, contact information, roles, departments, and potentially other relevant employee data", + FeatureId = new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), + IsEnabled = true, + Name = "View Team Members" + }, + new + { + Id = new Guid("a97d366a-c2bb-448d-be93-402bd2324566"), + Description = "Grants a user the authority to create new employee profiles and modify existing employee details within the system. This typically includes adding or updating information such as names, contact details, roles, departments, skills, and potentially other personal or professional data", + FeatureId = new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), + IsEnabled = true, + Name = "Add/Edit Employee" + }, + new + { + Id = new Guid("fbd213e0-0250-46f1-9f5f-4b2a1e6e76a3"), + Description = "Grants a user the authority to manage employee application roles, enabling them to assign or revoke access privileges within the system.", + FeatureId = new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), + IsEnabled = true, + Name = "Assign Roles" + }, + new + { + Id = new Guid("915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"), + Description = "Team Attendance refers to tracking and managing the attendance of all team members collectively, often monitored by a team lead or manager.", + FeatureId = new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), + IsEnabled = true, + Name = "Team Attendance " + }, + new + { + Id = new Guid("57802c4a-00aa-4a1f-a048-fd2f70dd44b6"), + Description = "Grants a user the authority to approve requests from employees to adjust or correct their recorded attendance. This typically involves reviewing the reason for the regularization, verifying any supporting documentation, and then officially accepting the changes to the employee's attendance records", + FeatureId = new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), + IsEnabled = true, + Name = "Regularize Attendance" + }, + new + { + Id = new Guid("ccb0589f-712b-43de-92ed-5b6088e7dc4e"), + Description = "Team Attendance refers to tracking and managing the attendance of all team members collectively, often monitored by a team lead or manager.", + FeatureId = new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), + IsEnabled = true, + Name = "Self Attendance" + }, + new + { + Id = new Guid("5ffbafe0-7ab0-48b1-bb50-c1bf76b65f9d"), + Description = "Grants a user read-only access to foundational or reference data within the system. \"Masters\" typically refer to predefined lists, categories, or templates that are used throughout the application to standardize information and maintain consistency", + FeatureId = new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), + IsEnabled = true, + Name = "View Masters" + }, + new + { + Id = new Guid("588a8824-f924-4955-82d8-fc51956cf323"), + Description = "Grants a user the authority to create, modify, and delete foundational or reference data within the system. These \"masters\" are typically the core lists, categories, and configurations that other data and functionalities rely upon, such as departments, job titles, product categories", + FeatureId = new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), + IsEnabled = true, + Name = "Manage Masters" + }, + new + { + Id = new Guid("4286a13b-bb40-4879-8c6d-18e9e393beda"), + Description = "Full control over all directories, including the ability to manage permissions for all directories in the system.", + FeatureId = new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), + IsEnabled = true, + Name = "Directory Admin" + }, + new + { + Id = new Guid("62668630-13ce-4f52-a0f0-db38af2230c5"), + Description = "Full control over directories they created or have been assigned. Can also manage permissions for those directories.", + FeatureId = new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), + IsEnabled = true, + Name = "Directory Manager" + }, + new + { + Id = new Guid("0f919170-92d4-4337-abd3-49b66fc871bb"), + Description = "Full control over directories they created. Can view contacts in directories they either created or were assigned to. Can manage permissions only for directories they created.", + FeatureId = new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), + IsEnabled = true, + Name = "Directory User" + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.RolePermissionMappings", b => + { + b.Property("ApplicationRoleId") + .HasColumnType("char(36)"); + + b.Property("FeaturePermissionId") + .HasColumnType("char(36)"); + + b.HasKey("ApplicationRoleId", "FeaturePermissionId"); + + b.HasIndex("FeaturePermissionId"); + + b.ToTable("RolePermissionMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactName") + .HasColumnType("longtext"); + + b.Property("ContactNumber") + .HasColumnType("longtext"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("DomainName") + .HasColumnType("longtext"); + + b.Property("IndustryId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OnBoardingDate") + .HasColumnType("datetime(6)"); + + b.Property("OragnizationSize") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("IndustryId"); + + b.ToTable("Tenants"); + + b.HasData( + new + { + Id = new Guid("b3466e83-7e11-464c-b93a-daf047838b26"), + ContactName = "Admin", + ContactNumber = "123456789", + Description = "", + DomainName = "www.marcobms.org", + IndustryId = new Guid("15436ee3-a650-469e-bfc2-59993f7514bb"), + IsActive = true, + Name = "MarcoBMS", + OnBoardingDate = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + OragnizationSize = "100-200" + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CommentId") + .HasColumnType("char(36)"); + + b.Property("FileId") + .HasColumnType("char(36)"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TicketId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("CommentId"); + + b.HasIndex("TicketId"); + + b.ToTable("TicketAttachments"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AuthorId") + .HasColumnType("char(36)"); + + b.Property("MessageText") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ParentMessageId") + .HasColumnType("char(36)"); + + b.Property("SentAt") + .HasColumnType("datetime(6)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("TicketId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("TicketComments"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketForum", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedById") + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LinkedActivityId") + .HasColumnType("char(36)"); + + b.Property("LinkedProjectId") + .HasColumnType("char(36)"); + + b.Property("PriorityId") + .HasColumnType("char(36)"); + + b.Property("StatusId") + .HasColumnType("char(36)"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("TypeId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("PriorityId"); + + b.HasIndex("StatusId"); + + b.HasIndex("TenantId"); + + b.HasIndex("TypeId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("TagId") + .HasColumnType("char(36)"); + + b.Property("TicketId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TagId"); + + b.HasIndex("TicketId"); + + b.ToTable("TicketTags"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketTypeMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IsDefault") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("TicketTypeMasters"); + + b.HasData( + new + { + Id = new Guid("c74e5480-2b71-483c-8f4a-1a9c69c32603"), + Description = "An identified problem that affects the performance, reliability, or standards of a product or service", + IsDefault = true, + Name = "Quality Issue", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("d1f55eab-9898-4e46-9f03-b263e33e5d38"), + Description = "A support service that assists users with technical issues, requests, or inquiries.", + IsDefault = true, + Name = "Help Desk", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Mail.MailDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("MailListId") + .HasColumnType("char(36)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("Recipient") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Schedule") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("MailListId"); + + b.ToTable("MailDetails"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Mail.MailLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("EmailId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("EmployeeId") + .HasColumnType("char(36)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("TimeStamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("MailLogs"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Mail.MailingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("MailingList"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.ActivityMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ActivityName") + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("UnitOfMeasurement") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("ActivityMasters"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.Feature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("ModuleId") + .HasColumnType("char(36)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ModuleId"); + + b.ToTable("Features"); + + b.HasData( + new + { + Id = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), + Description = "Manage Project", + IsActive = true, + ModuleId = new Guid("bf59fd88-b57a-4d67-bf01-3780f385896b"), + Name = "Project Management" + }, + new + { + Id = new Guid("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"), + Description = "Manage Tasks", + IsActive = true, + ModuleId = new Guid("bf59fd88-b57a-4d67-bf01-3780f385896b"), + Name = "Task Management" + }, + new + { + Id = new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), + Description = "Manage Employee", + IsActive = true, + ModuleId = new Guid("2a231490-bcb1-4bdd-91f1-f25fb7f25b23"), + Name = "Employee Management" + }, + new + { + Id = new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), + Description = "Attendance", + IsActive = true, + ModuleId = new Guid("2a231490-bcb1-4bdd-91f1-f25fb7f25b23"), + Name = "Attendance Management" + }, + new + { + Id = new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), + Description = "Global Masters", + IsActive = true, + ModuleId = new Guid("c43db8c7-ab73-47f4-9d3b-f83e81357924"), + Name = "Masters" + }, + new + { + Id = new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), + Description = "Managing all directory related rights", + IsActive = true, + ModuleId = new Guid("c43db8c7-ab73-47f4-9d3b-f83e81357924"), + Name = "Directory Management" + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.Industry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Industries"); + + b.HasData( + new + { + Id = new Guid("15436ee3-a650-469e-bfc2-59993f7514bb"), + Name = "Information Technology (IT) Services" + }, + new + { + Id = new Guid("0a63e657-2c5f-49b5-854b-42c978293154"), + Name = "Manufacturing & Production" + }, + new + { + Id = new Guid("bdc61e3b-69ea-4394-bab6-079ec135b5bd"), + Name = "Energy & Resources" + }, + new + { + Id = new Guid("5ca200ac-00d7-415e-a410-b948e27ac9d2"), + Name = "Finance & Professional Services" + }, + new + { + Id = new Guid("d5621700-cd87-441f-8cdb-6051ddfc83b4"), + Name = "Hospitals and Healthcare Services" + }, + new + { + Id = new Guid("23608891-657e-40f0-bbd4-2b0a2ec1a76f"), + Name = "Social Services" + }, + new + { + Id = new Guid("a493f4e3-16b1-4411-be3c-6bf2987a3168"), + Name = "Retail & Consumer Services" + }, + new + { + Id = new Guid("e9d8ce92-9371-4ed9-9831-83c07f78edec"), + Name = "Transportation & Logistics" + }, + new + { + Id = new Guid("8a0d6134-2dbe-4e0a-b250-ff34cb7b9df0"), + Name = "Education & Training" + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.Module", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Modules"); + + b.HasData( + new + { + Id = new Guid("bf59fd88-b57a-4d67-bf01-3780f385896b"), + Description = "Project Module", + Key = "b04da7e9-0406-409c-ac7f-b97256e6ea02", + Name = "Project" + }, + new + { + Id = new Guid("2a231490-bcb1-4bdd-91f1-f25fb7f25b23"), + Description = "Employee Module", + Key = "0971c7fb-6ce1-458a-ae3f-8d3205893637", + Name = "Employee" + }, + new + { + Id = new Guid("c43db8c7-ab73-47f4-9d3b-f83e81357924"), + Description = "Masters Module", + Key = "504ec132-e6a9-422f-8f85-050602cfce05", + Name = "Masters" + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.StatusMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("StatusMasters"); + + b.HasData( + new + { + Id = new Guid("b74da4c2-d07e-46f2-9919-e75e49b12731"), + Status = "Active", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("cdad86aa-8a56-4ff4-b633-9c629057dfef"), + Status = "In Progress", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("603e994b-a27f-4e5d-a251-f3d69b0498ba"), + Status = "On Hold", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("ef1c356e-0fe0-42df-a5d3-8daee355492d"), + Status = "In Active", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("33deaef9-9af1-4f2a-b443-681ea0d04f81"), + Status = "Completed", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.TicketPriorityMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ColorCode") + .HasColumnType("longtext"); + + b.Property("IsDefault") + .HasColumnType("tinyint(1)"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("TicketPriorityMasters"); + + b.HasData( + new + { + Id = new Guid("188d29b3-10f3-42d0-9587-1a46ae7a0320"), + ColorCode = "008000", + IsDefault = true, + Level = 1, + Name = "Low", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("0919bc84-9f82-4ecf-98c7-962755dd9a97"), + ColorCode = "FFFF00", + IsDefault = true, + Level = 2, + Name = "Medium", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("a13b7e59-16fd-4665-b5cf-a97399e8445a"), + ColorCode = "#FFA500", + IsDefault = true, + Level = 3, + Name = "High", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("f340fbc3-c9fd-46aa-b063-0093418830e4"), + ColorCode = "#FFA500", + IsDefault = true, + Level = 4, + Name = "Critical", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("44a7b91d-a0dd-45d1-8616-4d2f71e16401"), + ColorCode = "#FF0000", + IsDefault = true, + Level = 5, + Name = "Urgent", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.TicketStatusMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ColorCode") + .HasColumnType("longtext"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IsDefault") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("TicketStatusMasters"); + + b.HasData( + new + { + Id = new Guid("6b0c409b-3e80-4165-8b39-f3fcacb4c797"), + ColorCode = "#FFCC99", + Description = "This is a newly created issue.", + IsDefault = true, + Name = "New", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("6c5ac37d-5b7d-40f3-adec-2dabaa5cca86"), + ColorCode = "#E6FF99", + Description = "Assigned to employee or team of employees", + IsDefault = true, + Name = "Assigned", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("7f96bcd5-0c66-411b-8a1d-9d1a4785194e"), + ColorCode = "#99E6FF", + Description = "These issues are currently in progress", + IsDefault = true, + Name = "In Progress", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("5c72b630-6923-4215-bf2c-b1622afd76e7"), + ColorCode = "#6c757d", + Description = "These issues are currently under review", + IsDefault = true, + Name = "In Review", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("8ff85685-a875-4f21-aa95-d99551315fcc"), + ColorCode = "#B399FF", + Description = "The following issues are resolved and closed", + IsDefault = true, + Name = "Done", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.TicketTagMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ColorCode") + .HasColumnType("longtext"); + + b.Property("IsDefault") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("TicketTagMasters"); + + b.HasData( + new + { + Id = new Guid("ef6c2a65-f61d-4537-9650-a7ab7f8d98db"), + ColorCode = "#e59866", + IsDefault = true, + Name = "Quality Issue", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("5a168569-8ad7-4422-8db6-51ef25caddeb"), + ColorCode = "#85c1e9", + IsDefault = true, + Name = "Help Desk", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.WorkCategoryMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("WorkCategoryMasters"); + + b.HasData( + new + { + Id = new Guid("86bb2cc8-f6b5-4fdd-bbee-c389c713a44b"), + Description = "Created new task in a professional or creative context", + IsSystem = true, + Name = "Fresh Work", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("9ebfa19c-53b9-481b-b863-c25d2f843201"), + Description = "Revising, modifying, or correcting a task to improve its quality or fix issues", + IsSystem = true, + Name = "Rework", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("11a79929-1d07-42dc-9e98-82d0d2f4a240"), + Description = "Any defect, deviation, or non-conformance in a task that fails to meet established standards or customer expectations.", + IsSystem = true, + Name = "Quality Issue", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.WorkStatusMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("WorkStatusMasters"); + + b.HasData( + new + { + Id = new Guid("030bb085-e230-4370-aec7-9a74d652864e"), + Description = "Confirm the tasks are actually finished as reported", + IsSystem = true, + Name = "Approve", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("2a1a5b96-cf93-4111-b4b1-76c19d6333b4"), + Description = "Not all tasks are actually finished as reported", + IsSystem = true, + Name = "Partially Approve", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("00a062e6-62e6-42c5-b6b1-024328651b72"), + Description = "Tasks are not finished as reported or have any issues in al the tasks", + IsSystem = true, + Name = "NCR", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.Building", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("Buildings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.Floor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BuildingId") + .HasColumnType("char(36)"); + + b.Property("FloorName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("BuildingId"); + + b.HasIndex("TenantId"); + + b.ToTable("Floor"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactPerson") + .HasColumnType("longtext"); + + b.Property("EndDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProjectAddress") + .HasColumnType("longtext"); + + b.Property("ProjectStatusId") + .HasColumnType("char(36)"); + + b.Property("ShortName") + .HasColumnType("longtext"); + + b.Property("StartDate") + .HasColumnType("datetime(6)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ProjectStatusId"); + + b.HasIndex("TenantId"); + + b.ToTable("Projects"); + + b.HasData( + new + { + Id = new Guid("85bf587b-7ca9-4685-b77c-d817f5847e85"), + ContactPerson = "Project 1 Contact Person", + EndDate = new DateTime(2026, 4, 20, 10, 11, 17, 588, DateTimeKind.Unspecified), + Name = "Project 1", + ProjectAddress = "Project 1 Address", + ProjectStatusId = new Guid("b74da4c2-d07e-46f2-9919-e75e49b12731"), + StartDate = new DateTime(2025, 4, 20, 10, 11, 17, 588, DateTimeKind.Unspecified), + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.ProjectAllocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllocationDate") + .HasColumnType("datetime(6)"); + + b.Property("EmployeeId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("JobRoleId") + .HasColumnType("char(36)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("ReAllocationDate") + .HasColumnType("datetime(6)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("TenantId"); + + b.ToTable("ProjectAllocations"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.WorkArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AreaName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FloorId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("FloorId"); + + b.HasIndex("TenantId"); + + b.ToTable("WorkAreas"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.WorkItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ActivityId") + .HasColumnType("char(36)"); + + b.Property("CompletedWork") + .HasColumnType("double"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ParentTaskId") + .HasColumnType("char(36)"); + + b.Property("PlannedWork") + .HasColumnType("double"); + + b.Property("TaskDate") + .HasColumnType("datetime(6)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("WorkAreaId") + .HasColumnType("char(36)"); + + b.Property("WorkCategoryId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId"); + + b.HasIndex("TenantId"); + + b.HasIndex("WorkAreaId"); + + b.HasIndex("WorkCategoryId"); + + b.ToTable("WorkItems"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Roles.ApplicationRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("Role") + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("ApplicationRoles"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Roles.JobRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("JobRoles"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Utilities.Inquiries", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("About") + .HasColumnType("longtext"); + + b.Property("ContactNumber") + .HasColumnType("longtext"); + + b.Property("ContactPerson") + .HasColumnType("longtext"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("IndustryId") + .HasColumnType("char(36)"); + + b.Property("OragnizationSize") + .HasColumnType("longtext"); + + b.Property("OrganizatioinName") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Inquiries"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(21) + .HasColumnType("varchar(21)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("SecurityStamp") + .HasColumnType("longtext"); + + b.Property("TwoFactorEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + + b.HasDiscriminator().HasValue("IdentityUser"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("RoleId") + .HasColumnType("varchar(255)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.ApplicationUser", b => + { + b.HasBaseType("Microsoft.AspNetCore.Identity.IdentityUser"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRootUser") + .HasColumnType("tinyint(1)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasDiscriminator().HasValue("ApplicationUser"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskAllocation", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "ApprovedBy") + .WithMany() + .HasForeignKey("ApprovedById"); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("AssignedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "ReportedBy") + .WithMany() + .HasForeignKey("ReportedById"); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Projects.WorkItem", "WorkItem") + .WithMany() + .HasForeignKey("WorkItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Master.WorkStatusMaster", "WorkStatus") + .WithMany() + .HasForeignKey("WorkStatusId"); + + b.Navigation("ApprovedBy"); + + b.Navigation("Employee"); + + b.Navigation("ReportedBy"); + + b.Navigation("Tenant"); + + b.Navigation("WorkItem"); + + b.Navigation("WorkStatus"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskComment", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("CommentedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Activities.TaskAllocation", "TaskAllocation") + .WithMany() + .HasForeignKey("TaskAllocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("TaskAllocation"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskMembers", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Activities.TaskAllocation", "TaskAllocation") + .WithMany() + .HasForeignKey("TaskAllocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("TaskAllocation"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.AttendanceModule.Attendance", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "Approver") + .WithMany() + .HasForeignKey("EmployeeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Approver"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.AttendanceModule.AttendanceLog", b => + { + b.HasOne("Marco.Pms.Model.AttendanceModule.Attendance", "Attendance") + .WithMany() + .HasForeignKey("AttendanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.DocumentManager.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId"); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "UpdatedByEmployee") + .WithMany() + .HasForeignKey("UpdatedBy"); + + b.Navigation("Attendance"); + + b.Navigation("Document"); + + b.Navigation("Employee"); + + b.Navigation("Tenant"); + + b.Navigation("UpdatedByEmployee"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Authentication.MPINDetails", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Authentication.OTPDetails", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Authentication.RefreshToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.Bucket", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedByID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.Contact", b => + { + b.HasOne("Marco.Pms.Model.Directory.ContactCategoryMaster", "ContactCategory") + .WithMany() + .HasForeignKey("ContactCategoryId"); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.Navigation("ContactCategory"); + + b.Navigation("CreatedBy"); + + b.Navigation("Tenant"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactBucketMapping", b => + { + b.HasOne("Marco.Pms.Model.Directory.Bucket", "Bucket") + .WithMany() + .HasForeignKey("BucketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Directory.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Bucket"); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactCategoryMaster", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactEmail", b => + { + b.HasOne("Marco.Pms.Model.Directory.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactNote", b => + { + b.HasOne("Marco.Pms.Model.Directory.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "Createdby") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.Navigation("Contact"); + + b.Navigation("Createdby"); + + b.Navigation("Tenant"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactPhone", b => + { + b.HasOne("Marco.Pms.Model.Directory.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactProjectMapping", b => + { + b.HasOne("Marco.Pms.Model.Directory.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Projects.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contact"); + + b.Navigation("Project"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactTagMapping", b => + { + b.HasOne("Marco.Pms.Model.Directory.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Directory.ContactTagMaster", "ContactTag") + .WithMany() + .HasForeignKey("ContactTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contact"); + + b.Navigation("ContactTag"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactTagMaster", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.DirectoryUpdateLog", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.EmployeeBucketMapping", b => + { + b.HasOne("Marco.Pms.Model.Directory.Bucket", "Bucket") + .WithMany() + .HasForeignKey("BucketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Bucket"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Marco.Pms.Model.DocumentManager.Document", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "UploadedBy") + .WithMany() + .HasForeignKey("UploadedById"); + + b.Navigation("Tenant"); + + b.Navigation("UploadedBy"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Employees.Employee", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.ApplicationUser", "ApplicationUser") + .WithMany() + .HasForeignKey("ApplicationUserId"); + + b.HasOne("Marco.Pms.Model.Roles.JobRole", "JobRole") + .WithMany() + .HasForeignKey("JobRoleId"); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApplicationUser"); + + b.Navigation("JobRole"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Employees.EmployeeRoleMapping", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Roles.ApplicationRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("Role"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Employees.WorkShift", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.FeaturePermission", b => + { + b.HasOne("Marco.Pms.Model.Master.Feature", "Feature") + .WithMany("FeaturePermissions") + .HasForeignKey("FeatureId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Feature"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.RolePermissionMappings", b => + { + b.HasOne("Marco.Pms.Model.Roles.ApplicationRole", null) + .WithMany() + .HasForeignKey("ApplicationRoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.FeaturePermission", null) + .WithMany() + .HasForeignKey("FeaturePermissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.Tenant", b => + { + b.HasOne("Marco.Pms.Model.Master.Industry", "Industry") + .WithMany() + .HasForeignKey("IndustryId"); + + b.Navigation("Industry"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketAttachment", b => + { + b.HasOne("Marco.Pms.Model.Forum.TicketComment", "TicketComment") + .WithMany("Attachments") + .HasForeignKey("CommentId"); + + b.HasOne("Marco.Pms.Model.Forum.TicketForum", "Ticket") + .WithMany() + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ticket"); + + b.Navigation("TicketComment"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketComment", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketForum", b => + { + b.HasOne("Marco.Pms.Model.Master.TicketPriorityMaster", "Priority") + .WithMany() + .HasForeignKey("PriorityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Master.TicketStatusMaster", "TicketStatusMaster") + .WithMany() + .HasForeignKey("StatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Forum.TicketTypeMaster", "TicketTypeMaster") + .WithMany() + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Priority"); + + b.Navigation("Tenant"); + + b.Navigation("TicketStatusMaster"); + + b.Navigation("TicketTypeMaster"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketTag", b => + { + b.HasOne("Marco.Pms.Model.Master.TicketTagMaster", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Forum.TicketForum", "Ticket") + .WithMany() + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tag"); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Mail.MailDetails", b => + { + b.HasOne("Marco.Pms.Model.Mail.MailingList", "MailBody") + .WithMany() + .HasForeignKey("MailListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MailBody"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.ActivityMaster", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.Feature", b => + { + b.HasOne("Marco.Pms.Model.Master.Module", "Module") + .WithMany() + .HasForeignKey("ModuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Module"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.StatusMaster", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.WorkCategoryMaster", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.WorkStatusMaster", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.Building", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.Floor", b => + { + b.HasOne("Marco.Pms.Model.Projects.Building", "Building") + .WithMany() + .HasForeignKey("BuildingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Building"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.Project", b => + { + b.HasOne("Marco.Pms.Model.Master.StatusMaster", "ProjectStatus") + .WithMany() + .HasForeignKey("ProjectStatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProjectStatus"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.ProjectAllocation", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Projects.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("Project"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.WorkArea", b => + { + b.HasOne("Marco.Pms.Model.Projects.Floor", "Floor") + .WithMany() + .HasForeignKey("FloorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Floor"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.WorkItem", b => + { + b.HasOne("Marco.Pms.Model.Master.ActivityMaster", "ActivityMaster") + .WithMany() + .HasForeignKey("ActivityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Projects.WorkArea", "WorkArea") + .WithMany() + .HasForeignKey("WorkAreaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Master.WorkCategoryMaster", "WorkCategoryMaster") + .WithMany() + .HasForeignKey("WorkCategoryId"); + + b.Navigation("ActivityMaster"); + + b.Navigation("Tenant"); + + b.Navigation("WorkArea"); + + b.Navigation("WorkCategoryMaster"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Roles.ApplicationRole", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Marco.Pms.Model.Roles.JobRole", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketComment", b => + { + b.Navigation("Attachments"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.Feature", b => + { + b.Navigation("FeaturePermissions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Marco.Pms.DataAccess/Migrations/20250702042830_Added_UploadedBy_ForeginKey_In_Decuments_Table.cs b/Marco.Pms.DataAccess/Migrations/20250702042830_Added_UploadedBy_ForeginKey_In_Decuments_Table.cs new file mode 100644 index 0000000..fd31771 --- /dev/null +++ b/Marco.Pms.DataAccess/Migrations/20250702042830_Added_UploadedBy_ForeginKey_In_Decuments_Table.cs @@ -0,0 +1,50 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Marco.Pms.DataAccess.Migrations +{ + /// + public partial class Added_UploadedBy_ForeginKey_In_Decuments_Table : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UploadedById", + table: "Documents", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci"); + + migrationBuilder.CreateIndex( + name: "IX_Documents_UploadedById", + table: "Documents", + column: "UploadedById"); + + migrationBuilder.AddForeignKey( + name: "FK_Documents_Employees_UploadedById", + table: "Documents", + column: "UploadedById", + principalTable: "Employees", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Documents_Employees_UploadedById", + table: "Documents"); + + migrationBuilder.DropIndex( + name: "IX_Documents_UploadedById", + table: "Documents"); + + migrationBuilder.DropColumn( + name: "UploadedById", + table: "Documents"); + } + } +} diff --git a/Marco.Pms.DataAccess/Migrations/20250702045931_Added_Designation_Paraneter_In_Contacts_Table.Designer.cs b/Marco.Pms.DataAccess/Migrations/20250702045931_Added_Designation_Paraneter_In_Contacts_Table.Designer.cs new file mode 100644 index 0000000..3cd5d28 --- /dev/null +++ b/Marco.Pms.DataAccess/Migrations/20250702045931_Added_Designation_Paraneter_In_Contacts_Table.Designer.cs @@ -0,0 +1,3419 @@ +// +using System; +using Marco.Pms.DataAccess.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Marco.Pms.DataAccess.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250702045931_Added_Designation_Paraneter_In_Contacts_Table")] + partial class Added_Designation_Paraneter_In_Contacts_Table + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.12") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + //MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskAllocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ApprovedById") + .HasColumnType("char(36)"); + + b.Property("ApprovedDate") + .HasColumnType("datetime(6)"); + + b.Property("AssignedBy") + .HasColumnType("char(36)"); + + b.Property("AssignmentDate") + .HasColumnType("datetime(6)"); + + b.Property("CompletedTask") + .HasColumnType("double"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ParentTaskId") + .HasColumnType("char(36)"); + + b.Property("PlannedTask") + .HasColumnType("double"); + + b.Property("ReportedById") + .HasColumnType("char(36)"); + + b.Property("ReportedDate") + .HasColumnType("datetime(6)"); + + b.Property("ReportedTask") + .HasColumnType("double"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("WorkItemId") + .HasColumnType("char(36)"); + + b.Property("WorkStatusId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ApprovedById"); + + b.HasIndex("AssignedBy"); + + b.HasIndex("ReportedById"); + + b.HasIndex("TenantId"); + + b.HasIndex("WorkItemId"); + + b.HasIndex("WorkStatusId"); + + b.ToTable("TaskAllocations"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("DocumentId") + .HasColumnType("char(36)"); + + b.Property("ReferenceId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("TaskAttachments"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CommentDate") + .HasColumnType("datetime(6)"); + + b.Property("CommentedBy") + .HasColumnType("char(36)"); + + b.Property("TaskAllocationId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("CommentedBy"); + + b.HasIndex("TaskAllocationId"); + + b.HasIndex("TenantId"); + + b.ToTable("TaskComments"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskMembers", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("EmployeeId") + .HasColumnType("char(36)"); + + b.Property("TaskAllocationId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("TaskAllocationId"); + + b.HasIndex("TenantId"); + + b.ToTable("TaskMembers"); + }); + + modelBuilder.Entity("Marco.Pms.Model.AttendanceModule.Attendance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Activity") + .HasColumnType("int"); + + b.Property("ApprovedBy") + .HasColumnType("char(36)"); + + b.Property("AttendanceDate") + .HasColumnType("datetime(6)"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("EmployeeID") + .HasColumnType("char(36)"); + + b.Property("InTime") + .HasColumnType("datetime(6)"); + + b.Property("IsApproved") + .HasColumnType("tinyint(1)"); + + b.Property("OutTime") + .HasColumnType("datetime(6)"); + + b.Property("ProjectID") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeID"); + + b.HasIndex("TenantId"); + + b.ToTable("Attendes"); + }); + + modelBuilder.Entity("Marco.Pms.Model.AttendanceModule.AttendanceLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Activity") + .HasColumnType("int"); + + b.Property("ActivityTime") + .HasColumnType("datetime(6)"); + + b.Property("AttendanceId") + .HasColumnType("char(36)"); + + b.Property("Comment") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DocumentId") + .HasColumnType("char(36)"); + + b.Property("EmployeeID") + .HasColumnType("char(36)"); + + b.Property("Latitude") + .HasColumnType("longtext"); + + b.Property("Longitude") + .HasColumnType("longtext"); + + b.Property("Photo") + .HasColumnType("longblob"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("UpdatedBy") + .HasColumnType("char(36)"); + + b.Property("UpdatedOn") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("AttendanceId"); + + b.HasIndex("DocumentId"); + + b.HasIndex("EmployeeID"); + + b.HasIndex("TenantId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("AttendanceLogs"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Authentication.MPINDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("MPIN") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MPINToken") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("TimeStamp") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("MPINDetails"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Authentication.OTPDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ExpriesInSec") + .HasColumnType("int"); + + b.Property("IsUsed") + .HasColumnType("tinyint(1)"); + + b.Property("OTP") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("TimeStamp") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("OTPDetails"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Authentication.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExpiryDate") + .HasColumnType("datetime(6)"); + + b.Property("IsRevoked") + .HasColumnType("tinyint(1)"); + + b.Property("IsUsed") + .HasColumnType("tinyint(1)"); + + b.Property("RevokedAt") + .HasColumnType("datetime(6)"); + + b.Property("Token") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.Bucket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedByID") + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByID"); + + b.HasIndex("TenantId"); + + b.ToTable("Buckets"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Address") + .HasColumnType("longtext"); + + b.Property("ContactCategoryId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedById") + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Designation") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Organization") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedById") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ContactCategoryId"); + + b.HasIndex("CreatedById"); + + b.HasIndex("TenantId"); + + b.HasIndex("UpdatedById"); + + b.ToTable("Contacts"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactBucketMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BucketId") + .HasColumnType("char(36)"); + + b.Property("ContactId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("BucketId"); + + b.HasIndex("ContactId"); + + b.ToTable("ContactBucketMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactCategoryMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("ContactCategoryMasters"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactEmail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactId") + .HasColumnType("char(36)"); + + b.Property("EmailAddress") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)"); + + b.Property("Label") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.ToTable("ContactsEmails"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactId") + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedById") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Note") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedById") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.HasIndex("CreatedById"); + + b.HasIndex("TenantId"); + + b.HasIndex("UpdatedById"); + + b.ToTable("ContactNotes"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactPhone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactId") + .HasColumnType("char(36)"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)"); + + b.Property("Label") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.ToTable("ContactsPhones"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactProjectMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactId") + .HasColumnType("char(36)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("TenantId"); + + b.ToTable("ContactProjectMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactTagMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactId") + .HasColumnType("char(36)"); + + b.Property("ContactTagId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.HasIndex("ContactTagId"); + + b.ToTable("ContactTagMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactTagMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("ContactTagMasters"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.DirectoryUpdateLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("RefereanceId") + .HasColumnType("char(36)"); + + b.Property("UpdateAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedById") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UpdatedById"); + + b.ToTable("DirectoryUpdateLogs"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.EmployeeBucketMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BucketId") + .HasColumnType("char(36)"); + + b.Property("EmployeeId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("BucketId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("EmployeeBucketMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.DocumentManager.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Base64Data") + .HasColumnType("longtext"); + + b.Property("BatchId") + .HasColumnType("char(36)"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("S3Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("ThumbS3Key") + .HasColumnType("longtext"); + + b.Property("UploadedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Employees.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AadharNumber") + .HasColumnType("longtext"); + + b.Property("ApplicationUserId") + .HasColumnType("varchar(255)"); + + b.Property("BirthDate") + .HasColumnType("datetime(6)"); + + b.Property("CurrentAddress") + .HasColumnType("longtext"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("EmergencyContactPerson") + .HasColumnType("longtext"); + + b.Property("EmergencyPhoneNumber") + .HasColumnType("longtext"); + + b.Property("FirstName") + .HasColumnType("longtext"); + + b.Property("Gender") + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("JobRoleId") + .HasColumnType("char(36)"); + + b.Property("JoiningDate") + .HasColumnType("datetime(6)"); + + b.Property("LastName") + .HasColumnType("longtext"); + + b.Property("MiddleName") + .HasColumnType("longtext"); + + b.Property("PanNumber") + .HasColumnType("longtext"); + + b.Property("PermanentAddress") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("Photo") + .HasColumnType("longblob"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationUserId"); + + b.HasIndex("JobRoleId"); + + b.HasIndex("TenantId"); + + b.ToTable("Employees"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Employees.EmployeeRoleMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("EmployeeId") + .HasColumnType("char(36)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("RoleId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("RoleId"); + + b.HasIndex("TenantId"); + + b.ToTable("EmployeeRoleMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Employees.WorkShift", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("EndTime") + .HasColumnType("time(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("StartTime") + .HasColumnType("time(6)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("WorkShifts"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.ActivityCheckList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ActivityId") + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IsChecked") + .HasColumnType("tinyint(1)"); + + b.Property("IsMandatory") + .HasColumnType("tinyint(1)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("ActivityCheckLists"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.CheckListMappings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CheckListId") + .HasColumnType("char(36)"); + + b.Property("TaskAllocationId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("CheckListMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.FeaturePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("FeatureId") + .HasColumnType("char(36)"); + + b.Property("IsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("FeatureId"); + + b.ToTable("FeaturePermissions"); + + b.HasData( + new + { + Id = new Guid("6ea44136-987e-44ba-9e5d-1cf8f5837ebc"), + Description = "Access all information related to the project.", + FeatureId = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), + IsEnabled = true, + Name = "View Project" + }, + new + { + Id = new Guid("172fc9b6-755b-4f62-ab26-55c34a330614"), + Description = "Potentially edit the project name, description, start/end dates, or status.", + FeatureId = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), + IsEnabled = true, + Name = "Manage Project" + }, + new + { + Id = new Guid("b94802ce-0689-4643-9e1d-11c86950c35b"), + Description = "The \"Manage Team\" feature allows authorized users to organize project personnel by adding, removing, and assigning employee to projects.", + FeatureId = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), + IsEnabled = true, + Name = "Manage Team" + }, + new + { + Id = new Guid("8d7cc6e3-9147-41f7-aaa7-fa507e450bd4"), + Description = "Grants a user comprehensive read-only access to all details concerning the project's underlying systems, technologies, resources, and configurations", + FeatureId = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), + IsEnabled = true, + Name = "View Project Infra" + }, + new + { + Id = new Guid("cf2825ad-453b-46aa-91d9-27c124d63373"), + Description = "This allows them to create, modify, and manage all aspects of the supporting infrastructure.", + FeatureId = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), + IsEnabled = true, + Name = "Manage Project Infra" + }, + new + { + Id = new Guid("9fcc5f87-25e3-4846-90ac-67a71ab92e3c"), + Description = "Grants a user comprehensive read-only access to all details associated with tasks within a project. This includes task descriptions, statuses, assignees, due dates, dependencies, progress, history, and any related attachments or discussions.", + FeatureId = new Guid("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"), + IsEnabled = true, + Name = "View Task" + }, + new + { + Id = new Guid("08752f33-3b29-4816-b76b-ea8a968ed3c5"), + Description = "This allows them to create new tasks, modify existing task attributes (description, status, assignee, due date, etc.),", + FeatureId = new Guid("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"), + IsEnabled = true, + Name = "Add/Edit Task" + }, + new + { + Id = new Guid("6a32379b-8b3f-49a6-8c48-4b7ac1b55dc2"), + Description = "Grants a user the ability to designate team members responsible for specific tasks and to update the completion status or provide progress updates for those tasks", + FeatureId = new Guid("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"), + IsEnabled = true, + Name = "Assign/Report Progress" + }, + new + { + Id = new Guid("db4e40c5-2ba9-4b6d-b8a6-a16a250ff99c"), + Description = "Grants a user the authority to officially confirm the completion or acceptance of a task, often signifying that it meets the required standards or criteria", + FeatureId = new Guid("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"), + IsEnabled = true, + Name = "Approve Task" + }, + new + { + Id = new Guid("60611762-7f8a-4fb5-b53f-b1139918796b"), + Description = "Grants a user read-only access to details about the all individuals within the system. This typically includes names, contact information, roles, departments, and potentially other relevant employee data", + FeatureId = new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), + IsEnabled = true, + Name = "View All Employees" + }, + new + { + Id = new Guid("b82d2b7e-0d52-45f3-997b-c008ea460e7f"), + Description = "Grants a user read-only access to details about the individuals within the system which are is assigned to same projects as user. This typically includes names, contact information, roles, departments, and potentially other relevant employee data", + FeatureId = new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), + IsEnabled = true, + Name = "View Team Members" + }, + new + { + Id = new Guid("a97d366a-c2bb-448d-be93-402bd2324566"), + Description = "Grants a user the authority to create new employee profiles and modify existing employee details within the system. This typically includes adding or updating information such as names, contact details, roles, departments, skills, and potentially other personal or professional data", + FeatureId = new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), + IsEnabled = true, + Name = "Add/Edit Employee" + }, + new + { + Id = new Guid("fbd213e0-0250-46f1-9f5f-4b2a1e6e76a3"), + Description = "Grants a user the authority to manage employee application roles, enabling them to assign or revoke access privileges within the system.", + FeatureId = new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), + IsEnabled = true, + Name = "Assign Roles" + }, + new + { + Id = new Guid("915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"), + Description = "Team Attendance refers to tracking and managing the attendance of all team members collectively, often monitored by a team lead or manager.", + FeatureId = new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), + IsEnabled = true, + Name = "Team Attendance " + }, + new + { + Id = new Guid("57802c4a-00aa-4a1f-a048-fd2f70dd44b6"), + Description = "Grants a user the authority to approve requests from employees to adjust or correct their recorded attendance. This typically involves reviewing the reason for the regularization, verifying any supporting documentation, and then officially accepting the changes to the employee's attendance records", + FeatureId = new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), + IsEnabled = true, + Name = "Regularize Attendance" + }, + new + { + Id = new Guid("ccb0589f-712b-43de-92ed-5b6088e7dc4e"), + Description = "Team Attendance refers to tracking and managing the attendance of all team members collectively, often monitored by a team lead or manager.", + FeatureId = new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), + IsEnabled = true, + Name = "Self Attendance" + }, + new + { + Id = new Guid("5ffbafe0-7ab0-48b1-bb50-c1bf76b65f9d"), + Description = "Grants a user read-only access to foundational or reference data within the system. \"Masters\" typically refer to predefined lists, categories, or templates that are used throughout the application to standardize information and maintain consistency", + FeatureId = new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), + IsEnabled = true, + Name = "View Masters" + }, + new + { + Id = new Guid("588a8824-f924-4955-82d8-fc51956cf323"), + Description = "Grants a user the authority to create, modify, and delete foundational or reference data within the system. These \"masters\" are typically the core lists, categories, and configurations that other data and functionalities rely upon, such as departments, job titles, product categories", + FeatureId = new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), + IsEnabled = true, + Name = "Manage Masters" + }, + new + { + Id = new Guid("4286a13b-bb40-4879-8c6d-18e9e393beda"), + Description = "Full control over all directories, including the ability to manage permissions for all directories in the system.", + FeatureId = new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), + IsEnabled = true, + Name = "Directory Admin" + }, + new + { + Id = new Guid("62668630-13ce-4f52-a0f0-db38af2230c5"), + Description = "Full control over directories they created or have been assigned. Can also manage permissions for those directories.", + FeatureId = new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), + IsEnabled = true, + Name = "Directory Manager" + }, + new + { + Id = new Guid("0f919170-92d4-4337-abd3-49b66fc871bb"), + Description = "Full control over directories they created. Can view contacts in directories they either created or were assigned to. Can manage permissions only for directories they created.", + FeatureId = new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), + IsEnabled = true, + Name = "Directory User" + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.RolePermissionMappings", b => + { + b.Property("ApplicationRoleId") + .HasColumnType("char(36)"); + + b.Property("FeaturePermissionId") + .HasColumnType("char(36)"); + + b.HasKey("ApplicationRoleId", "FeaturePermissionId"); + + b.HasIndex("FeaturePermissionId"); + + b.ToTable("RolePermissionMappings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactName") + .HasColumnType("longtext"); + + b.Property("ContactNumber") + .HasColumnType("longtext"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("DomainName") + .HasColumnType("longtext"); + + b.Property("IndustryId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OnBoardingDate") + .HasColumnType("datetime(6)"); + + b.Property("OragnizationSize") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("IndustryId"); + + b.ToTable("Tenants"); + + b.HasData( + new + { + Id = new Guid("b3466e83-7e11-464c-b93a-daf047838b26"), + ContactName = "Admin", + ContactNumber = "123456789", + Description = "", + DomainName = "www.marcobms.org", + IndustryId = new Guid("15436ee3-a650-469e-bfc2-59993f7514bb"), + IsActive = true, + Name = "MarcoBMS", + OnBoardingDate = new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), + OragnizationSize = "100-200" + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CommentId") + .HasColumnType("char(36)"); + + b.Property("FileId") + .HasColumnType("char(36)"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TicketId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("CommentId"); + + b.HasIndex("TicketId"); + + b.ToTable("TicketAttachments"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AuthorId") + .HasColumnType("char(36)"); + + b.Property("MessageText") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ParentMessageId") + .HasColumnType("char(36)"); + + b.Property("SentAt") + .HasColumnType("datetime(6)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("TicketId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("TicketComments"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketForum", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedById") + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LinkedActivityId") + .HasColumnType("char(36)"); + + b.Property("LinkedProjectId") + .HasColumnType("char(36)"); + + b.Property("PriorityId") + .HasColumnType("char(36)"); + + b.Property("StatusId") + .HasColumnType("char(36)"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("TypeId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("PriorityId"); + + b.HasIndex("StatusId"); + + b.HasIndex("TenantId"); + + b.HasIndex("TypeId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("TagId") + .HasColumnType("char(36)"); + + b.Property("TicketId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TagId"); + + b.HasIndex("TicketId"); + + b.ToTable("TicketTags"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketTypeMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IsDefault") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("TicketTypeMasters"); + + b.HasData( + new + { + Id = new Guid("c74e5480-2b71-483c-8f4a-1a9c69c32603"), + Description = "An identified problem that affects the performance, reliability, or standards of a product or service", + IsDefault = true, + Name = "Quality Issue", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("d1f55eab-9898-4e46-9f03-b263e33e5d38"), + Description = "A support service that assists users with technical issues, requests, or inquiries.", + IsDefault = true, + Name = "Help Desk", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Mail.MailDetails", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("MailListId") + .HasColumnType("char(36)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("Recipient") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Schedule") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("MailListId"); + + b.ToTable("MailDetails"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Mail.MailLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("EmailId") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("EmployeeId") + .HasColumnType("char(36)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("TimeStamp") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("MailLogs"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Mail.MailingList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Body") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Keywords") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("MailingList"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.ActivityMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ActivityName") + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("UnitOfMeasurement") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("ActivityMasters"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.Feature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("ModuleId") + .HasColumnType("char(36)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("ModuleId"); + + b.ToTable("Features"); + + b.HasData( + new + { + Id = new Guid("53176ebf-c75d-42e5-839f-4508ffac3def"), + Description = "Manage Project", + IsActive = true, + ModuleId = new Guid("bf59fd88-b57a-4d67-bf01-3780f385896b"), + Name = "Project Management" + }, + new + { + Id = new Guid("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"), + Description = "Manage Tasks", + IsActive = true, + ModuleId = new Guid("bf59fd88-b57a-4d67-bf01-3780f385896b"), + Name = "Task Management" + }, + new + { + Id = new Guid("81ab8a87-8ccd-4015-a917-0627cee6a100"), + Description = "Manage Employee", + IsActive = true, + ModuleId = new Guid("2a231490-bcb1-4bdd-91f1-f25fb7f25b23"), + Name = "Employee Management" + }, + new + { + Id = new Guid("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), + Description = "Attendance", + IsActive = true, + ModuleId = new Guid("2a231490-bcb1-4bdd-91f1-f25fb7f25b23"), + Name = "Attendance Management" + }, + new + { + Id = new Guid("be3b3afc-6ccf-4566-b9b6-aafcb65546be"), + Description = "Global Masters", + IsActive = true, + ModuleId = new Guid("c43db8c7-ab73-47f4-9d3b-f83e81357924"), + Name = "Masters" + }, + new + { + Id = new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), + Description = "Managing all directory related rights", + IsActive = true, + ModuleId = new Guid("c43db8c7-ab73-47f4-9d3b-f83e81357924"), + Name = "Directory Management" + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.Industry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Industries"); + + b.HasData( + new + { + Id = new Guid("15436ee3-a650-469e-bfc2-59993f7514bb"), + Name = "Information Technology (IT) Services" + }, + new + { + Id = new Guid("0a63e657-2c5f-49b5-854b-42c978293154"), + Name = "Manufacturing & Production" + }, + new + { + Id = new Guid("bdc61e3b-69ea-4394-bab6-079ec135b5bd"), + Name = "Energy & Resources" + }, + new + { + Id = new Guid("5ca200ac-00d7-415e-a410-b948e27ac9d2"), + Name = "Finance & Professional Services" + }, + new + { + Id = new Guid("d5621700-cd87-441f-8cdb-6051ddfc83b4"), + Name = "Hospitals and Healthcare Services" + }, + new + { + Id = new Guid("23608891-657e-40f0-bbd4-2b0a2ec1a76f"), + Name = "Social Services" + }, + new + { + Id = new Guid("a493f4e3-16b1-4411-be3c-6bf2987a3168"), + Name = "Retail & Consumer Services" + }, + new + { + Id = new Guid("e9d8ce92-9371-4ed9-9831-83c07f78edec"), + Name = "Transportation & Logistics" + }, + new + { + Id = new Guid("8a0d6134-2dbe-4e0a-b250-ff34cb7b9df0"), + Name = "Education & Training" + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.Module", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Modules"); + + b.HasData( + new + { + Id = new Guid("bf59fd88-b57a-4d67-bf01-3780f385896b"), + Description = "Project Module", + Key = "b04da7e9-0406-409c-ac7f-b97256e6ea02", + Name = "Project" + }, + new + { + Id = new Guid("2a231490-bcb1-4bdd-91f1-f25fb7f25b23"), + Description = "Employee Module", + Key = "0971c7fb-6ce1-458a-ae3f-8d3205893637", + Name = "Employee" + }, + new + { + Id = new Guid("c43db8c7-ab73-47f4-9d3b-f83e81357924"), + Description = "Masters Module", + Key = "504ec132-e6a9-422f-8f85-050602cfce05", + Name = "Masters" + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.StatusMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("StatusMasters"); + + b.HasData( + new + { + Id = new Guid("b74da4c2-d07e-46f2-9919-e75e49b12731"), + Status = "Active", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("cdad86aa-8a56-4ff4-b633-9c629057dfef"), + Status = "In Progress", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("603e994b-a27f-4e5d-a251-f3d69b0498ba"), + Status = "On Hold", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("ef1c356e-0fe0-42df-a5d3-8daee355492d"), + Status = "In Active", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("33deaef9-9af1-4f2a-b443-681ea0d04f81"), + Status = "Completed", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.TicketPriorityMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ColorCode") + .HasColumnType("longtext"); + + b.Property("IsDefault") + .HasColumnType("tinyint(1)"); + + b.Property("Level") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("TicketPriorityMasters"); + + b.HasData( + new + { + Id = new Guid("188d29b3-10f3-42d0-9587-1a46ae7a0320"), + ColorCode = "008000", + IsDefault = true, + Level = 1, + Name = "Low", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("0919bc84-9f82-4ecf-98c7-962755dd9a97"), + ColorCode = "FFFF00", + IsDefault = true, + Level = 2, + Name = "Medium", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("a13b7e59-16fd-4665-b5cf-a97399e8445a"), + ColorCode = "#FFA500", + IsDefault = true, + Level = 3, + Name = "High", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("f340fbc3-c9fd-46aa-b063-0093418830e4"), + ColorCode = "#FFA500", + IsDefault = true, + Level = 4, + Name = "Critical", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("44a7b91d-a0dd-45d1-8616-4d2f71e16401"), + ColorCode = "#FF0000", + IsDefault = true, + Level = 5, + Name = "Urgent", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.TicketStatusMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ColorCode") + .HasColumnType("longtext"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IsDefault") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("TicketStatusMasters"); + + b.HasData( + new + { + Id = new Guid("6b0c409b-3e80-4165-8b39-f3fcacb4c797"), + ColorCode = "#FFCC99", + Description = "This is a newly created issue.", + IsDefault = true, + Name = "New", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("6c5ac37d-5b7d-40f3-adec-2dabaa5cca86"), + ColorCode = "#E6FF99", + Description = "Assigned to employee or team of employees", + IsDefault = true, + Name = "Assigned", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("7f96bcd5-0c66-411b-8a1d-9d1a4785194e"), + ColorCode = "#99E6FF", + Description = "These issues are currently in progress", + IsDefault = true, + Name = "In Progress", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("5c72b630-6923-4215-bf2c-b1622afd76e7"), + ColorCode = "#6c757d", + Description = "These issues are currently under review", + IsDefault = true, + Name = "In Review", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("8ff85685-a875-4f21-aa95-d99551315fcc"), + ColorCode = "#B399FF", + Description = "The following issues are resolved and closed", + IsDefault = true, + Name = "Done", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.TicketTagMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ColorCode") + .HasColumnType("longtext"); + + b.Property("IsDefault") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("TicketTagMasters"); + + b.HasData( + new + { + Id = new Guid("ef6c2a65-f61d-4537-9650-a7ab7f8d98db"), + ColorCode = "#e59866", + IsDefault = true, + Name = "Quality Issue", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("5a168569-8ad7-4422-8db6-51ef25caddeb"), + ColorCode = "#85c1e9", + IsDefault = true, + Name = "Help Desk", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.WorkCategoryMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("WorkCategoryMasters"); + + b.HasData( + new + { + Id = new Guid("86bb2cc8-f6b5-4fdd-bbee-c389c713a44b"), + Description = "Created new task in a professional or creative context", + IsSystem = true, + Name = "Fresh Work", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("9ebfa19c-53b9-481b-b863-c25d2f843201"), + Description = "Revising, modifying, or correcting a task to improve its quality or fix issues", + IsSystem = true, + Name = "Rework", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("11a79929-1d07-42dc-9e98-82d0d2f4a240"), + Description = "Any defect, deviation, or non-conformance in a task that fails to meet established standards or customer expectations.", + IsSystem = true, + Name = "Quality Issue", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.WorkStatusMaster", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("WorkStatusMasters"); + + b.HasData( + new + { + Id = new Guid("030bb085-e230-4370-aec7-9a74d652864e"), + Description = "Confirm the tasks are actually finished as reported", + IsSystem = true, + Name = "Approve", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("2a1a5b96-cf93-4111-b4b1-76c19d6333b4"), + Description = "Not all tasks are actually finished as reported", + IsSystem = true, + Name = "Partially Approve", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }, + new + { + Id = new Guid("00a062e6-62e6-42c5-b6b1-024328651b72"), + Description = "Tasks are not finished as reported or have any issues in al the tasks", + IsSystem = true, + Name = "NCR", + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.Building", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("Buildings"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.Floor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BuildingId") + .HasColumnType("char(36)"); + + b.Property("FloorName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("BuildingId"); + + b.HasIndex("TenantId"); + + b.ToTable("Floor"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ContactPerson") + .HasColumnType("longtext"); + + b.Property("EndDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProjectAddress") + .HasColumnType("longtext"); + + b.Property("ProjectStatusId") + .HasColumnType("char(36)"); + + b.Property("ShortName") + .HasColumnType("longtext"); + + b.Property("StartDate") + .HasColumnType("datetime(6)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ProjectStatusId"); + + b.HasIndex("TenantId"); + + b.ToTable("Projects"); + + b.HasData( + new + { + Id = new Guid("85bf587b-7ca9-4685-b77c-d817f5847e85"), + ContactPerson = "Project 1 Contact Person", + EndDate = new DateTime(2026, 4, 20, 10, 11, 17, 588, DateTimeKind.Unspecified), + Name = "Project 1", + ProjectAddress = "Project 1 Address", + ProjectStatusId = new Guid("b74da4c2-d07e-46f2-9919-e75e49b12731"), + StartDate = new DateTime(2025, 4, 20, 10, 11, 17, 588, DateTimeKind.Unspecified), + TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") + }); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.ProjectAllocation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AllocationDate") + .HasColumnType("datetime(6)"); + + b.Property("EmployeeId") + .HasColumnType("char(36)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("JobRoleId") + .HasColumnType("char(36)"); + + b.Property("ProjectId") + .HasColumnType("char(36)"); + + b.Property("ReAllocationDate") + .HasColumnType("datetime(6)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ProjectId"); + + b.HasIndex("TenantId"); + + b.ToTable("ProjectAllocations"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.WorkArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("AreaName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FloorId") + .HasColumnType("char(36)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("FloorId"); + + b.HasIndex("TenantId"); + + b.ToTable("WorkAreas"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.WorkItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ActivityId") + .HasColumnType("char(36)"); + + b.Property("CompletedWork") + .HasColumnType("double"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ParentTaskId") + .HasColumnType("char(36)"); + + b.Property("PlannedWork") + .HasColumnType("double"); + + b.Property("TaskDate") + .HasColumnType("datetime(6)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.Property("WorkAreaId") + .HasColumnType("char(36)"); + + b.Property("WorkCategoryId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId"); + + b.HasIndex("TenantId"); + + b.HasIndex("WorkAreaId"); + + b.HasIndex("WorkCategoryId"); + + b.ToTable("WorkItems"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Roles.ApplicationRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("Role") + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("ApplicationRoles"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Roles.JobRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("JobRoles"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Utilities.Inquiries", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("About") + .HasColumnType("longtext"); + + b.Property("ContactNumber") + .HasColumnType("longtext"); + + b.Property("ContactPerson") + .HasColumnType("longtext"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("IndustryId") + .HasColumnType("char(36)"); + + b.Property("OragnizationSize") + .HasColumnType("longtext"); + + b.Property("OrganizatioinName") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Inquiries"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(21) + .HasColumnType("varchar(21)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("SecurityStamp") + .HasColumnType("longtext"); + + b.Property("TwoFactorEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + + b.HasDiscriminator().HasValue("IdentityUser"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("RoleId") + .HasColumnType("varchar(255)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.ApplicationUser", b => + { + b.HasBaseType("Microsoft.AspNetCore.Identity.IdentityUser"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("IsRootUser") + .HasColumnType("tinyint(1)"); + + b.Property("TenantId") + .HasColumnType("char(36)"); + + b.HasDiscriminator().HasValue("ApplicationUser"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskAllocation", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "ApprovedBy") + .WithMany() + .HasForeignKey("ApprovedById"); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("AssignedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "ReportedBy") + .WithMany() + .HasForeignKey("ReportedById"); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Projects.WorkItem", "WorkItem") + .WithMany() + .HasForeignKey("WorkItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Master.WorkStatusMaster", "WorkStatus") + .WithMany() + .HasForeignKey("WorkStatusId"); + + b.Navigation("ApprovedBy"); + + b.Navigation("Employee"); + + b.Navigation("ReportedBy"); + + b.Navigation("Tenant"); + + b.Navigation("WorkItem"); + + b.Navigation("WorkStatus"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskComment", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("CommentedBy") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Activities.TaskAllocation", "TaskAllocation") + .WithMany() + .HasForeignKey("TaskAllocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("TaskAllocation"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Activities.TaskMembers", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Activities.TaskAllocation", "TaskAllocation") + .WithMany() + .HasForeignKey("TaskAllocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("TaskAllocation"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.AttendanceModule.Attendance", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "Approver") + .WithMany() + .HasForeignKey("EmployeeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Approver"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.AttendanceModule.AttendanceLog", b => + { + b.HasOne("Marco.Pms.Model.AttendanceModule.Attendance", "Attendance") + .WithMany() + .HasForeignKey("AttendanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.DocumentManager.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId"); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "UpdatedByEmployee") + .WithMany() + .HasForeignKey("UpdatedBy"); + + b.Navigation("Attendance"); + + b.Navigation("Document"); + + b.Navigation("Employee"); + + b.Navigation("Tenant"); + + b.Navigation("UpdatedByEmployee"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Authentication.MPINDetails", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Authentication.OTPDetails", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Authentication.RefreshToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.Bucket", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedByID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.Contact", b => + { + b.HasOne("Marco.Pms.Model.Directory.ContactCategoryMaster", "ContactCategory") + .WithMany() + .HasForeignKey("ContactCategoryId"); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.Navigation("ContactCategory"); + + b.Navigation("CreatedBy"); + + b.Navigation("Tenant"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactBucketMapping", b => + { + b.HasOne("Marco.Pms.Model.Directory.Bucket", "Bucket") + .WithMany() + .HasForeignKey("BucketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Directory.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Bucket"); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactCategoryMaster", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactEmail", b => + { + b.HasOne("Marco.Pms.Model.Directory.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactNote", b => + { + b.HasOne("Marco.Pms.Model.Directory.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "Createdby") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.Navigation("Contact"); + + b.Navigation("Createdby"); + + b.Navigation("Tenant"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactPhone", b => + { + b.HasOne("Marco.Pms.Model.Directory.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactProjectMapping", b => + { + b.HasOne("Marco.Pms.Model.Directory.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Projects.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contact"); + + b.Navigation("Project"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactTagMapping", b => + { + b.HasOne("Marco.Pms.Model.Directory.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Directory.ContactTagMaster", "ContactTag") + .WithMany() + .HasForeignKey("ContactTagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Contact"); + + b.Navigation("ContactTag"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.ContactTagMaster", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.DirectoryUpdateLog", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Directory.EmployeeBucketMapping", b => + { + b.HasOne("Marco.Pms.Model.Directory.Bucket", "Bucket") + .WithMany() + .HasForeignKey("BucketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Bucket"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Marco.Pms.Model.DocumentManager.Document", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Employees.Employee", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.ApplicationUser", "ApplicationUser") + .WithMany() + .HasForeignKey("ApplicationUserId"); + + b.HasOne("Marco.Pms.Model.Roles.JobRole", "JobRole") + .WithMany() + .HasForeignKey("JobRoleId"); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ApplicationUser"); + + b.Navigation("JobRole"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Employees.EmployeeRoleMapping", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Roles.ApplicationRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("Role"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Employees.WorkShift", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.FeaturePermission", b => + { + b.HasOne("Marco.Pms.Model.Master.Feature", "Feature") + .WithMany("FeaturePermissions") + .HasForeignKey("FeatureId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Feature"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.RolePermissionMappings", b => + { + b.HasOne("Marco.Pms.Model.Roles.ApplicationRole", null) + .WithMany() + .HasForeignKey("ApplicationRoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.FeaturePermission", null) + .WithMany() + .HasForeignKey("FeaturePermissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Marco.Pms.Model.Entitlements.Tenant", b => + { + b.HasOne("Marco.Pms.Model.Master.Industry", "Industry") + .WithMany() + .HasForeignKey("IndustryId"); + + b.Navigation("Industry"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketAttachment", b => + { + b.HasOne("Marco.Pms.Model.Forum.TicketComment", "TicketComment") + .WithMany("Attachments") + .HasForeignKey("CommentId"); + + b.HasOne("Marco.Pms.Model.Forum.TicketForum", "Ticket") + .WithMany() + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ticket"); + + b.Navigation("TicketComment"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketComment", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketForum", b => + { + b.HasOne("Marco.Pms.Model.Master.TicketPriorityMaster", "Priority") + .WithMany() + .HasForeignKey("PriorityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Master.TicketStatusMaster", "TicketStatusMaster") + .WithMany() + .HasForeignKey("StatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Forum.TicketTypeMaster", "TicketTypeMaster") + .WithMany() + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Priority"); + + b.Navigation("Tenant"); + + b.Navigation("TicketStatusMaster"); + + b.Navigation("TicketTypeMaster"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketTag", b => + { + b.HasOne("Marco.Pms.Model.Master.TicketTagMaster", "Tag") + .WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Forum.TicketForum", "Ticket") + .WithMany() + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tag"); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Mail.MailDetails", b => + { + b.HasOne("Marco.Pms.Model.Mail.MailingList", "MailBody") + .WithMany() + .HasForeignKey("MailListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MailBody"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.ActivityMaster", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.Feature", b => + { + b.HasOne("Marco.Pms.Model.Master.Module", "Module") + .WithMany() + .HasForeignKey("ModuleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Module"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.StatusMaster", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.WorkCategoryMaster", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.WorkStatusMaster", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.Building", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.Floor", b => + { + b.HasOne("Marco.Pms.Model.Projects.Building", "Building") + .WithMany() + .HasForeignKey("BuildingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Building"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.Project", b => + { + b.HasOne("Marco.Pms.Model.Master.StatusMaster", "ProjectStatus") + .WithMany() + .HasForeignKey("ProjectStatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProjectStatus"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.ProjectAllocation", b => + { + b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Projects.Project", "Project") + .WithMany() + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("Project"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.WorkArea", b => + { + b.HasOne("Marco.Pms.Model.Projects.Floor", "Floor") + .WithMany() + .HasForeignKey("FloorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Floor"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Projects.WorkItem", b => + { + b.HasOne("Marco.Pms.Model.Master.ActivityMaster", "ActivityMaster") + .WithMany() + .HasForeignKey("ActivityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Projects.WorkArea", "WorkArea") + .WithMany() + .HasForeignKey("WorkAreaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Marco.Pms.Model.Master.WorkCategoryMaster", "WorkCategoryMaster") + .WithMany() + .HasForeignKey("WorkCategoryId"); + + b.Navigation("ActivityMaster"); + + b.Navigation("Tenant"); + + b.Navigation("WorkArea"); + + b.Navigation("WorkCategoryMaster"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Roles.ApplicationRole", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Marco.Pms.Model.Roles.JobRole", b => + { + b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Marco.Pms.Model.Forum.TicketComment", b => + { + b.Navigation("Attachments"); + }); + + modelBuilder.Entity("Marco.Pms.Model.Master.Feature", b => + { + b.Navigation("FeaturePermissions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Marco.Pms.DataAccess/Migrations/20250702045931_Added_Designation_Paraneter_In_Contacts_Table.cs b/Marco.Pms.DataAccess/Migrations/20250702045931_Added_Designation_Paraneter_In_Contacts_Table.cs new file mode 100644 index 0000000..45231d4 --- /dev/null +++ b/Marco.Pms.DataAccess/Migrations/20250702045931_Added_Designation_Paraneter_In_Contacts_Table.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Marco.Pms.DataAccess.Migrations +{ + /// + public partial class Added_Designation_Paraneter_In_Contacts_Table : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Designation", + table: "Contacts", + type: "longtext", + nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Designation", + table: "Contacts"); + } + } +} diff --git a/Marco.Pms.DataAccess/Migrations/ApplicationDbContextModelSnapshot.cs b/Marco.Pms.DataAccess/Migrations/ApplicationDbContextModelSnapshot.cs index 57bcc44..eab79d5 100644 --- a/Marco.Pms.DataAccess/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Marco.Pms.DataAccess/Migrations/ApplicationDbContextModelSnapshot.cs @@ -420,6 +420,10 @@ namespace Marco.Pms.DataAccess.Migrations .IsRequired() .HasColumnType("longtext"); + b.Property("Designation") + .IsRequired() + .HasColumnType("longtext"); + b.Property("IsActive") .HasColumnType("tinyint(1)"); @@ -752,10 +756,15 @@ namespace Marco.Pms.DataAccess.Migrations b.Property("UploadedAt") .HasColumnType("datetime(6)"); + b.Property("UploadedById") + .HasColumnType("char(36)"); + b.HasKey("Id"); b.HasIndex("TenantId"); + b.HasIndex("UploadedById"); + b.ToTable("Documents"); }); @@ -3316,7 +3325,13 @@ namespace Marco.Pms.DataAccess.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("Marco.Pms.Model.Employees.Employee", "UploadedBy") + .WithMany() + .HasForeignKey("UploadedById"); + b.Navigation("Tenant"); + + b.Navigation("UploadedBy"); }); modelBuilder.Entity("Marco.Pms.Model.Employees.Employee", b => diff --git a/Marco.Pms.Model/Directory/Contact.cs b/Marco.Pms.Model/Directory/Contact.cs index fe82711..4cbcb37 100644 --- a/Marco.Pms.Model/Directory/Contact.cs +++ b/Marco.Pms.Model/Directory/Contact.cs @@ -12,6 +12,7 @@ namespace Marco.Pms.Model.Directory //public Guid? ProjectId { get; set; } public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; + public string Designation { get; set; } = string.Empty; public string Organization { get; set; } = string.Empty; public string? Address { get; set; } public bool IsActive { get; set; } = true; diff --git a/Marco.Pms.Model/DocumentManager/Document.cs b/Marco.Pms.Model/DocumentManager/Document.cs index 3652774..bcbe7a7 100644 --- a/Marco.Pms.Model/DocumentManager/Document.cs +++ b/Marco.Pms.Model/DocumentManager/Document.cs @@ -1,4 +1,7 @@ -using Marco.Pms.Model.Utilities; +using System.ComponentModel.DataAnnotations.Schema; +using Marco.Pms.Model.Employees; +using Marco.Pms.Model.Utilities; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; namespace Marco.Pms.Model.DocumentManager { @@ -16,10 +19,15 @@ namespace Marco.Pms.Model.DocumentManager /// public string? ThumbS3Key { get; set; } - public string? Base64Data { get; set; } + public string? Base64Data { get; set; } = null; public long FileSize { get; set; } public string ContentType { get; set; } = string.Empty; + public Guid? UploadedById { get; set; } + + [ValidateNever] + [ForeignKey("UploadedById")] + public Employee? UploadedBy { get; set; } public DateTime UploadedAt { get; set; } } } diff --git a/Marco.Pms.Model/Dtos/Directory/CreateContactDto.cs b/Marco.Pms.Model/Dtos/Directory/CreateContactDto.cs index 577f405..f581fe3 100644 --- a/Marco.Pms.Model/Dtos/Directory/CreateContactDto.cs +++ b/Marco.Pms.Model/Dtos/Directory/CreateContactDto.cs @@ -9,6 +9,7 @@ public List? BucketIds { get; set; } public Guid? ContactCategoryId { get; set; } public string? Description { get; set; } + public string? Designation { get; set; } public string? Organization { get; set; } public string? Address { get; set; } public List? Tags { get; set; } diff --git a/Marco.Pms.Model/Dtos/Directory/UpdateContactDto.cs b/Marco.Pms.Model/Dtos/Directory/UpdateContactDto.cs index 16c8645..b4d18d8 100644 --- a/Marco.Pms.Model/Dtos/Directory/UpdateContactDto.cs +++ b/Marco.Pms.Model/Dtos/Directory/UpdateContactDto.cs @@ -10,6 +10,7 @@ public List? BucketIds { get; set; } public Guid? ContactCategoryId { get; set; } public string? Description { get; set; } + public string? Designation { get; set; } public string? Organization { get; set; } public string? Address { get; set; } public List? Tags { get; set; } diff --git a/Marco.Pms.Model/Dtos/DocumentManager/DocumentBatchDto.cs b/Marco.Pms.Model/Dtos/DocumentManager/DocumentBatchDto.cs new file mode 100644 index 0000000..a3befae --- /dev/null +++ b/Marco.Pms.Model/Dtos/DocumentManager/DocumentBatchDto.cs @@ -0,0 +1,10 @@ +using Marco.Pms.Model.DocumentManager; + +namespace Marco.Pms.Model.Dtos.DocumentManager +{ + public class DocumentBatchDto + { + public Guid? BatchId { get; set; } + public List? Documents { get; set; } + } +} diff --git a/Marco.Pms.Model/Dtos/Projects/BuildingDot.cs b/Marco.Pms.Model/Dtos/Projects/BuildingDto.cs similarity index 92% rename from Marco.Pms.Model/Dtos/Projects/BuildingDot.cs rename to Marco.Pms.Model/Dtos/Projects/BuildingDto.cs index a5b160b..e6a7b89 100644 --- a/Marco.Pms.Model/Dtos/Projects/BuildingDot.cs +++ b/Marco.Pms.Model/Dtos/Projects/BuildingDto.cs @@ -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; } diff --git a/Marco.Pms.Model/Dtos/Projects/FloorDot.cs b/Marco.Pms.Model/Dtos/Projects/FloorDto.cs similarity index 92% rename from Marco.Pms.Model/Dtos/Projects/FloorDot.cs rename to Marco.Pms.Model/Dtos/Projects/FloorDto.cs index a3d1c86..3dbe06f 100644 --- a/Marco.Pms.Model/Dtos/Projects/FloorDot.cs +++ b/Marco.Pms.Model/Dtos/Projects/FloorDto.cs @@ -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; } diff --git a/Marco.Pms.Model/Dtos/Projects/InfraDot.cs b/Marco.Pms.Model/Dtos/Projects/InfraDot.cs deleted file mode 100644 index 7c16c09..0000000 --- a/Marco.Pms.Model/Dtos/Projects/InfraDot.cs +++ /dev/null @@ -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; } - } -} diff --git a/Marco.Pms.Model/Dtos/Projects/InfraDto.cs b/Marco.Pms.Model/Dtos/Projects/InfraDto.cs new file mode 100644 index 0000000..09d1462 --- /dev/null +++ b/Marco.Pms.Model/Dtos/Projects/InfraDto.cs @@ -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; } + } +} diff --git a/Marco.Pms.Model/Dtos/Projects/WorkAreaDot.cs b/Marco.Pms.Model/Dtos/Projects/WorkAreaDto.cs similarity index 91% rename from Marco.Pms.Model/Dtos/Projects/WorkAreaDot.cs rename to Marco.Pms.Model/Dtos/Projects/WorkAreaDto.cs index 604ee3e..ffc80c4 100644 --- a/Marco.Pms.Model/Dtos/Projects/WorkAreaDot.cs +++ b/Marco.Pms.Model/Dtos/Projects/WorkAreaDto.cs @@ -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; } diff --git a/Marco.Pms.Model/Dtos/Projects/WorkItemDot.cs b/Marco.Pms.Model/Dtos/Projects/WorkItemDto.cs similarity index 94% rename from Marco.Pms.Model/Dtos/Projects/WorkItemDot.cs rename to Marco.Pms.Model/Dtos/Projects/WorkItemDto.cs index e6ba436..7c98051 100644 --- a/Marco.Pms.Model/Dtos/Projects/WorkItemDot.cs +++ b/Marco.Pms.Model/Dtos/Projects/WorkItemDto.cs @@ -2,7 +2,7 @@ namespace Marco.Pms.Model.Dtos.Project { - public class WorkItemDot + public class WorkItemDto { [Key] public Guid? Id { get; set; } diff --git a/Marco.Pms.Model/Entitlements/PermissionsMaster.cs b/Marco.Pms.Model/Entitlements/PermissionsMaster.cs new file mode 100644 index 0000000..d0bef58 --- /dev/null +++ b/Marco.Pms.Model/Entitlements/PermissionsMaster.cs @@ -0,0 +1,28 @@ +namespace Marco.Pms.Model.Entitlements +{ + public static class PermissionsMaster + { + public static readonly Guid DirectoryAdmin = Guid.Parse("4286a13b-bb40-4879-8c6d-18e9e393beda"); + public static readonly Guid DirectoryManager = Guid.Parse("62668630-13ce-4f52-a0f0-db38af2230c5"); + public static readonly Guid DirectoryUser = Guid.Parse("0f919170-92d4-4337-abd3-49b66fc871bb"); + public static readonly Guid ViewProject = Guid.Parse("6ea44136-987e-44ba-9e5d-1cf8f5837ebc"); + public static readonly Guid ManageProject = Guid.Parse("172fc9b6-755b-4f62-ab26-55c34a330614"); + public static readonly Guid ManageTeam = Guid.Parse("b94802ce-0689-4643-9e1d-11c86950c35b"); + public static readonly Guid ViewProjectInfra = Guid.Parse("8d7cc6e3-9147-41f7-aaa7-fa507e450bd4"); + public static readonly Guid ManageProjectInfra = Guid.Parse("cf2825ad-453b-46aa-91d9-27c124d63373"); + public static readonly Guid ViewTask = Guid.Parse("9fcc5f87-25e3-4846-90ac-67a71ab92e3c"); + public static readonly Guid AddAndEditTask = Guid.Parse("08752f33-3b29-4816-b76b-ea8a968ed3c5"); + public static readonly Guid AssignAndReportProgress = Guid.Parse("6a32379b-8b3f-49a6-8c48-4b7ac1b55dc2"); + public static readonly Guid ApproveTask = Guid.Parse("db4e40c5-2ba9-4b6d-b8a6-a16a250ff99c"); + public static readonly Guid ViewAllEmployees = Guid.Parse("60611762-7f8a-4fb5-b53f-b1139918796b"); + public static readonly Guid ViewTeamMembers = Guid.Parse("b82d2b7e-0d52-45f3-997b-c008ea460e7f"); + public static readonly Guid AddAndEditEmployee = Guid.Parse("a97d366a-c2bb-448d-be93-402bd2324566"); + public static readonly Guid AssignRoles = Guid.Parse("fbd213e0-0250-46f1-9f5f-4b2a1e6e76a3"); + public static readonly Guid TeamAttendance = Guid.Parse("915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"); + public static readonly Guid RegularizeAttendance = Guid.Parse("57802c4a-00aa-4a1f-a048-fd2f70dd44b6"); + public static readonly Guid SelfAttendance = Guid.Parse("ccb0589f-712b-43de-92ed-5b6088e7dc4e"); + public static readonly Guid ViewMasters = Guid.Parse("5ffbafe0-7ab0-48b1-bb50-c1bf76b65f9d"); + public static readonly Guid ManageMasters = Guid.Parse("588a8824-f924-4955-82d8-fc51956cf323"); + } +} + diff --git a/Marco.Pms.Model/Mapper/DirectoryMapper.cs b/Marco.Pms.Model/Mapper/DirectoryMapper.cs index b175cb7..c9965c8 100644 --- a/Marco.Pms.Model/Mapper/DirectoryMapper.cs +++ b/Marco.Pms.Model/Mapper/DirectoryMapper.cs @@ -16,6 +16,7 @@ namespace Marco.Pms.Model.Mapper Name = createContactDto.Name ?? string.Empty, ContactCategoryId = createContactDto.ContactCategoryId, Description = createContactDto.Description ?? string.Empty, + Designation = createContactDto.Designation ?? string.Empty, Organization = createContactDto?.Organization ?? string.Empty, Address = createContactDto != null ? createContactDto.Address : string.Empty, CreatedById = employeeId, @@ -34,6 +35,7 @@ namespace Marco.Pms.Model.Mapper CreatedAt = contact.CreatedAt, CreatedById = contact.CreatedById, Description = updateContactDto.Description ?? string.Empty, + Designation = updateContactDto.Designation ?? string.Empty, Organization = updateContactDto?.Organization ?? string.Empty, Address = updateContactDto != null ? updateContactDto.Address : string.Empty, TenantId = tenantId @@ -47,6 +49,7 @@ namespace Marco.Pms.Model.Mapper Name = contact.Name, ContactCategory = contact.ContactCategory != null ? contact.ContactCategory.ToContactCategoryVMFromContactCategoryMaster() : null, Description = contact.Description ?? string.Empty, + Designation = contact.Designation ?? string.Empty, Organization = contact.Organization ?? string.Empty, Address = contact.Address ?? string.Empty }; @@ -59,6 +62,7 @@ namespace Marco.Pms.Model.Mapper Name = contact.Name, ContactCategory = contact.ContactCategory != null ? contact.ContactCategory.ToContactCategoryVMFromContactCategoryMaster() : null, Description = contact.Description ?? string.Empty, + Designation = contact.Designation ?? string.Empty, Organization = contact.Organization ?? string.Empty, Address = contact.Address ?? string.Empty, CreatedAt = contact.CreatedAt, diff --git a/Marco.Pms.Model/Mapper/ForumMapper.cs b/Marco.Pms.Model/Mapper/ForumMapper.cs index c5f84ba..cf15331 100644 --- a/Marco.Pms.Model/Mapper/ForumMapper.cs +++ b/Marco.Pms.Model/Mapper/ForumMapper.cs @@ -90,29 +90,35 @@ namespace Marco.Pms.Model.Mapper }; } - public static Document ToDocumentFromForumAttachmentDto(this ForumAttachmentDto AttachmentDto, string objectKey, string thumbS3Key, DateTime uploadedAt, Guid tenantId) + public static Document ToDocumentFromForumAttachmentDto(this ForumAttachmentDto AttachmentDto, string objectKey, string thumbS3Key, DateTime uploadedAt, + Guid tenantId, Guid batchId, Guid loggedInEmployeeId) { return new Document { + BatchId = batchId, + UploadedById = loggedInEmployeeId, FileName = AttachmentDto.FileName, ContentType = AttachmentDto.ContentType, S3Key = objectKey, ThumbS3Key = thumbS3Key, - Base64Data = AttachmentDto.Base64Data, + //Base64Data = AttachmentDto.Base64Data, FileSize = AttachmentDto.FileSize, UploadedAt = uploadedAt, TenantId = tenantId }; } - public static Document ToDocumentFromUpdateAttachmentDto(this UpdateAttachmentDto AttachmentDto, string objectKey, string thumbS3Key, DateTime uploadedAt, Guid tenantId) + public static Document ToDocumentFromUpdateAttachmentDto(this UpdateAttachmentDto AttachmentDto, string objectKey, string thumbS3Key, DateTime uploadedAt, + Guid tenantId, Guid batchId, Guid loggedInEmployeeId) { return new Document { + BatchId = batchId, + UploadedById = loggedInEmployeeId, FileName = AttachmentDto.FileName, ContentType = AttachmentDto.ContentType, S3Key = objectKey, ThumbS3Key = thumbS3Key, - Base64Data = AttachmentDto.Base64Data, + //Base64Data = AttachmentDto.Base64Data, FileSize = AttachmentDto.FileSize, UploadedAt = uploadedAt, TenantId = tenantId diff --git a/Marco.Pms.Model/Mapper/InfraMapper.cs b/Marco.Pms.Model/Mapper/InfraMapper.cs index 4ccb7c8..5364494 100644 --- a/Marco.Pms.Model/Mapper/InfraMapper.cs +++ b/Marco.Pms.Model/Mapper/InfraMapper.cs @@ -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 { diff --git a/Marco.Pms.Model/Marco.Pms.Model.csproj b/Marco.Pms.Model/Marco.Pms.Model.csproj index d5927ce..a1a21a5 100644 --- a/Marco.Pms.Model/Marco.Pms.Model.csproj +++ b/Marco.Pms.Model/Marco.Pms.Model.csproj @@ -10,6 +10,7 @@ + diff --git a/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs new file mode 100644 index 0000000..cc77d96 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/ActivityMasterMongoDB.cs @@ -0,0 +1,9 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class ActivityMasterMongoDB + { + public string Id { get; set; } = string.Empty; + public string? ActivityName { get; set; } + public string? UnitOfMeasurement { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs b/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs new file mode 100644 index 0000000..786ceb5 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs @@ -0,0 +1,22 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class BuildingMongoDB + { + public string Id { get; set; } = string.Empty; + public string? BuildingName { get; set; } + public string? Description { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } + public string ProjectId { get; set; } = string.Empty; + public List Floors { get; set; } = new List(); + } + public class BuildingMongoDBVM + { + public string Id { get; set; } = string.Empty; + public string? BuildingName { get; set; } + public string? Description { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } + public string ProjectId { get; set; } = string.Empty; + } +} diff --git a/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs b/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs new file mode 100644 index 0000000..fab2b84 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/EmployeePermissionMongoDB.cs @@ -0,0 +1,14 @@ +using MongoDB.Bson.Serialization.Attributes; + +namespace Marco.Pms.Model.MongoDBModels +{ + [BsonIgnoreExtraElements] + public class EmployeePermissionMongoDB + { + public string Id { get; set; } = string.Empty; // Employee ID + public List ApplicationRoleIds { get; set; } = new List(); + public List PermissionIds { get; set; } = new List(); + public List ProjectIds { get; set; } = new List(); + public DateTime ExpireAt { get; set; } = DateTime.UtcNow.Date.AddDays(1); + } +} diff --git a/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs b/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs new file mode 100644 index 0000000..15d3060 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs @@ -0,0 +1,21 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class FloorMongoDB + { + public string Id { get; set; } = string.Empty; + public string BuildingId { get; set; } = string.Empty; + public string? FloorName { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } + public List WorkAreas { get; set; } = new List(); + } + + public class FloorMongoDBVM + { + public string Id { get; set; } = string.Empty; + public string BuildingId { get; set; } = string.Empty; + public string? FloorName { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs new file mode 100644 index 0000000..aac0e2c --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/ProjectMongoDB.cs @@ -0,0 +1,19 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class ProjectMongoDB + { + public string Id { get; set; } = string.Empty; + public string? Name { get; set; } + public string? ShortName { get; set; } + public string? ProjectAddress { get; set; } + public string? ContactPerson { get; set; } + public List Buildings { get; set; } = new List(); + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + public StatusMasterMongoDB? ProjectStatus { get; set; } + public int TeamSize { get; set; } + public double CompletedWork { get; set; } + public double PlannedWork { get; set; } + public DateTime ExpireAt { get; set; } = DateTime.UtcNow.Date.AddDays(1); + } +} diff --git a/Marco.Pms.Model/MongoDBModels/ProjectReportEmailMongoDB.cs b/Marco.Pms.Model/MongoDBModels/ProjectReportEmailMongoDB.cs new file mode 100644 index 0000000..519ea4f --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/ProjectReportEmailMongoDB.cs @@ -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? Receivers { get; set; } + public bool IsSent { get; set; } = false; + } +} diff --git a/Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs new file mode 100644 index 0000000..77e8eb5 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/StatusMasterMongoDB.cs @@ -0,0 +1,8 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class StatusMasterMongoDB + { + public string Id { get; set; } = string.Empty; + public string? Status { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/WorkAreaInfoMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkAreaInfoMongoDB.cs new file mode 100644 index 0000000..da1001b --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/WorkAreaInfoMongoDB.cs @@ -0,0 +1,13 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class WorkAreaInfoMongoDB + { + public string ProjectId { get; set; } = string.Empty; + public string? ProjectName { get; set; } + public BuildingMongoDBVM? Building { get; set; } + public FloorMongoDBVM? Floor { get; set; } + public WorkAreaMongoDB? WorkArea { get; set; } + public double CompletedWork { get; set; } + public double PlannedWork { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs new file mode 100644 index 0000000..412c940 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs @@ -0,0 +1,16 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class WorkAreaMongoDB + { + public string Id { get; set; } = string.Empty; + public string FloorId { get; set; } = string.Empty; + public string? AreaName { get; set; } + public double PlannedWork { get; set; } + public double CompletedWork { get; set; } + } + public class WorkAreaMongoDBVM + { + public string Id { get; set; } = string.Empty; + public string? AreaName { get; set; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs new file mode 100644 index 0000000..4ea4682 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/WorkCategoryMasterMongoDB.cs @@ -0,0 +1,9 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class WorkCategoryMasterMongoDB + { + public string Id { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + } +} diff --git a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs new file mode 100644 index 0000000..cf798f3 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs @@ -0,0 +1,17 @@ +namespace Marco.Pms.Model.MongoDBModels +{ + public class WorkItemMongoDB + { + public string Id { get; set; } = string.Empty; + public string WorkAreaId { get; set; } = string.Empty; + public ActivityMasterMongoDB? ActivityMaster { get; set; } + public WorkCategoryMasterMongoDB? WorkCategoryMaster { get; set; } + public string? ParentTaskId { get; set; } = null; + public double PlannedWork { get; set; } = 0; + public double TodaysAssigned { get; set; } = 0; + public double CompletedWork { get; set; } = 0; + public string? Description { get; set; } + public DateTime TaskDate { get; set; } + public DateTime ExpireAt { get; set; } = DateTime.UtcNow.Date.AddDays(1); + } +} diff --git a/Marco.Pms.Model/Utilities/ImageFilter.cs b/Marco.Pms.Model/Utilities/ImageFilter.cs new file mode 100644 index 0000000..a5cb7f7 --- /dev/null +++ b/Marco.Pms.Model/Utilities/ImageFilter.cs @@ -0,0 +1,14 @@ +namespace Marco.Pms.Model.Utilities +{ + public class ImageFilter + { + public List? BuildingIds { get; set; } + public List? FloorIds { get; set; } + public List? WorkAreaIds { get; set; } + public List? WorkCategoryIds { get; set; } + public List? ActivityIds { get; set; } + public List? UploadedByIds { get; set; } + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + } +} diff --git a/Marco.Pms.Model/Utilities/ServiceResponse.cs b/Marco.Pms.Model/Utilities/ServiceResponse.cs new file mode 100644 index 0000000..a76c45c --- /dev/null +++ b/Marco.Pms.Model/Utilities/ServiceResponse.cs @@ -0,0 +1,8 @@ +namespace Marco.Pms.Model.Utilities +{ + public class ServiceResponse + { + public object? Notification { get; set; } + public ApiResponse Response { get; set; } = ApiResponse.ErrorResponse(""); + } +} diff --git a/Marco.Pms.Model/ViewModels/Directory/ContactProfileVM.cs b/Marco.Pms.Model/ViewModels/Directory/ContactProfileVM.cs index 9e8f4cb..de53d25 100644 --- a/Marco.Pms.Model/ViewModels/Directory/ContactProfileVM.cs +++ b/Marco.Pms.Model/ViewModels/Directory/ContactProfileVM.cs @@ -9,6 +9,7 @@ namespace Marco.Pms.Model.ViewModels.Directory public Guid Id { get; set; } public string? Name { get; set; } public string? Description { get; set; } + public string? Designation { get; set; } public string? Organization { get; set; } public string? Address { get; set; } public DateTime CreatedAt { get; set; } diff --git a/Marco.Pms.Model/ViewModels/Directory/ContactVM.cs b/Marco.Pms.Model/ViewModels/Directory/ContactVM.cs index d394f73..4b212ae 100644 --- a/Marco.Pms.Model/ViewModels/Directory/ContactVM.cs +++ b/Marco.Pms.Model/ViewModels/Directory/ContactVM.cs @@ -12,6 +12,7 @@ namespace Marco.Pms.Model.ViewModels.Directory public ContactCategoryVM? ContactCategory { get; set; } public List? BucketIds { get; set; } public string? Description { get; set; } + public string? Designation { get; set; } public string? Organization { get; set; } public string? Address { get; set; } public List? Tags { get; set; } diff --git a/Marco.Pms.Model/ViewModels/Projects/OldProjectVM.cs b/Marco.Pms.Model/ViewModels/Projects/OldProjectVM.cs new file mode 100644 index 0000000..cb38dfc --- /dev/null +++ b/Marco.Pms.Model/ViewModels/Projects/OldProjectVM.cs @@ -0,0 +1,10 @@ +using Marco.Pms.Model.Dtos.Project; + +namespace Marco.Pms.Model.ViewModels.Projects +{ + public class OldProjectVM : ProjectDto + { + public List? Buildings { get; set; } + + } +} diff --git a/Marco.Pms.Model/ViewModels/Projects/ProjectAllocationVM.cs b/Marco.Pms.Model/ViewModels/Projects/ProjectAllocationVM.cs new file mode 100644 index 0000000..6d9138e --- /dev/null +++ b/Marco.Pms.Model/ViewModels/Projects/ProjectAllocationVM.cs @@ -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; } + } +} diff --git a/Marco.Pms.Model/ViewModels/Projects/ProjectVM.cs b/Marco.Pms.Model/ViewModels/Projects/ProjectVM.cs index cd349bb..240b35f 100644 --- a/Marco.Pms.Model/ViewModels/Projects/ProjectVM.cs +++ b/Marco.Pms.Model/ViewModels/Projects/ProjectVM.cs @@ -1,10 +1,17 @@ -using Marco.Pms.Model.Dtos.Project; +using Marco.Pms.Model.Master; namespace Marco.Pms.Model.ViewModels.Projects { - public class ProjectVM : ProjectDto + public class ProjectVM { - public List? Buildings { get; set; } + public Guid Id { get; set; } + public string? Name { get; set; } + public string? ShortName { get; set; } + public string? ProjectAddress { get; set; } + public string? ContactPerson { get; set; } + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + public StatusMaster? ProjectStatus { get; set; } } } diff --git a/Marco.Pms.Services/Controllers/AttendanceController.cs b/Marco.Pms.Services/Controllers/AttendanceController.cs index d23a007..7339966 100644 --- a/Marco.Pms.Services/Controllers/AttendanceController.cs +++ b/Marco.Pms.Services/Controllers/AttendanceController.cs @@ -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 signalR) + ApplicationDbContext context, EmployeeHelper employeeHelper, IProjectServices projectServices, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permission, IHubContext 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 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 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 attendanceLogVMs = new List(); 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.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.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.ErrorResponse("Employee ID is required and must not be Empty.", "Employee ID is required and must not be empty.", 400)); } List 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.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.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.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 lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date >= fromDate.Date && c.AttendanceDate.Date <= toDate.Date && c.TenantId == TenantId).ToListAsync(); - List projectteam = await _projectsHelper.GetTeamByProject(TenantId, projectId, true); + List 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.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.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 lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date == forDate && c.TenantId == TenantId).ToListAsync(); - List projectteam = await _projectsHelper.GetTeamByProject(TenantId, projectId, IncludeInActive); + List 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(); - 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 lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE && c.TenantId == TenantId).ToListAsync(); - - List projectteam = await _projectsHelper.GetTeamByProject(TenantId, projectId, true); + List 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.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.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.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.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,12 +605,13 @@ 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.ErrorResponse("Invalid data", errors, 400)); } Guid tenantId = GetTenantId(); - var currentEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var batchId = Guid.NewGuid(); using var transaction = await _context.Database.BeginTransactionAsync(); try @@ -704,10 +712,12 @@ namespace MarcoBMS.Services.Controllers document = new Document { + BatchId = batchId, + UploadedById = loggedInEmployee.Id, FileName = recordAttendanceDot.Image.FileName ?? "", ContentType = recordAttendanceDot.Image.ContentType, S3Key = objectKey, - Base64Data = recordAttendanceDot.Image.Base64Data, + //Base64Data = recordAttendanceDot.Image.Base64Data, FileSize = recordAttendanceDot.Image.FileSize, UploadedAt = recordAttendanceDot.Date, TenantId = tenantId @@ -728,7 +738,7 @@ namespace MarcoBMS.Services.Controllers Longitude = recordAttendanceDot.Longitude, DocumentId = document?.Id, TenantId = tenantId, - UpdatedBy = recordAttendanceDot.EmployeeID, + UpdatedBy = loggedInEmployee.Id, UpdatedOn = recordAttendanceDot.Date }; _context.AttendanceLogs.Add(attendanceLog); @@ -755,7 +765,7 @@ namespace MarcoBMS.Services.Controllers var notification = new { - LoggedInUserId = currentEmployee.Id, + LoggedInUserId = loggedInEmployee.Id, Keyword = "Attendance", Activity = recordAttendanceDot.Id == Guid.Empty ? 1 : 0, ProjectId = attendance.ProjectID, @@ -771,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.ErrorResponse("Something went wrong", ex.Message, 500)); } } diff --git a/Marco.Pms.Services/Controllers/AuthController.cs b/Marco.Pms.Services/Controllers/AuthController.cs index 1b45eb7..429a38b 100644 --- a/Marco.Pms.Services/Controllers/AuthController.cs +++ b/Marco.Pms.Services/Controllers/AuthController.cs @@ -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.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.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.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.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.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.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.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.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.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.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.ErrorResponse("Provided invalid information", "Provided invalid information", 400)); } diff --git a/Marco.Pms.Services/Controllers/DashboardController.cs b/Marco.Pms.Services/Controllers/DashboardController.cs index 8ed0ba0..108a3ec 100644 --- a/Marco.Pms.Services/Controllers/DashboardController.cs +++ b/Marco.Pms.Services/Controllers/DashboardController.cs @@ -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,12 +22,15 @@ namespace Marco.Pms.Services.Controllers { private readonly ApplicationDbContext _context; private readonly UserHelper _userHelper; + private readonly IProjectServices _projectServices; private readonly ILoggingService _logger; private readonly PermissionServices _permissionServices; - public DashboardController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, PermissionServices permissionServices) + public static readonly Guid ActiveId = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731"); + public DashboardController(ApplicationDbContext context, UserHelper userHelper, IProjectServices projectServices, ILoggingService logger, PermissionServices permissionServices) { _context = context; _userHelper = userHelper; + _projectServices = projectServices; _logger = logger; _permissionServices = permissionServices; } @@ -162,46 +166,188 @@ namespace Marco.Pms.Services.Controllers return Ok(ApiResponse.SuccessResponse(projectDashboardVM, "Success", 200)); } + /// + /// Retrieves a dashboard summary of total employees and today's attendance. + /// If a projectId is provided, it returns totals for that project; otherwise, for all accessible active projects. + /// + /// Optional. The ID of a specific project to get totals for. [HttpGet("teams")] - public async Task GetTotalEmployees() + public async Task GetTotalEmployees([FromQuery] Guid? projectId) { - var tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var date = DateTime.UtcNow.Date; - - var Employees = await _context.Employees.Where(e => e.TenantId == tenantId && e.IsActive == true).Select(e => e.Id).ToListAsync(); - - var checkedInEmployee = await _context.Attendes.Where(e => e.InTime != null ? e.InTime.Value.Date == date : false).Select(e => e.EmployeeID).ToListAsync(); - - TeamDashboardVM teamDashboardVM = new TeamDashboardVM + try { - TotalEmployees = Employees.Count(), - InToday = checkedInEmployee.Distinct().Count() - }; - _logger.LogInfo("Today's total checked in employees fetched by employee {EmployeeId}", LoggedInEmployee.Id); - return Ok(ApiResponse.SuccessResponse(teamDashboardVM, "Success", 200)); - } + var tenantId = _userHelper.GetTenantId(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - [HttpGet("tasks")] - public async Task GetTotalTasks() - { - var tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var Tasks = await _context.WorkItems.Where(t => t.TenantId == tenantId).Select(t => new { PlannedWork = t.PlannedWork, CompletedWork = t.CompletedWork }).ToListAsync(); - TasksDashboardVM tasksDashboardVM = new TasksDashboardVM - { - TotalTasks = 0, - CompletedTasks = 0 - }; - foreach (var task in Tasks) - { - tasksDashboardVM.TotalTasks += task.PlannedWork; - tasksDashboardVM.CompletedTasks += task.CompletedWork; + _logger.LogInfo("GetTotalEmployees called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty); + + // --- Step 1: Get the list of projects the user can access --- + // This query is more efficient as it only selects the IDs needed. + var projects = await _projectServices.GetMyProjectIdsAsync(tenantId, loggedInEmployee); + + var accessibleActiveProjectIds = await _context.Projects + .Where(p => p.ProjectStatusId == ActiveId && projects.Contains(p.Id)) + .Select(p => p.Id) + .ToListAsync(); + + if (!accessibleActiveProjectIds.Any()) + { + _logger.LogInfo("User {UserId} has no accessible active projects.", loggedInEmployee.Id); + return Ok(ApiResponse.SuccessResponse(new TeamDashboardVM(), "No accessible active projects found.", 200)); + } + + // --- Step 2: Build the list of project IDs to query against --- + List finalProjectIds; + + if (projectId.HasValue) + { + // Security Check: Ensure the requested project is in the user's accessible list. + var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId} (not active or not accessible).", loggedInEmployee.Id, projectId.Value); + return StatusCode(403, ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to view this project, or it is not active.", 403)); + } + finalProjectIds = new List { projectId.Value }; + } + else + { + finalProjectIds = accessibleActiveProjectIds; + } + + // --- Step 3: Run efficient aggregation queries SEQUENTIALLY --- + // Since we only have one DbContext instance, we await each query one by one. + + // Query 1: Count total distinct employees allocated to the final project list + int totalEmployees = await _context.ProjectAllocations + .Where(pa => pa.TenantId == tenantId && + finalProjectIds.Contains(pa.ProjectId) && + pa.IsActive) + .Select(pa => pa.EmployeeId) + .Distinct() + .CountAsync(); + + // Query 2: Count total distinct employees who checked in today + // Use an efficient date range check + var today = DateTime.UtcNow.Date; + var tomorrow = today.AddDays(1); + + int inTodays = await _context.Attendes + .Where(a => a.InTime >= today && a.InTime < tomorrow && + finalProjectIds.Contains(a.ProjectID)) + .Select(a => a.EmployeeID) + .Distinct() + .CountAsync(); + + // --- Step 4: Assemble the response --- + var teamDashboardVM = new TeamDashboardVM + { + TotalEmployees = totalEmployees, + InToday = inTodays + }; + + _logger.LogInfo("Successfully fetched team dashboard for user {UserId}. Total: {TotalEmployees}, InToday: {InToday}", + loggedInEmployee.Id, teamDashboardVM.TotalEmployees, teamDashboardVM.InToday); + + return Ok(ApiResponse.SuccessResponse(teamDashboardVM, "Dashboard data retrieved successfully.", 200)); + } + catch (Exception ex) + { + _logger.LogError(ex, "An unexpected error occurred in GetTotalEmployees for projectId {ProjectId}", projectId ?? Guid.Empty); + return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred.", null, 500)); } - _logger.LogInfo("Total targeted tasks and total completed tasks fetched by employee {EmployeeId}", LoggedInEmployee.Id); - return Ok(ApiResponse.SuccessResponse(tasksDashboardVM, "Success", 200)); } + /// + /// Retrieves a dashboard summary of total planned and completed tasks. + /// If a projectId is provided, it returns totals for that project; otherwise, for all accessible projects. + /// + /// Optional. The ID of a specific project to get totals for. + /// An ApiResponse containing the task dashboard summary. + [HttpGet("tasks")] // Example route + public async Task GetTotalTasks1([FromQuery] Guid? projectId) // Changed to FromQuery as it's optional + { + try + { + var tenantId = _userHelper.GetTenantId(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + _logger.LogInfo("GetTotalTasks called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty); + + // --- Step 1: Build the base IQueryable for WorkItems --- + // This query is NOT executed yet. We will add more filters to it. + var baseWorkItemQuery = _context.WorkItems.Where(t => t.TenantId == tenantId); + + // --- Step 2: Apply Filters based on the request (Project or All Accessible) --- + if (projectId.HasValue) + { + // --- Logic for a SINGLE Project --- + + // 2a. Security Check: Verify permission for the specific project. + var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, projectId.Value); + return StatusCode(403, ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to view this project.", 403)); + } + + // 2b. Add project-specific filter to the base query. + // This is more efficient than fetching workAreaIds separately. + baseWorkItemQuery = baseWorkItemQuery + .Where(wi => wi.WorkArea != null && + wi.WorkArea.Floor != null && + wi.WorkArea.Floor.Building != null && + wi.WorkArea.Floor.Building.ProjectId == projectId.Value); + } + else + { + // --- Logic for ALL Accessible Projects --- + + // 2c. Get a list of all projects the user is allowed to see. + var accessibleProjectIds = await _projectServices.GetMyProjectIdsAsync(tenantId, loggedInEmployee); + + if (!accessibleProjectIds.Any()) + { + _logger.LogInfo("User {UserId} has no accessible projects.", loggedInEmployee.Id); + // Return a zeroed-out dashboard if the user has no projects. + return Ok(ApiResponse.SuccessResponse(new TasksDashboardVM(), "No accessible projects found.", 200)); + } + + // 2d. Add a filter to include all work items from all accessible projects. + baseWorkItemQuery = baseWorkItemQuery + .Where(wi => wi.WorkArea != null && + wi.WorkArea.Floor != null && + wi.WorkArea.Floor.Building != null && + accessibleProjectIds.Contains(wi.WorkArea.Floor.Building.ProjectId)); + } + + // --- Step 3: Execute the Aggregation Query ON THE DATABASE SERVER --- + // This is the most powerful optimization. The database does all the summing. + // EF Core translates this into a single, efficient SQL query like: + // SELECT SUM(PlannedWork), SUM(CompletedWork) FROM WorkItems WHERE ... + var tasksDashboardVM = await baseWorkItemQuery + .GroupBy(x => 1) // Group by a constant to aggregate all rows into one result. + .Select(g => new TasksDashboardVM + { + TotalTasks = g.Sum(wi => wi.PlannedWork), + CompletedTasks = g.Sum(wi => wi.CompletedWork) + }) + .FirstOrDefaultAsync(); // Use FirstOrDefaultAsync as GroupBy might return no rows. + + // If the query returned no work items, the result will be null. Default to a zeroed object. + tasksDashboardVM ??= new TasksDashboardVM(); + + _logger.LogInfo("Successfully fetched task dashboard for user {UserId}. Total: {TotalTasks}, Completed: {CompletedTasks}", + loggedInEmployee.Id, tasksDashboardVM.TotalTasks, tasksDashboardVM.CompletedTasks); + + return Ok(ApiResponse.SuccessResponse(tasksDashboardVM, "Dashboard data retrieved successfully.", 200)); + } + catch (Exception ex) + { + _logger.LogError(ex, "An unexpected error occurred in GetTotalTasks for projectId {ProjectId}", projectId ?? Guid.Empty); + return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred.", null, 500)); + } + } [HttpGet("pending-attendance")] public async Task GetPendingAttendance() { @@ -221,7 +367,7 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Number of pending regularization and pending check-out are fetched successfully for employee {EmployeeId}", LoggedInEmployee.Id); return Ok(ApiResponse.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.ErrorResponse("No attendance entry was found for this employee", "No attendance entry was found for this employee", 404)); } @@ -235,14 +381,14 @@ namespace Marco.Pms.Services.Controllers List? projectProgressionVMs = new List(); if (date != null && DateTime.TryParse(date, out currentDate) == false) { - _logger.LogError($"user send invalid date"); + _logger.LogWarning($"user send invalid date"); return BadRequest(ApiResponse.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.ErrorResponse("Project not found", "Project not found", 404)); } List? projectAllocation = await _context.ProjectAllocations.Where(p => p.ProjectId == projectId && p.IsActive && p.TenantId == tenantId).ToListAsync(); @@ -288,14 +434,14 @@ namespace Marco.Pms.Services.Controllers DateTime currentDate = DateTime.UtcNow; if (date != null && DateTime.TryParse(date, out currentDate) == false) { - _logger.LogError($"user send invalid date"); + _logger.LogWarning($"user send invalid date"); return BadRequest(ApiResponse.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.ErrorResponse("Project not found", "Project not found", 404)); } @@ -373,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) { diff --git a/Marco.Pms.Services/Controllers/DirectoryController.cs b/Marco.Pms.Services/Controllers/DirectoryController.cs index 4a0e41e..69cd9d0 100644 --- a/Marco.Pms.Services/Controllers/DirectoryController.cs +++ b/Marco.Pms.Services/Controllers/DirectoryController.cs @@ -33,20 +33,7 @@ namespace Marco.Pms.Services.Controllers CategoryIds = categoryIds }; var response = await _directoryHelper.GetListOfContacts(search, active, filterDto, projectId); - - - if (response.StatusCode == 200) - { - return Ok(response); - } - else if (response.StatusCode == 401) - { - return Unauthorized(response); - } - else - { - return BadRequest(response); - } + return StatusCode(response.StatusCode, response); } @@ -54,18 +41,7 @@ namespace Marco.Pms.Services.Controllers public async Task GetContactsListByBucketId(Guid bucketId) { var response = await _directoryHelper.GetContactsListByBucketId(bucketId); - if (response.StatusCode == 200) - { - return Ok(response); - } - else if (response.StatusCode == 401) - { - return Unauthorized(response); - } - else - { - return BadRequest(response); - } + return StatusCode(response.StatusCode, response); } [HttpPost] @@ -77,65 +53,38 @@ 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.ErrorResponse("Invalid data", errors, 400)); } var response = await _directoryHelper.CreateContact(createContact); - if (response.StatusCode == 200) - { - return Ok(response); - } - else - { - return BadRequest(response); - } + return StatusCode(response.StatusCode, response); } [HttpPut("{id}")] public async Task UpdateContact(Guid id, [FromBody] UpdateContactDto updateContact) { var response = await _directoryHelper.UpdateContact(id, updateContact); - if (response.StatusCode == 200) - { - return Ok(response); - } - else if (response.StatusCode == 404) - { - return NotFound(response); - } - else if (response.StatusCode == 401) - { - return Unauthorized(response); - } - else - { - return BadRequest(response); - } + return StatusCode(response.StatusCode, response); } [HttpGet("profile/{id}")] public async Task GetContactProfile(Guid id) { var response = await _directoryHelper.GetContactProfile(id); - if (response.StatusCode == 200) - { - return Ok(response); - } - else if (response.StatusCode == 404) - { - return NotFound(response); - } - else - { - return BadRequest(response); - } + return StatusCode(response.StatusCode, response); } [HttpGet("organization")] public async Task GetOrganizationList() { var response = await _directoryHelper.GetOrganizationList(); - return Ok(response); + return StatusCode(response.StatusCode, response); + } + [HttpGet("designations")] + public async Task GetDesignationList() + { + var response = await _directoryHelper.GetDesignationList(); + return StatusCode(response.StatusCode, response); } [HttpDelete("{id}")] @@ -256,7 +205,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.ErrorResponse("Invalid data", errors, 400)); } var response = await _directoryHelper.CreateBucket(bucketDto); diff --git a/Marco.Pms.Services/Controllers/EmployeeController.cs b/Marco.Pms.Services/Controllers/EmployeeController.cs index 698dd67..d5d7f3d 100644 --- a/Marco.Pms.Services/Controllers/EmployeeController.cs +++ b/Marco.Pms.Services/Controllers/EmployeeController.cs @@ -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,15 +38,13 @@ namespace MarcoBMS.Services.Controllers private readonly ILoggingService _logger; private readonly IHubContext _signalR; private readonly PermissionServices _permission; - private readonly ProjectsHelper _projectsHelper; - private readonly Guid ViewAllEmployees; - private readonly Guid ViewTeamMembers; + private readonly IProjectServices _projectServices; private readonly Guid tenantId; public EmployeeController(UserManager userManager, IEmailSender emailSender, ApplicationDbContext context, EmployeeHelper employeeHelper, UserHelper userHelper, IConfiguration configuration, ILoggingService logger, - IHubContext signalR, PermissionServices permission, ProjectsHelper projectsHelper) + IHubContext signalR, PermissionServices permission, IProjectServices projectServices) { _context = context; _userManager = userManager; @@ -56,9 +55,7 @@ namespace MarcoBMS.Services.Controllers _logger = logger; _signalR = signalR; _permission = permission; - ViewAllEmployees = Guid.Parse("60611762-7f8a-4fb5-b53f-b1139918796b"); - ViewTeamMembers = Guid.Parse("b82d2b7e-0d52-45f3-997b-c008ea460e7f"); - _projectsHelper = projectsHelper; + _projectServices = projectServices; tenantId = _userHelper.GetTenantId(); } @@ -123,11 +120,10 @@ namespace MarcoBMS.Services.Controllers loggedInEmployee.Id, projectid ?? Guid.Empty, ShowInactive); // Step 3: Fetch project access and permissions - List 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(ViewAllEmployees, loggedInEmployee.Id); - var hasViewTeamMembersPermission = await _permission.HasPermission(ViewTeamMembers, loggedInEmployee.Id); + var hasViewAllEmployeesPermission = await _permission.HasPermission(PermissionsMaster.ViewAllEmployees, loggedInEmployee.Id); + var hasViewTeamMembersPermission = await _permission.HasPermission(PermissionsMaster.ViewTeamMembers, loggedInEmployee.Id); List result = new(); @@ -387,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.ErrorResponse("Employee not found", "Employee not found", 404)); } byte[]? imageBytes = null; @@ -500,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.SuccessResponse(new { }, "Employee Suspended successfully", 200)); } diff --git a/Marco.Pms.Services/Controllers/ForumController.cs b/Marco.Pms.Services/Controllers/ForumController.cs index f50a077..fb6d0e7 100644 --- a/Marco.Pms.Services/Controllers/ForumController.cs +++ b/Marco.Pms.Services/Controllers/ForumController.cs @@ -44,10 +44,12 @@ 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.ErrorResponse("Invalid data", errors, 400)); } Guid tenantId = _userHelper.GetTenantId(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var batchId = Guid.NewGuid(); TicketForum ticketForum = createTicketDto.ToTicketForumFromCreateTicketDto(tenantId); _context.Tickets.Add(ticketForum); await _context.SaveChangesAsync(); @@ -64,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.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); } @@ -79,7 +81,7 @@ namespace Marco.Pms.Services.Controllers string objectKey = $"tenant-{tenantId}/project-{createTicketDto.LinkedProjectId}/froum/{fileName}"; await _s3Service.UploadFileAsync(base64, fileType, objectKey); - Document document = attachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, createTicketDto.CreatedAt, tenantId); + Document document = attachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, createTicketDto.CreatedAt, tenantId, batchId, loggedInEmployee.Id); _context.Documents.Add(document); await _context.SaveChangesAsync(); @@ -158,11 +160,19 @@ 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.ErrorResponse("Invalid data", errors, 400)); } Guid tenantId = _userHelper.GetTenantId(); - var existingTicket = await _context.Tickets.Include(t => t.TicketTypeMaster).Include(t => t.TicketStatusMaster).Include(t => t.Priority).AsNoTracking().FirstOrDefaultAsync(t => t.Id == updateTicketDto.Id); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var batchId = Guid.NewGuid(); + + var existingTicket = await _context.Tickets + .Include(t => t.TicketTypeMaster) + .Include(t => t.TicketStatusMaster) + .Include(t => t.Priority) + .AsNoTracking() + .FirstOrDefaultAsync(t => t.Id == updateTicketDto.Id); if (existingTicket != null) { TicketForum ticketForum = updateTicketDto.ToTicketForumFromUpdateTicketDto(existingTicket); @@ -187,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.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); } @@ -202,7 +212,7 @@ namespace Marco.Pms.Services.Controllers string objectKey = $"tenant-{tenantId}/project-{updateTicketDto.LinkedProjectId}/froum/{fileName}"; await _s3Service.UploadFileAsync(base64, fileType, objectKey); - Document document = attachmentDto.ToDocumentFromUpdateAttachmentDto(objectKey, objectKey, updateTicketDto.CreatedAt, tenantId); + Document document = attachmentDto.ToDocumentFromUpdateAttachmentDto(objectKey, objectKey, updateTicketDto.CreatedAt, tenantId, batchId, loggedInEmployee.Id); _context.Documents.Add(document); await _context.SaveChangesAsync(); @@ -326,7 +336,7 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Ticket {TicketId} updated", updateTicketDto.Id); return Ok(ApiResponse.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.ErrorResponse("Ticket not Found", "Ticket not Found", 404)); } @@ -339,11 +349,14 @@ 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.ErrorResponse("Invalid data", errors, 400)); } Guid tenantId = _userHelper.GetTenantId(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var batchId = Guid.NewGuid(); + List attachments = new List(); List documents = new List(); @@ -351,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.ErrorResponse("Ticket not Found", "Ticket not Found", 404)); } @@ -366,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.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); } @@ -381,7 +394,7 @@ namespace Marco.Pms.Services.Controllers string objectKey = $"tenant-{tenantId}/project-{ticket.LinkedProjectId}/froum/{fileName}"; await _s3Service.UploadFileAsync(base64, fileType, objectKey); - Document document = attachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, addCommentDto.SentAt, tenantId); + Document document = attachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, addCommentDto.SentAt, tenantId, batchId, loggedInEmployee.Id); _context.Documents.Add(document); await _context.SaveChangesAsync(); @@ -424,18 +437,21 @@ 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.ErrorResponse("Invalid data", errors, 400)); } Guid tenantId = _userHelper.GetTenantId(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var batchId = Guid.NewGuid(); + List attachments = new List(); TicketForum? ticket = await _context.Tickets.FirstOrDefaultAsync(t => t.Id == updateCommentDto.TicketId); 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.ErrorResponse("Ticket not Found", "Ticket not Found", 404)); } @@ -458,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.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); } @@ -473,7 +489,7 @@ namespace Marco.Pms.Services.Controllers string objectKey = $"tenant-{tenantId}/project-{ticket.LinkedProjectId}/froum/{fileName}"; await _s3Service.UploadFileAsync(base64, fileType, objectKey); - Document document = attachmentDto.ToDocumentFromUpdateAttachmentDto(objectKey, objectKey, existingComment.SentAt, tenantId); + Document document = attachmentDto.ToDocumentFromUpdateAttachmentDto(objectKey, objectKey, existingComment.SentAt, tenantId, batchId, loggedInEmployee.Id); _context.Documents.Add(document); await _context.SaveChangesAsync(); @@ -536,11 +552,14 @@ 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.ErrorResponse("Invalid data", errors, 400)); } Guid tenantId = _userHelper.GetTenantId(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var batchId = Guid.NewGuid(); + List ticketAttachmentVMs = new List(); List ticketIds = forumAttachmentDtos.Select(f => f.TicketId.HasValue ? f.TicketId.Value : Guid.Empty).ToList(); @@ -549,7 +568,7 @@ namespace Marco.Pms.Services.Controllers if (tickets == null || tickets.Count > 0) { - _logger.LogError("Tickets not Found in database"); + _logger.LogWarning("Tickets not Found in database"); return NotFound(ApiResponse.ErrorResponse("Ticket not Found", "Ticket not Found", 404)); } @@ -559,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.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.ErrorResponse("ticket ID is missing", "ticket ID is missing", 400)); } var ticket = tickets.FirstOrDefault(t => t.Id == forumAttachmentDto.TicketId); @@ -579,7 +598,7 @@ namespace Marco.Pms.Services.Controllers string objectKey = $"tenant-{tenantId}/project-{ticket?.LinkedProjectId}/froum/{fileName}"; await _s3Service.UploadFileAsync(base64, fileType, objectKey); - Document document = forumAttachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, forumAttachmentDto.SentAt, tenantId); + Document document = forumAttachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, forumAttachmentDto.SentAt, tenantId, batchId, loggedInEmployee.Id); _context.Documents.Add(document); await _context.SaveChangesAsync(); diff --git a/Marco.Pms.Services/Controllers/ImageController.cs b/Marco.Pms.Services/Controllers/ImageController.cs new file mode 100644 index 0000000..9014171 --- /dev/null +++ b/Marco.Pms.Services/Controllers/ImageController.cs @@ -0,0 +1,404 @@ +using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.Activities; +using Marco.Pms.Model.Dtos.DocumentManager; +using Marco.Pms.Model.Employees; +using Marco.Pms.Model.Mapper; +using Marco.Pms.Model.Projects; +using Marco.Pms.Model.Utilities; +using Marco.Pms.Services.Service; +using MarcoBMS.Services.Helpers; +using MarcoBMS.Services.Service; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.CodeAnalysis; +using Microsoft.EntityFrameworkCore; +using System.Text.Json; + +namespace Marco.Pms.Services.Controllers +{ + [Route("api/[controller]")] + [ApiController] + [Authorize] + public class ImageController : ControllerBase + { + private readonly ApplicationDbContext _context; + private readonly S3UploadService _s3Service; + private readonly UserHelper _userHelper; + private readonly ILoggingService _logger; + private readonly PermissionServices _permission; + private readonly Guid tenantId; + public ImageController(ApplicationDbContext context, S3UploadService s3Service, UserHelper userHelper, ILoggingService logger, PermissionServices permission) + { + _context = context; + _s3Service = s3Service; + _userHelper = userHelper; + _logger = logger; + tenantId = userHelper.GetTenantId(); + _permission = permission; + } + + [HttpGet("images/{projectId}")] + + public async Task GetImageList(Guid projectId, [FromQuery] string? filter, [FromQuery] int? pageNumber = 1, [FromQuery] int? pageSize = 10) + { + _logger.LogInfo("[GetImageList] Called by Employee for ProjectId: {ProjectId}", projectId); + + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // Step 1: Validate project existence + var isProjectExist = await _context.Projects.AnyAsync(p => p.Id == projectId && p.TenantId == tenantId); + if (!isProjectExist) + { + _logger.LogWarning("[GetImageList] ProjectId: {ProjectId} not found", projectId); + return BadRequest(ApiResponse.ErrorResponse("Project not found", "Project not found in database", 400)); + } + + // Step 2: Check project access permission + var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId); + if (!hasPermission) + { + _logger.LogWarning("[GetImageList] Access denied for EmployeeId: {EmployeeId} on ProjectId: {ProjectId}", loggedInEmployee.Id, projectId); + return StatusCode(403, ApiResponse.ErrorResponse("You don't have access", "You don't have access", 403)); + } + + // Step 3: Deserialize filter + ImageFilter? imageFilter = null; + if (!string.IsNullOrWhiteSpace(filter)) + { + try + { + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + //string unescapedJsonString = JsonSerializer.Deserialize(filter, options) ?? ""; + //imageFilter = JsonSerializer.Deserialize(unescapedJsonString, options); + imageFilter = JsonSerializer.Deserialize(filter, options); + } + catch (Exception ex) + { + _logger.LogWarning("[GetImageList] Failed to parse filter: {Message}", ex.Message); + } + } + + // Step 4: Extract filter values + var buildingIds = imageFilter?.BuildingIds; + var floorIds = imageFilter?.FloorIds; + var workAreaIds = imageFilter?.WorkAreaIds; + var activityIds = imageFilter?.ActivityIds; + var workCategoryIds = imageFilter?.WorkCategoryIds; + var startDate = imageFilter?.StartDate; + var endDate = imageFilter?.EndDate; + var uploadedByIds = imageFilter?.UploadedByIds; + + // Step 5: Fetch building > floor > area > work item hierarchy + List? buildings = null; + List? floors = null; + List? workAreas = null; + + if (buildingIds != null && buildingIds.Count > 0) + { + + buildings = await _context.Buildings + .Where(b => b.ProjectId == projectId && buildingIds.Contains(b.Id)) + .ToListAsync(); + } + else + { + buildings = await _context.Buildings + .Where(b => b.ProjectId == projectId) + .ToListAsync(); + + buildingIds = buildings.Select(b => b.Id).ToList(); + } + + if (floorIds != null && floorIds.Count > 0) + { + floors = await _context.Floor + .Where(f => buildingIds.Contains(f.BuildingId) && floorIds.Contains(f.Id)) + .ToListAsync(); + } + else + { + floors = await _context.Floor + .Where(f => buildingIds.Contains(f.BuildingId)) + .ToListAsync(); + + floorIds = floors.Select(f => f.Id).ToList(); + } + if (workAreaIds != null && workAreaIds.Count > 0) + { + workAreas = await _context.WorkAreas + .Where(wa => floorIds.Contains(wa.FloorId) && workAreaIds.Contains(wa.Id)) + .ToListAsync(); + } + else + { + workAreas = await _context.WorkAreas + .Where(wa => floorIds.Contains(wa.FloorId)) + .ToListAsync(); + + workAreaIds = workAreas.Select(wa => wa.Id).ToList(); + } + + var workItemsQuery = _context.WorkItems.Include(w => w.ActivityMaster).Include(w => w.WorkCategoryMaster) + .Where(wi => workAreaIds.Contains(wi.WorkAreaId)); + if (activityIds?.Any() == true) workItemsQuery = workItemsQuery.Where(wi => activityIds.Contains(wi.ActivityId)); + + if (workCategoryIds?.Any() == true) + { + workItemsQuery = workItemsQuery.Where(wi => wi.WorkCategoryMaster != null && workCategoryIds.Contains(wi.WorkCategoryMaster.Id)); + } + var workItems = await workItemsQuery.ToListAsync(); + var workItemIds = workItems.Select(wi => wi.Id).ToList(); + + // Step 6: Fetch task allocations and comments + var tasks = await _context.TaskAllocations.Include(t => t.ReportedBy) + .Where(t => workItemIds.Contains(t.WorkItemId)).ToListAsync(); + var taskIds = tasks.Select(t => t.Id).ToList(); + + var comments = await _context.TaskComments.Include(c => c.Employee) + .Where(c => taskIds.Contains(c.TaskAllocationId)).ToListAsync(); + var commentIds = comments.Select(c => c.Id).ToList(); + + var attachments = await _context.TaskAttachments + .Where(ta => taskIds.Contains(ta.ReferenceId) || commentIds.Contains(ta.ReferenceId)).ToListAsync(); + + var documentIds = attachments.Select(ta => ta.DocumentId).ToList(); + + // Step 7: Fetch and filter documents + List documents = new List(); + var docQuery = _context.Documents.Include(d => d.UploadedBy) + .Where(d => documentIds.Contains(d.Id) && d.TenantId == tenantId); + if (startDate != null && endDate != null) + { + docQuery = docQuery.Where(d => d.UploadedAt.Date >= startDate.Value.Date && d.UploadedAt.Date <= endDate.Value.Date); + } + if (pageNumber != null && pageSize != null) + { + documents = await docQuery + .GroupBy(d => d.BatchId) + .OrderByDescending(g => g.Max(d => d.UploadedAt)) + .Skip((pageNumber.Value - 1) * pageSize.Value) + .Take(pageSize.Value) + .Select(g => new DocumentBatchDto + { + BatchId = g.Key, + Documents = g.ToList() + }) + .ToListAsync(); + Console.Write("Pagenation Success"); + } + + + // Step 8: Build response + var documentVM = documents.Select(d => + { + var docIds = d.Documents?.Select(x => x.Id).ToList() ?? new List(); + var refId = attachments.FirstOrDefault(ta => docIds.Contains(ta.DocumentId))?.ReferenceId; + var task = tasks.FirstOrDefault(t => t.Id == refId); + var comment = comments.FirstOrDefault(c => c.Id == refId); + + var source = task != null ? "Report" : comment != null ? "Comment" : ""; + var uploadedBy = task?.ReportedBy ?? comment?.Employee; + + if (comment != null) + { + task = tasks.FirstOrDefault(t => t.Id == comment.TaskAllocationId); + } + if (task != null) + { + comment = comments.OrderBy(c => c.CommentDate).FirstOrDefault(c => c.TaskAllocationId == task.Id); + } + + var workItem = workItems.FirstOrDefault(w => w.Id == task?.WorkItemId); + var workArea = workAreas.FirstOrDefault(wa => wa.Id == workItem?.WorkAreaId); + var floor = floors.FirstOrDefault(f => f.Id == workArea?.FloorId); + var building = buildings.FirstOrDefault(b => b.Id == floor?.BuildingId); + + return new + { + + BatchId = d.BatchId, + Documents = d.Documents?.Select(x => new + { + Id = x.Id, + thumbnailUrl = x.ThumbS3Key != null ? _s3Service.GeneratePreSignedUrlAsync(x.ThumbS3Key) : (x.S3Key != null ? _s3Service.GeneratePreSignedUrlAsync(x.S3Key) : null), + Url = x.S3Key != null ? _s3Service.GeneratePreSignedUrlAsync(x.S3Key) : null, + UploadedBy = x.UploadedBy?.ToBasicEmployeeVMFromEmployee() ?? uploadedBy?.ToBasicEmployeeVMFromEmployee(), + UploadedAt = x.UploadedAt, + }).ToList(), + Source = source, + ProjectId = projectId, + BuildingId = building?.Id, + BuildingName = building?.Name, + FloorIds = floor?.Id, + FloorName = floor?.FloorName, + WorkAreaId = workArea?.Id, + WorkAreaName = workArea?.AreaName, + TaskId = task?.Id, + ActivityId = workItem?.ActivityMaster?.Id, + ActivityName = workItem?.ActivityMaster?.ActivityName, + WorkCategoryId = workItem?.WorkCategoryMaster?.Id, + WorkCategoryName = workItem?.WorkCategoryMaster?.Name, + CommentId = comment?.Id, + Comment = comment?.Comment + }; + }).ToList(); + + if (uploadedByIds?.Any() == true) + { + documentVM = documentVM.Where(d => d.Documents != null && d.Documents.Any(x => uploadedByIds.Contains(x.UploadedBy?.Id ?? Guid.Empty))).ToList(); + } + + _logger.LogInfo("[GetImageList] Fetched {Count} documents for ProjectId: {ProjectId}", documentVM.Count, projectId); + return Ok(ApiResponse.SuccessResponse(documentVM, $"{documentVM.Count} image records fetched successfully", 200)); + } + + [HttpGet("batch/{batchId}")] + public async Task GetImagesByBatch(Guid batchId) + { + _logger.LogInfo("GetImagesByBatch called for BatchId: {BatchId}", batchId); + + // Step 1: Get the logged-in employee + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // Step 2: Retrieve all documents in the batch + var documents = await _context.Documents + .Include(d => d.UploadedBy) + .Where(d => d.BatchId == batchId) + .ToListAsync(); + + if (!documents.Any()) + { + _logger.LogWarning("No documents found for BatchId: {BatchId}", batchId); + return NotFound(ApiResponse.ErrorResponse("No images found", "No images associated with this batch", 404)); + } + + var documentIds = documents.Select(d => d.Id).ToList(); + + // Step 3: Get task/comment reference IDs linked to these documents + var referenceIds = await _context.TaskAttachments + .Where(ta => documentIds.Contains(ta.DocumentId)) + .Select(ta => ta.ReferenceId) + .Distinct() + .ToListAsync(); + + // Step 4: Try to identify the source of the attachment (task or comment) + var task = await _context.TaskAllocations + .Include(t => t.ReportedBy) + .FirstOrDefaultAsync(t => referenceIds.Contains(t.Id)); + + TaskComment? comment = null; + WorkItem? workItem = null; + Employee? uploadedBy = null; + string source = ""; + + if (task != null) + { + uploadedBy = task.ReportedBy; + workItem = await _context.WorkItems + .Include(wi => wi.ActivityMaster) + .Include(wi => wi.WorkCategoryMaster) + .FirstOrDefaultAsync(wi => wi.Id == task.WorkItemId); + source = "Report"; + } + else + { + comment = await _context.TaskComments + .Include(tc => tc.TaskAllocation) + .Include(tc => tc.Employee) + .FirstOrDefaultAsync(tc => referenceIds.Contains(tc.Id)); + var workItemId = comment?.TaskAllocation?.WorkItemId; + + uploadedBy = comment?.Employee; + workItem = await _context.WorkItems + .Include(wi => wi.ActivityMaster) + .Include(wi => wi.WorkCategoryMaster) + .FirstOrDefaultAsync(wi => wi.Id == workItemId); + source = "Comment"; + } + + // Step 5: Traverse up to building level + var workAreaId = workItem?.WorkAreaId; + var workArea = await _context.WorkAreas + .Include(wa => wa.Floor) + .FirstOrDefaultAsync(wa => wa.Id == workAreaId); + + var buildingId = workArea?.Floor?.BuildingId; + var building = await _context.Buildings + .FirstOrDefaultAsync(b => b.Id == buildingId); + + // Step 6: Construct the response + var response = new + { + + BatchId = batchId, + Documents = documents?.Select(x => new + { + Id = x.Id, + thumbnailUrl = x.ThumbS3Key != null ? _s3Service.GeneratePreSignedUrlAsync(x.ThumbS3Key) : (x.S3Key != null ? _s3Service.GeneratePreSignedUrlAsync(x.S3Key) : null), + Url = x.S3Key != null ? _s3Service.GeneratePreSignedUrlAsync(x.S3Key) : null, + UploadedBy = x.UploadedBy?.ToBasicEmployeeVMFromEmployee() ?? uploadedBy?.ToBasicEmployeeVMFromEmployee(), + UploadedAt = x.UploadedAt, + }).ToList(), + Source = source, + ProjectId = building?.ProjectId, + BuildingId = building?.Id, + BuildingName = building?.Name, + FloorIds = workArea?.Floor?.Id, + FloorName = workArea?.Floor?.FloorName, + WorkAreaId = workArea?.Id, + WorkAreaName = workArea?.AreaName, + TaskId = task?.Id, + ActivityId = workItem?.ActivityMaster?.Id, + ActivityName = workItem?.ActivityMaster?.ActivityName, + WorkCategoryId = workItem?.WorkCategoryMaster?.Id, + WorkCategoryName = workItem?.WorkCategoryMaster?.Name, + CommentId = comment?.Id, + Comment = comment?.Comment + }; + + _logger.LogInfo("Fetched {Count} image(s) for BatchId: {BatchId}", response.Documents?.Count ?? 0, batchId); + + return Ok(ApiResponse.SuccessResponse(response, "Images for provided batchId fetched successfully", 200)); + } + + [HttpGet("{documentId}")] + public async Task GetImage(Guid documentId) + { + // Log the start of the image fetch process + _logger.LogInfo("GetImage called for DocumentId: {DocumentId}", documentId); + + // Step 1: Get the currently logged-in employee (for future use like permission checks or auditing) + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // Step 2: Fetch the document from the database based on the provided ID + var document = await _context.Documents.FirstOrDefaultAsync(d => d.Id == documentId); + + // Step 3: If document doesn't exist, return a 400 Bad Request response + if (document == null) + { + _logger.LogWarning("Document not found for DocumentId: {DocumentId}", documentId); + return BadRequest(ApiResponse.ErrorResponse("Document not found", "Document not found", 400)); + } + + // Step 4: Generate pre-signed URLs for thumbnail and full image (if keys exist) + string? thumbnailUrl = document.ThumbS3Key != null + ? _s3Service.GeneratePreSignedUrlAsync(document.ThumbS3Key) + : null; + + string? imageUrl = document.S3Key != null + ? _s3Service.GeneratePreSignedUrlAsync(document.S3Key) + : null; + + // Step 5: Prepare the response object + var response = new + { + ThumbnailUrl = thumbnailUrl, + ImageUrl = imageUrl + }; + + // Step 6: Log successful fetch and return the result + _logger.LogInfo("Image fetched successfully for DocumentId: {DocumentId}", documentId); + return Ok(ApiResponse.SuccessResponse(response, "Image fetched successfully", 200)); + } + } +} diff --git a/Marco.Pms.Services/Controllers/MasterController.cs b/Marco.Pms.Services/Controllers/MasterController.cs index 85b5f14..cdc193e 100644 --- a/Marco.Pms.Services/Controllers/MasterController.cs +++ b/Marco.Pms.Services/Controllers/MasterController.cs @@ -169,7 +169,7 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Ticket Status master {TicketStatusId} added successfully from tenant {tenantId}", statusMaster.Id, tenantId); return Ok(ApiResponse.SuccessResponse(statusVM, "Ticket Status master added successfully", 200)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("Sent Empty payload", "Sent Empty payload", 400)); } @@ -190,10 +190,10 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Ticket Status master {TicketStatusId} updated successfully from tenant {tenantId}", statusMaster.Id, tenantId); return Ok(ApiResponse.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.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.ErrorResponse("Sent Empty payload", "Sent Empty payload", 400)); } @@ -220,7 +220,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.ErrorResponse("Ticket Status not found", "Ticket Status not found", 404)); } } @@ -257,7 +257,7 @@ namespace Marco.Pms.Services.Controllers return Ok(ApiResponse.SuccessResponse(typeVM, "Ticket type master added successfully", 200)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); } @@ -278,10 +278,10 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Ticket Type master {TicketTypeId} updated successfully from tenant {tenantId}", typeMaster.Id, tenantId); return Ok(ApiResponse.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.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.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); } @@ -308,7 +308,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.ErrorResponse("Ticket Type not found", "Ticket Type not found", 404)); } } @@ -346,7 +346,7 @@ namespace Marco.Pms.Services.Controllers return Ok(ApiResponse.SuccessResponse(typeVM, "Ticket Priority master added successfully", 200)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); } [HttpPost("ticket-priorities/edit/{id}")] @@ -366,10 +366,10 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Ticket Priority master {TicketPriorityId} updated successfully from tenant {tenantId}", typeMaster.Id, tenantId); return Ok(ApiResponse.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.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.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); } @@ -396,7 +396,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.ErrorResponse("Ticket Priority not found", "Ticket Priority not found", 404)); } } @@ -433,7 +433,7 @@ namespace Marco.Pms.Services.Controllers return Ok(ApiResponse.SuccessResponse(typeVM, "Ticket tag master added successfully", 200)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); } @@ -454,10 +454,10 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Ticket Tag master {TicketTypeId} updated successfully from tenant {tenantId}", tagMaster.Id, tenantId); return Ok(ApiResponse.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.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.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); } @@ -484,7 +484,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.ErrorResponse("Ticket tag not found", "Ticket tag not found", 404)); } } @@ -548,7 +548,7 @@ namespace Marco.Pms.Services.Controllers return Ok(ApiResponse.SuccessResponse(workCategoryMasterVM, "Work category master added successfully", 200)); } - _logger.LogError("User sent empyt payload"); + _logger.LogWarning("User sent empyt payload"); return BadRequest(ApiResponse.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); } @@ -563,7 +563,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.ErrorResponse("Cannot update system-defined work", "Cannot update system-defined work", 400)); } workCategory = workCategoryMasterDto.ToWorkCategoryMasterFromWorkCategoryMasterDto(tenantId); @@ -574,10 +574,10 @@ namespace Marco.Pms.Services.Controllers _logger.LogInfo("Work category master {WorkCategoryId} updated successfully from tenant {tenantId}", workCategory.Id, tenantId); return Ok(ApiResponse.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.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.ErrorResponse("User sent Empty payload", "User sent Empty payload", 400)); } @@ -605,7 +605,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.ErrorResponse("Work category not found", "Work category not found", 404)); } } @@ -628,7 +628,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.ErrorResponse("Invalid data", errors, 400)); } var response = await _masterHelper.CreateWorkStatus(createWorkStatusDto); @@ -742,7 +742,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.ErrorResponse("Invalid data", errors, 400)); } var response = await _masterHelper.CreateContactTag(contactTagDto); diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 6b83a6c..796fd39 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -1,20 +1,13 @@ -using Marco.Pms.DataAccess.Data; -using Marco.Pms.Model.Activities; -using Marco.Pms.Model.Dtos.Project; +using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Employees; -using Marco.Pms.Model.Entitlements; -using Marco.Pms.Model.Mapper; -using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; -using Marco.Pms.Model.ViewModels.Employee; -using Marco.Pms.Model.ViewModels.Projects; -using Marco.Pms.Services.Hubs; +using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.SignalR; -using Microsoft.EntityFrameworkCore; +using Microsoft.CodeAnalysis; +using MongoDB.Driver; namespace MarcoBMS.Services.Controllers { @@ -23,159 +16,110 @@ namespace MarcoBMS.Services.Controllers [Authorize] public class ProjectController : ControllerBase { - private readonly ApplicationDbContext _context; + private readonly IProjectServices _projectServices; private readonly UserHelper _userHelper; private readonly ILoggingService _logger; - private readonly RolesHelper _rolesHelper; - private readonly ProjectsHelper _projectsHelper; - private readonly IHubContext _signalR; + private readonly ISignalRService _signalR; + private readonly Guid tenantId; - public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, ProjectsHelper projectHelper, IHubContext signalR) + public ProjectController( + UserHelper userHelper, + ILoggingService logger, + ISignalRService signalR, + IProjectServices projectServices) { - _context = context; _userHelper = userHelper; _logger = logger; - _rolesHelper = rolesHelper; - _projectsHelper = projectHelper; _signalR = signalR; - + _projectServices = projectServices; + tenantId = userHelper.GetTenantId(); } + #region =================================================================== Project Get APIs =================================================================== + [HttpGet("list/basic")] - public async Task GetAllProjects() + public async Task GetAllProjectsBasic() { - if (!ModelState.IsValid) - { - var errors = ModelState.Values - .SelectMany(v => v.Errors) - .Select(e => e.ErrorMessage) - .ToList(); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); - - } - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - // Defensive check for null employee (important for robust APIs) - if (LoggedInEmployee == null) - { - return Unauthorized(ApiResponse.ErrorResponse("Employee not found.", null, 401)); - } - - - List projects = await _projectsHelper.GetMyProjects(tenantId, LoggedInEmployee); - - - // 4. Project projection to ProjectInfoVM - // This part is already quite efficient. - // Ensure ToProjectInfoVMFromProject() is also optimized and doesn't perform N+1 queries. - // If ProjectInfoVM only needs a subset of Project properties, - // you can use a LINQ Select directly on the IQueryable before ToListAsync() - // to fetch only the required columns from the database. - List response = projects - .Select(project => project.ToProjectInfoVMFromProject()) - .ToList(); - - - //List response = new List(); - - //foreach (var project in projects) - //{ - // response.Add(project.ToProjectInfoVMFromProject()); - //} - - return Ok(ApiResponse.SuccessResponse(response, "Success.", 200)); + // Get the current user + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetAllProjectsBasicAsync(tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } + /// + /// Retrieves a list of projects accessible to the current user, including aggregated details. + /// This method is optimized to use a cache-first approach. If data is not in the cache, + /// it fetches and aggregates data efficiently from the database in parallel. + /// + /// An ApiResponse containing a list of projects or an error. + [HttpGet("list")] - public async Task GetAll() + public async Task GetAllProjects() { + // --- Input Validation and Initial Setup --- if (!ModelState.IsValid) { var errors = ModelState.Values .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); - + _logger.LogWarning("GetAllProjects called with invalid model state. Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - //List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(LoggedInEmployee.Id); - //string[] projectsId = []; - //List projects = new List(); - - ///* User with permission manage project can see all projects */ - //if (featurePermission != null && featurePermission.Exists(c => c.Id.ToString() == "172fc9b6-755b-4f62-ab26-55c34a330614")) - //{ - // projects = await _projectsHelper.GetAllProjectByTanentID(LoggedInEmployee.TenantId); - //} - //else - //{ - // List allocation = await _projectsHelper.GetProjectByEmployeeID(LoggedInEmployee.Id); - // projectsId = allocation.Select(c => c.ProjectId.ToString()).ToArray(); - // projects = await _context.Projects.Where(c => projectsId.Contains(c.Id.ToString()) && c.TenantId == tenantId).ToListAsync(); - //} - - List projects = await _projectsHelper.GetMyProjects(tenantId, LoggedInEmployee); - - - - - List response = new List(); - foreach (var project in projects) - { - var result = project.ToProjectListVMFromProject(); - var team = await _context.ProjectAllocations.Where(p => p.TenantId == tenantId && p.ProjectId == project.Id && p.IsActive == true).ToListAsync(); - - result.TeamSize = team.Count(); - - List buildings = await _context.Buildings.Where(b => b.ProjectId == project.Id && b.TenantId == tenantId).ToListAsync(); - List idList = buildings.Select(b => b.Id).ToList(); - - List floors = await _context.Floor.Where(f => idList.Contains(f.BuildingId) && f.TenantId == tenantId).ToListAsync(); - idList = floors.Select(f => f.Id).ToList(); - - List workAreas = await _context.WorkAreas.Where(a => idList.Contains(a.FloorId) && a.TenantId == tenantId).ToListAsync(); - idList = workAreas.Select(a => a.Id).ToList(); - - List workItems = await _context.WorkItems.Where(i => idList.Contains(i.WorkAreaId) && i.TenantId == tenantId).Include(i => i.ActivityMaster).ToListAsync(); - double completedTask = 0; - double plannedTask = 0; - foreach (var workItem in workItems) - { - completedTask += workItem.CompletedWork; - plannedTask += workItem.PlannedWork; - } - result.PlannedWork = plannedTask; - result.CompletedWork = completedTask; - response.Add(result); - } - - return Ok(ApiResponse.SuccessResponse(response, "Success.", 200)); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetAllProjectsAsync(tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } + /// + /// Retrieves details for a specific project by its ID. + /// This endpoint is optimized with a cache-first strategy and parallel permission checks. + /// + /// The unique identifier of the project. + /// An ApiResponse containing the project details or an appropriate error. + [HttpGet("get/{id}")] - public async Task Get([FromRoute] Guid id) + public async Task GetProject([FromRoute] Guid id) { + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) + { + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("Get project called with invalid model state for ID {ProjectId}. Errors: {Errors}", id, string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); + } + + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetProjectAsync(id, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); + } + + + [HttpGet("details/{id}")] + public async Task GetProjectDetails([FromRoute] Guid id) + { + // Step 1: Validate model state if (!ModelState.IsValid) { var errors = ModelState.Values .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); + _logger.LogWarning("Invalid model state in Details endpoint. Errors: {@Errors}", errors); + return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } - var project = await _context.Projects.Where(c => c.TenantId == _userHelper.GetTenantId() && c.Id == id).SingleOrDefaultAsync(); - if (project == null) return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); - return Ok(ApiResponse.SuccessResponse(project, "Success.", 200)); + // Step 2: Get logged-in employee + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + var response = await _projectServices.GetProjectDetailsAsync(id, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } - [HttpGet("details/{id}")] - public async Task Details([FromRoute] Guid id) + [HttpGet("details-old/{id}")] + public async Task GetProjectDetailsOld([FromRoute] Guid id) { // ProjectDetailsVM vm = new ProjectDetailsVM(); @@ -189,769 +133,290 @@ namespace MarcoBMS.Services.Controllers } - var project = await _context.Projects.Where(c => c.TenantId == _userHelper.GetTenantId() && c.Id == id).Include(c => c.ProjectStatus).SingleOrDefaultAsync(); // includeProperties: "ProjectStatus,Tenant"); //_context.Stock.FindAsync(id); - - if (project == null) - { - return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); - - } - else - { - //var project = projects.Where(c => c.Id == id).SingleOrDefault(); - ProjectDetailsVM vm = await GetProjectViewModel(id, project); - - ProjectVM projectVM = new ProjectVM(); - if (vm.project != null) - { - projectVM.Id = vm.project.Id; - projectVM.Name = vm.project.Name; - projectVM.ShortName = vm.project.ShortName; - projectVM.ProjectAddress = vm.project.ProjectAddress; - projectVM.ContactPerson = vm.project.ContactPerson; - projectVM.StartDate = vm.project.StartDate; - projectVM.EndDate = vm.project.EndDate; - projectVM.ProjectStatusId = vm.project.ProjectStatusId; - } - projectVM.Buildings = new List(); - if (vm.buildings != null) - { - foreach (Building build in vm.buildings) - { - BuildingVM buildVM = new BuildingVM() { Id = build.Id, Description = build.Description, Name = build.Name }; - buildVM.Floors = new List(); - if (vm.floors != null) - { - foreach (Floor floorDto in vm.floors.Where(c => c.BuildingId == build.Id).ToList()) - { - FloorsVM floorVM = new FloorsVM() { FloorName = floorDto.FloorName, Id = floorDto.Id }; - floorVM.WorkAreas = new List(); - - if (vm.workAreas != null) - { - foreach (WorkArea workAreaDto in vm.workAreas.Where(c => c.FloorId == floorVM.Id).ToList()) - { - WorkAreaVM workAreaVM = new WorkAreaVM() { Id = workAreaDto.Id, AreaName = workAreaDto.AreaName, WorkItems = new List() }; - - if (vm.workItems != null) - { - foreach (WorkItem workItemDto in vm.workItems.Where(c => c.WorkAreaId == workAreaDto.Id).ToList()) - { - WorkItemVM workItemVM = new WorkItemVM() { WorkItemId = workItemDto.Id, WorkItem = workItemDto }; - - workItemVM.WorkItem.WorkArea = new WorkArea(); - - if (workItemVM.WorkItem.ActivityMaster != null) - { - workItemVM.WorkItem.ActivityMaster.Tenant = new Tenant(); - } - workItemVM.WorkItem.Tenant = new Tenant(); - - double todaysAssigned = 0; - if (vm.Tasks != null) - { - var tasks = vm.Tasks.Where(t => t.WorkItemId == workItemDto.Id).ToList(); - foreach (TaskAllocation task in tasks) - { - todaysAssigned += task.PlannedTask; - } - } - workItemVM.TodaysAssigned = todaysAssigned; - - workAreaVM.WorkItems.Add(workItemVM); - } - } - - floorVM.WorkAreas.Add(workAreaVM); - } - } - - buildVM.Floors.Add(floorVM); - } - } - projectVM.Buildings.Add(buildVM); - } - } - return Ok(ApiResponse.SuccessResponse(projectVM, "Success.", 200)); - } + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetProjectDetailsAsync(id, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } - private async Task GetProjectViewModel(Guid? id, Project project) - { - ProjectDetailsVM vm = new ProjectDetailsVM(); + #endregion - // List buildings = _unitOfWork.Building.GetAll(c => c.ProjectId == id).ToList(); - List buildings = await _context.Buildings.Where(c => c.ProjectId == id).ToListAsync(); - List idList = buildings.Select(o => o.Id).ToList(); - // List floors = _unitOfWork.Floor.GetAll(c => idList.Contains(c.Id)).ToList(); - List floors = await _context.Floor.Where(c => idList.Contains(c.BuildingId)).ToListAsync(); - idList = floors.Select(o => o.Id).ToList(); - //List workAreas = _unitOfWork.WorkArea.GetAll(c => idList.Contains(c.Id), includeProperties: "WorkItems,WorkItems.ActivityMaster").ToList(); - - List workAreas = await _context.WorkAreas.Where(c => idList.Contains(c.FloorId)).ToListAsync(); - - idList = workAreas.Select(o => o.Id).ToList(); - List workItems = await _context.WorkItems.Include(c => c.WorkCategoryMaster).Where(c => idList.Contains(c.WorkAreaId)).Include(c => c.ActivityMaster).ToListAsync(); - // List workItems = _unitOfWork.WorkItem.GetAll(c => idList.Contains(c.WorkAreaId), includeProperties: "ActivityMaster").ToList(); - idList = workItems.Select(t => t.Id).ToList(); - List tasks = await _context.TaskAllocations.Where(t => idList.Contains(t.WorkItemId) && t.AssignmentDate.Date == DateTime.UtcNow.Date).ToListAsync(); - vm.project = project; - vm.buildings = buildings; - vm.floors = floors; - vm.workAreas = workAreas; - vm.workItems = workItems; - vm.Tasks = tasks; - return vm; - } - - private Guid GetTenantId() - { - return _userHelper.GetTenantId(); - //var tenant = User.FindFirst("TenantId")?.Value; - //return (tenant != null ? Convert.ToInt32(tenant) : 1); - } + #region =================================================================== Project Manage APIs =================================================================== [HttpPost] - public async Task Create([FromBody] CreateProjectDto projectDto) + public async Task CreateProject([FromBody] CreateProjectDto projectDto) { - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + // 1. Validate input first (early exit) if (!ModelState.IsValid) { - var errors = ModelState.Values - .SelectMany(v => v.Errors) - .Select(e => e.ErrorMessage) - .ToList(); + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); - } - Guid TenantId = GetTenantId(); - var project = projectDto.ToProjectFromCreateProjectDto(TenantId); - - _context.Projects.Add(project); - - await _context.SaveChangesAsync(); - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Create_Project", Response = project.ToProjectDto() }; - - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); - - return Ok(ApiResponse.SuccessResponse(project.ToProjectDto(), "Success.", 200)); + // 2. Prepare data without I/O + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.CreateProjectAsync(projectDto, tenantId, loggedInEmployee); + if (response.Success) + { + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Create_Project", Response = response.Data }; + await _signalR.SendNotificationAsync(notification); + } + return StatusCode(response.StatusCode, response); } - [HttpPut] - [Route("update/{id}")] - public async Task Update([FromRoute] Guid id, [FromBody] UpdateProjectDto updateProjectDto) + /// + /// Updates an existing project's details. + /// This endpoint is secure, handles concurrency, and performs non-essential tasks in the background. + /// + /// The ID of the project to update. + /// The data to update the project with. + /// An ApiResponse confirming the update or an appropriate error. + + [HttpPut("update/{id}")] + public async Task UpdateProject([FromRoute] Guid id, [FromBody] UpdateProjectDto updateProjectDto) { - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + // --- Step 1: Input Validation --- if (!ModelState.IsValid) { - var errors = ModelState.Values - .SelectMany(v => v.Errors) - .Select(e => e.ErrorMessage) - .ToList(); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); - + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("Update project called with invalid model state for ID {ProjectId}. Errors: {Errors}", id, string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - try + + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.UpdateProjectAsync(id, updateProjectDto, tenantId, loggedInEmployee); + if (response.Success) { - Guid TenantId = GetTenantId(); - - Project project = updateProjectDto.ToProjectFromUpdateProjectDto(TenantId, id); - _context.Projects.Update(project); - - await _context.SaveChangesAsync(); - - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Update_Project", Response = project.ToProjectDto() }; - - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); - - return Ok(ApiResponse.SuccessResponse(project.ToProjectDto(), "Success.", 200)); - - } - catch (Exception ex) - { - return BadRequest(ApiResponse.ErrorResponse(ex.Message, ex, 400)); + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Update_Project", Response = response.Data }; + await _signalR.SendNotificationAsync(notification); } + return StatusCode(response.StatusCode, response); } - //[HttpPost("assign-employee")] - //public async Task AssignEmployee(int? allocationid, int employeeId, int projectId) - //{ - // var employee = await _context.Employees.FindAsync(employeeId); - // var project = _projectrepo.Get(c => c.Id == projectId); - // if (employee == null || project == null) - // { - // return NotFound(); - // } + #endregion - // // Logic to add the product to a new table (e.g., selected products) + #region =================================================================== Project Allocation APIs =================================================================== - // if (allocationid == null) - // { - // // Add allocation - // ProjectAllocation allocation = new ProjectAllocation() - // { - // EmployeeId = employeeId, - // ProjectId = project.Id, - // AllocationDate = DateTime.UtcNow, - // //EmployeeRole = employee.Rol - // TenantId = project.TenantId - // }; - - // _unitOfWork.ProjectAllocation.CreateAsync(allocation); - // } - // else - // { - // //remove allocation - // var allocation = await _context.ProjectAllocations.FindAsync(allocationid); - // if (allocation != null) - // { - // allocation.ReAllocationDate = DateTime.UtcNow; - - // _unitOfWork.ProjectAllocation.UpdateAsync(allocation.Id, allocation); - // } - // else - // { - // return NotFound(); - // } - // } - - // return Ok(); - //} - - [HttpGet] - [Route("employees/get/{projectid?}/{includeInactive?}")] - public async Task GetEmployeeByProjectID(Guid? projectid, bool includeInactive = false) + [HttpGet("employees/get/{projectid?}/{includeInactive?}")] + public async Task GetEmployeeByProjectId(Guid? projectId, bool includeInactive = false) { + // --- Step 1: Input Validation --- if (!ModelState.IsValid) { - var errors = ModelState.Values - .SelectMany(v => v.Errors) - .Select(e => e.ErrorMessage) - .ToList(); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); - - } - Guid TenantId = GetTenantId(); - - if (projectid != null) - { - // Fetch assigned project - List result = new List(); - - if ((bool)includeInactive) - { - - result = await (from rpm in _context.Employees.Include(c => c.JobRole) - join fp in _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == projectid) - on rpm.Id equals fp.EmployeeId - select rpm).ToListAsync(); - } - else - { - result = await (from rpm in _context.Employees.Include(c => c.JobRole) - join fp in _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == projectid && c.IsActive == true) - on rpm.Id equals fp.EmployeeId - select rpm).ToListAsync(); - } - - List resultVM = new List(); - foreach (Employee employee in result) - { - EmployeeVM vm = employee.ToEmployeeVMFromEmployee(); - resultVM.Add(vm); - } - - return Ok(ApiResponse.SuccessResponse(resultVM, "Success.", 200)); - } - else - { - return NotFound(ApiResponse.ErrorResponse("Invalid Input Parameter", 404)); + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("Get employee list by ProjectId called with invalid model state \n Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetEmployeeByProjectIdAsync(projectId, includeInactive, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } - [HttpGet] - [Route("allocation/{projectId}")] + [HttpGet("allocation/{projectId}")] public async Task GetProjectAllocation(Guid? projectId) { + // --- Step 1: Input Validation --- if (!ModelState.IsValid) { - var errors = ModelState.Values - .SelectMany(v => v.Errors) - .Select(e => e.ErrorMessage) - .ToList(); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); - + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("Get employee list by ProjectId called with invalid model state \n Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - Guid TenantId = GetTenantId(); - - var employees = await _context.ProjectAllocations - .Where(c => c.TenantId == TenantId && c.ProjectId == projectId && c.Employee != null) - .Include(e => e.Employee) - .Select(e => new - { - ID = e.Id, - EmployeeId = e.EmployeeId, - ProjectId = e.ProjectId, - AllocationDate = e.AllocationDate, - ReAllocationDate = e.ReAllocationDate, - FirstName = e.Employee != null ? e.Employee.FirstName : string.Empty, - LastName = e.Employee != null ? e.Employee.LastName : string.Empty, - MiddleName = e.Employee != null ? e.Employee.MiddleName : string.Empty, - IsActive = e.IsActive, - JobRoleId = (e.JobRoleId != null ? e.JobRoleId : e.Employee != null ? e.Employee.JobRoleId : null) - }).ToListAsync(); - - return Ok(ApiResponse.SuccessResponse(employees, "Success.", 200)); + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetProjectAllocationAsync(projectId, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } [HttpPost("allocation")] - public async Task ManageAllocation(List projectAllocationDot) + public async Task ManageAllocation([FromBody] List projectAllocationDot) { - if (projectAllocationDot != null) + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) { - Guid TenentID = GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - List? result = new List(); - List employeeIds = new List(); - List projectIds = new List(); - - foreach (var item in projectAllocationDot) - { - try - { - ProjectAllocation projectAllocation = item.ToProjectAllocationFromProjectAllocationDto(TenentID); - ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == projectAllocation.EmployeeId - && c.ProjectId == projectAllocation.ProjectId - && c.ReAllocationDate == null - && c.TenantId == TenentID).SingleOrDefaultAsync(); - - if (projectAllocationFromDb != null) - { - _context.ProjectAllocations.Attach(projectAllocationFromDb); - - if (item.Status) - { - projectAllocationFromDb.JobRoleId = projectAllocation.JobRoleId; ; - projectAllocationFromDb.IsActive = true; - _context.Entry(projectAllocationFromDb).Property(e => e.JobRoleId).IsModified = true; - _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; - } - else - { - projectAllocationFromDb.ReAllocationDate = DateTime.Now; - projectAllocationFromDb.IsActive = false; - _context.Entry(projectAllocationFromDb).Property(e => e.ReAllocationDate).IsModified = true; - _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; - - employeeIds.Add(projectAllocation.EmployeeId); - projectIds.Add(projectAllocation.ProjectId); - } - await _context.SaveChangesAsync(); - var result1 = new - { - Id = projectAllocationFromDb.Id, - EmployeeId = projectAllocation.EmployeeId, - JobRoleId = projectAllocation.JobRoleId, - IsActive = projectAllocation.IsActive, - ProjectId = projectAllocation.ProjectId, - AllocationDate = projectAllocation.AllocationDate, - ReAllocationDate = projectAllocation.ReAllocationDate, - TenantId = projectAllocation.TenantId - }; - result.Add(result1); - } - else - { - projectAllocation.AllocationDate = DateTime.Now; - projectAllocation.IsActive = true; - _context.ProjectAllocations.Add(projectAllocation); - await _context.SaveChangesAsync(); - - employeeIds.Add(projectAllocation.EmployeeId); - projectIds.Add(projectAllocation.ProjectId); - } - - } - catch (Exception ex) - { - return Ok(ApiResponse.ErrorResponse(ex.Message, ex, 400)); - } - } - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeList = employeeIds }; - - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); - return Ok(ApiResponse.SuccessResponse(result, "Data saved successfully", 200)); - - } - return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Work Item Details are not valid.", 400)); - - } - - [HttpPost("task")] - public async Task CreateProjectTask(List workItemDtos) - { - _logger.LogInfo("CreateProjectTask called with {Count} items", workItemDtos?.Count ?? 0); - - // Validate request - if (workItemDtos == null || !workItemDtos.Any()) - { - _logger.LogWarning("No work items provided in the request."); - return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Work Item details are not valid.", 400)); + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - Guid tenantId = GetTenantId(); - var workItemsToCreate = new List(); - var workItemsToUpdate = new List(); - var responseList = new List(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - string message = ""; - List projectIds = new List(); - - foreach (var itemDto in workItemDtos) + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.ManageAllocationAsync(projectAllocationDot, tenantId, loggedInEmployee); + if (response.Success) { - var workItem = itemDto.ToWorkItemFromWorkItemDto(tenantId); - var workArea = await _context.WorkAreas.Include(a => a.Floor).FirstOrDefaultAsync(a => a.Id == workItem.WorkAreaId) ?? new WorkArea(); + List employeeIds = response.Data.Select(pa => pa.EmployeeId).ToList(); + List projectIds = response.Data.Select(pa => pa.ProjectId).ToList(); - Building building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == (workArea.Floor != null ? workArea.Floor.BuildingId : Guid.Empty)) ?? new Building(); - - if (itemDto.Id != null && itemDto.Id != Guid.Empty) - { - // Update existing - workItemsToUpdate.Add(workItem); - message = $"Task Updated in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}"; - } - else - { - // Create new - workItem.Id = Guid.NewGuid(); - workItemsToCreate.Add(workItem); - message = $"Task Added in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}"; - } - - responseList.Add(new WorkItemVM - { - WorkItemId = workItem.Id, - WorkItem = workItem - }); - projectIds.Add(building.ProjectId); + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeList = employeeIds }; + await _signalR.SendNotificationAsync(notification); } - string responseMessage = ""; - // Apply DB changes - if (workItemsToCreate.Any()) - { - _logger.LogInfo("Adding {Count} new work items", workItemsToCreate.Count); - await _context.WorkItems.AddRangeAsync(workItemsToCreate); - responseMessage = "Task Added Successfully"; - - } - - if (workItemsToUpdate.Any()) - { - _logger.LogInfo("Updating {Count} existing work items", workItemsToUpdate.Count); - _context.WorkItems.UpdateRange(workItemsToUpdate); - responseMessage = "Task Updated Successfully"; - - } - - await _context.SaveChangesAsync(); - - _logger.LogInfo("CreateProjectTask completed successfully. Created: {Created}, Updated: {Updated}", workItemsToCreate.Count, workItemsToUpdate.Count); - - - - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = message }; - - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); - - return Ok(ApiResponse.SuccessResponse(responseList, responseMessage, 200)); - } - - [HttpDelete("task/{id}")] - public async Task DeleteProjectTask(Guid id) - { - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - List projectIds = new List(); - WorkItem? task = await _context.WorkItems.AsNoTracking().Include(t => t.WorkArea).FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId); - if (task != null) - { - if (task.CompletedWork == 0) - { - var assignedTask = await _context.TaskAllocations.Where(t => t.WorkItemId == id).ToListAsync(); - if (assignedTask.Count == 0) - { - _context.WorkItems.Remove(task); - await _context.SaveChangesAsync(); - _logger.LogInfo("Task with ID {WorkItemId} has been successfully deleted.", task.Id); - - var floorId = task.WorkArea?.FloorId; - var floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == floorId); - - - projectIds.Add(floor?.Building?.ProjectId ?? Guid.Empty); - - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = $"Task Deleted in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}, in Area: {task.WorkArea?.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}" }; - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); - } - else - { - _logger.LogWarning("Task with ID {WorkItemId} is currently assigned and cannot be deleted.", task.Id); - return BadRequest(ApiResponse.ErrorResponse("Task is currently assigned and cannot be deleted.", "Task is currently assigned and cannot be deleted.", 400)); - } - } - else - { - double percentage = (task.CompletedWork / task.PlannedWork) * 100; - percentage = Math.Round(percentage, 2); - _logger.LogWarning("Task with ID {WorkItemId} is {CompletionPercentage}% complete and cannot be deleted", task.Id, percentage); - return BadRequest(ApiResponse.ErrorResponse(System.String.Format("Task is {0}% complete and cannot be deleted", percentage), System.String.Format("Task is {0}% complete and cannot be deleted", percentage), 400)); - - } - } - else - { - _logger.LogError("Task with ID {WorkItemId} not found ID database", id); - } - return Ok(ApiResponse.SuccessResponse(new { }, "Task deleted successfully", 200)); - } - - [HttpPost("manage-infra")] - public async Task ManageProjectInfra(List infraDots) - { - Guid tenantId = GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - var responseData = new InfraVM { }; - string responseMessage = ""; - string message = ""; - List projectIds = new List(); - if (infraDots != null) - { - foreach (var item in infraDots) - { - if (item.Building != null) - { - - Building building = item.Building.ToBuildingFromBuildingDto(tenantId); - building.TenantId = GetTenantId(); - - if (item.Building.Id == null) - { - //create - _context.Buildings.Add(building); - await _context.SaveChangesAsync(); - responseData.building = building; - responseMessage = "Buliding Added Successfully"; - message = "Building Added"; - } - else - { - //update - _context.Buildings.Update(building); - await _context.SaveChangesAsync(); - responseData.building = building; - responseMessage = "Buliding Updated Successfully"; - message = "Building Updated"; - - } - projectIds.Add(building.ProjectId); - } - if (item.Floor != null) - { - Floor floor = item.Floor.ToFloorFromFloorDto(tenantId); - floor.TenantId = GetTenantId(); - - if (item.Floor.Id == null) - { - //create - _context.Floor.Add(floor); - await _context.SaveChangesAsync(); - responseData.floor = floor; - responseMessage = "Floor Added Successfully"; - message = "Floor Added"; - } - else - { - //update - _context.Floor.Update(floor); - await _context.SaveChangesAsync(); - responseData.floor = floor; - responseMessage = "Floor Updated Successfully"; - message = "Floor Updated"; - } - Building? building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == floor.BuildingId); - projectIds.Add(building?.ProjectId ?? Guid.Empty); - message = $"{message} in Building: {building?.Name}"; - } - if (item.WorkArea != null) - { - WorkArea workArea = item.WorkArea.ToWorkAreaFromWorkAreaDto(tenantId); - workArea.TenantId = GetTenantId(); - - if (item.WorkArea.Id == null) - { - //create - _context.WorkAreas.Add(workArea); - await _context.SaveChangesAsync(); - responseData.workArea = workArea; - responseMessage = "Work Area Added Successfully"; - message = "Work Area Added"; - } - else - { - //update - _context.WorkAreas.Update(workArea); - await _context.SaveChangesAsync(); - responseData.workArea = workArea; - responseMessage = "Work Area Updated Successfully"; - message = "Work Area Updated"; - } - Floor? floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == workArea.FloorId); - projectIds.Add(floor?.Building?.ProjectId ?? Guid.Empty); - message = $"{message} in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}"; - } - } - message = $"{message} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}"; - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = message }; - - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); - return Ok(ApiResponse.SuccessResponse(responseData, responseMessage, 200)); - } - return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Infra Details are not valid.", 400)); + return StatusCode(response.StatusCode, response); } [HttpGet("assigned-projects/{employeeId}")] public async Task GetProjectsByEmployee([FromRoute] Guid employeeId) { - - Guid tenantId = _userHelper.GetTenantId(); - if (employeeId == Guid.Empty) + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) { - return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Employee id not valid.", 400)); + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("Get project list by employee Id called with invalid model state \n Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - List projectList = await _context.ProjectAllocations - .Where(c => c.TenantId == tenantId && c.EmployeeId == employeeId && c.IsActive) - .Select(c => c.ProjectId).Distinct() - .ToListAsync(); - - if (!projectList.Any()) - { - return NotFound(ApiResponse.SuccessResponse(new List(), "No projects found.", 200)); - } - - - List projectlist = await _context.Projects - .Where(p => projectList.Contains(p.Id)) - .ToListAsync(); - - List projects = new List(); - - - foreach (var project in projectlist) - { - - projects.Add(project.ToProjectListVMFromProject()); - } - - - - return Ok(ApiResponse.SuccessResponse(projects, "Success.", 200)); + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetProjectsByEmployeeAsync(employeeId, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } [HttpPost("assign-projects/{employeeId}")] - public async Task AssigneProjectsToEmployee([FromBody] List projectAllocationDtos, [FromRoute] Guid employeeId) + public async Task AssigneProjectsToEmployee([FromBody] List projectAllocationDtos, [FromRoute] Guid employeeId) { - if (projectAllocationDtos != null && employeeId != Guid.Empty) + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) { - Guid TenentID = GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - List? result = new List(); - List projectIds = new List(); - - foreach (var projectAllocationDto in projectAllocationDtos) - { - try - { - ProjectAllocation projectAllocation = projectAllocationDto.ToProjectAllocationFromProjectsAllocationDto(TenentID, employeeId); - ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.ProjectId == projectAllocationDto.ProjectId && c.ReAllocationDate == null && c.TenantId == TenentID).SingleOrDefaultAsync(); - - if (projectAllocationFromDb != null) - { - - - _context.ProjectAllocations.Attach(projectAllocationFromDb); - - if (projectAllocationDto.Status) - { - projectAllocationFromDb.JobRoleId = projectAllocation.JobRoleId; ; - projectAllocationFromDb.IsActive = true; - _context.Entry(projectAllocationFromDb).Property(e => e.JobRoleId).IsModified = true; - _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; - } - else - { - projectAllocationFromDb.ReAllocationDate = DateTime.UtcNow; - projectAllocationFromDb.IsActive = false; - _context.Entry(projectAllocationFromDb).Property(e => e.ReAllocationDate).IsModified = true; - _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; - - projectIds.Add(projectAllocation.ProjectId); - } - await _context.SaveChangesAsync(); - var result1 = new - { - Id = projectAllocationFromDb.Id, - EmployeeId = projectAllocation.EmployeeId, - JobRoleId = projectAllocation.JobRoleId, - IsActive = projectAllocation.IsActive, - ProjectId = projectAllocation.ProjectId, - AllocationDate = projectAllocation.AllocationDate, - ReAllocationDate = projectAllocation.ReAllocationDate, - TenantId = projectAllocation.TenantId - }; - result.Add(result1); - } - else - { - projectAllocation.AllocationDate = DateTime.Now; - projectAllocation.IsActive = true; - _context.ProjectAllocations.Add(projectAllocation); - await _context.SaveChangesAsync(); - - projectIds.Add(projectAllocation.ProjectId); - - } - - - } - catch (Exception ex) - { - - return Ok(ApiResponse.ErrorResponse(ex.Message, ex, 400)); - } - } - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeId = employeeId }; - - await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); - - return Ok(ApiResponse.SuccessResponse(result, "Data saved successfully", 200)); + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - else + + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.AssigneProjectsToEmployeeAsync(projectAllocationDtos, employeeId, tenantId, loggedInEmployee); + if (response.Success) { - return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "All Field is required", 400)); + List projectIds = response.Data.Select(pa => pa.ProjectId).ToList(); + + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeId = employeeId }; + await _signalR.SendNotificationAsync(notification); } + return StatusCode(response.StatusCode, response); + } + + #endregion + + #region =================================================================== Project InfraStructure Get APIs =================================================================== + + [HttpGet("infra-details/{projectId}")] + public async Task GetInfraDetails(Guid projectId) + { + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) + { + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("Get Project Infrastructure by ProjectId called with invalid model state \n Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); + } + + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetInfraDetailsAsync(projectId, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } + [HttpGet("tasks/{workAreaId}")] + public async Task GetWorkItems(Guid workAreaId) + { + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) + { + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("Get Work Items by WorkAreaId called with invalid model state \n Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); + } + + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.GetWorkItemsAsync(workAreaId, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); + } + + #endregion + + #region =================================================================== Project Infrastructre Manage APIs =================================================================== + + [HttpPost("manage-infra")] + public async Task ManageProjectInfra(List infraDtos) + { + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) + { + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); + } + + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var serviceResponse = await _projectServices.ManageProjectInfraAsync(infraDtos, tenantId, loggedInEmployee); + var response = serviceResponse.Response; + var notification = serviceResponse.Notification; + if (notification != null) + { + await _signalR.SendNotificationAsync(notification); + } + return StatusCode(response.StatusCode, response); + + } + + [HttpPost("task")] + public async Task CreateProjectTask([FromBody] List workItemDtos) + { + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) + { + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); + } + + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _projectServices.CreateProjectTaskAsync(workItemDtos, tenantId, loggedInEmployee); + if (response.Success) + { + List workAreaIds = response.Data.Select(pa => pa.WorkItem?.WorkAreaId ?? Guid.Empty).ToList(); + string message = response.Message; + + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = message }; + await _signalR.SendNotificationAsync(notification); + } + return StatusCode(response.StatusCode, response); + + } + + [HttpDelete("task/{id}")] + public async Task DeleteProjectTask(Guid id) + { + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) + { + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); + } + + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var serviceResponse = await _projectServices.DeleteProjectTaskAsync(id, tenantId, loggedInEmployee); + var response = serviceResponse.Response; + var notification = serviceResponse.Notification; + if (notification != null) + { + await _signalR.SendNotificationAsync(notification); + } + return StatusCode(response.StatusCode, response); + } + + #endregion } } \ No newline at end of file diff --git a/Marco.Pms.Services/Controllers/ReportController.cs b/Marco.Pms.Services/Controllers/ReportController.cs index 893c16b..a46c391 100644 --- a/Marco.Pms.Services/Controllers/ReportController.cs +++ b/Marco.Pms.Services/Controllers/ReportController.cs @@ -1,18 +1,19 @@ -using System.Data; -using System.Globalization; -using Marco.Pms.DataAccess.Data; -using Marco.Pms.Model.Dtos.Attendance; +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.Model.ViewModels.Report; +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 { @@ -26,34 +27,135 @@ namespace Marco.Pms.Services.Controllers private readonly ILoggingService _logger; private readonly UserHelper _userHelper; private readonly IWebHostEnvironment _env; - public ReportController(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, UserHelper userHelper, IWebHostEnvironment env) + private readonly ReportHelper _reportHelper; + private readonly IConfiguration _configuration; + private readonly CacheUpdateHelper _cache; + private readonly IServiceScopeFactory _serviceScopeFactory; + public ReportController(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, UserHelper userHelper, + IWebHostEnvironment env, ReportHelper reportHelper, IConfiguration configuration, CacheUpdateHelper cache, IServiceScopeFactory serviceScopeFactory) { _context = context; _emailSender = emailSender; _logger = logger; _userHelper = userHelper; _env = env; + _reportHelper = reportHelper; + _configuration = configuration; + _cache = cache; + _serviceScopeFactory = serviceScopeFactory; } - [HttpPost("set-mail")] + /// + /// Adds new mail details for a project report. + /// + /// The mail details data. + /// An API response indicating success or failure. + [HttpPost("mail-details")] // More specific route for adding mail details public async Task 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.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.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.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.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.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.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.ErrorResponse("Invalid Mail Template", "The specified mail template (MailListId) was not found or accessible.", 404)); + } + } + // If MailListId can be null/empty and implies no specific template, adjust logic accordingly. + // Currently assumes it must exist if provided. + + // 4. Create and Add New Mail Details + var newMailDetails = new MailDetails { ProjectId = mailDetailsDto.ProjectId, Recipient = mailDetailsDto.Recipient, Schedule = mailDetailsDto.Schedule, MailListId = mailDetailsDto.MailListId, - TenantId = tenantId + TenantId = tenantId, }; - _context.MailDetails.Add(mailDetails); - await _context.SaveChangesAsync(); - return Ok("Success"); + + try + { + _context.MailDetails.Add(newMailDetails); + await _context.SaveChangesAsync(); + _logger.LogInfo("Successfully added new mail details with ID {MailDetailsId} for ProjectId: {ProjectId}, Recipient: '{Recipient}', TenantId: {TenantId}.", newMailDetails.Id, newMailDetails.ProjectId, newMailDetails.Recipient, tenantId); + + // 5. Return Success Response (201 Created is ideal for resource creation) + return StatusCode(201, ApiResponse.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.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.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500)); + } } - [HttpPost("mail-template")] - public async Task AddMailTemplate([FromBody] MailTemeplateDto mailTemeplateDto) + [HttpPost("mail-template1")] + public async Task AddMailTemplate1([FromBody] MailTemeplateDto mailTemeplateDto) { Guid tenantId = _userHelper.GetTenantId(); if (string.IsNullOrWhiteSpace(mailTemeplateDto.Body) && string.IsNullOrWhiteSpace(mailTemeplateDto.Title)) @@ -80,263 +182,298 @@ namespace Marco.Pms.Services.Controllers return Ok("Success"); } + /// + /// Adds a new mail template. + /// + /// The mail template data. + /// An API response indicating success or failure. + [HttpPost("mail-template")] // More specific route for adding a template + public async Task 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.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.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.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.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.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.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.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.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.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500)); + } + } + [HttpGet("project-statistics")] public async Task SendProjectReport() { Guid tenantId = _userHelper.GetTenantId(); - // Use AsNoTracking() for read-only queries to improve performance - List mailDetails = await _context.MailDetails + // 1. OPTIMIZATION: Perform grouping and projection on the database server. + // This is far more efficient than loading all entities into memory. + var projectMailGroups = await _context.MailDetails .AsNoTracking() - .Include(m => m.MailBody) .Where(m => m.TenantId == tenantId) - .ToListAsync(); - - int successCount = 0; - int notFoundCount = 0; - int invalidIdCount = 0; - - var groupedMails = mailDetails .GroupBy(m => new { m.ProjectId, m.MailListId }) .Select(g => new { ProjectId = g.Key.ProjectId, - MailListId = g.Key.MailListId, Recipients = g.Select(m => m.Recipient).Distinct().ToList(), - MailBody = g.FirstOrDefault()?.MailBody?.Body ?? "", - Subject = g.FirstOrDefault()?.MailBody?.Subject ?? string.Empty, + // Project the mail body and subject from the first record in the group + MailInfo = g.Select(m => new { Body = m.MailBody != null ? m.MailBody.Body : "", Subject = m.MailBody != null ? m.MailBody.Subject : "" }).FirstOrDefault() }) - .ToList(); + .ToListAsync(); - var semaphore = new SemaphoreSlim(1); - - // Using Task.WhenAll to send reports concurrently for better performance - var sendTasks = groupedMails.Select(async mailDetail => + if (!projectMailGroups.Any()) { - await semaphore.WaitAsync(); - try + return Ok(ApiResponse.SuccessResponse(new { }, "No projects found to send reports for.", 200)); + } + + int successCount = 0; + int notFoundCount = 0; + int invalidIdCount = 0; + int failureCount = 0; + + // 2. OPTIMIZATION: Use true concurrency by removing SemaphoreSlim(1) + // and giving each task its own isolated set of services (including DbContext). + var sendTasks = projectMailGroups.Select(async mailGroup => + { + // SOLUTION: Create a new Dependency Injection scope for each parallel task. + using (var scope = _serviceScopeFactory.CreateScope()) { - var response = await GetProjectStatistics(mailDetail.ProjectId, mailDetail.Recipients, mailDetail.MailBody, mailDetail.Subject, tenantId); - if (response.StatusCode == 200) - Interlocked.Increment(ref successCount); - else if (response.StatusCode == 404) - Interlocked.Increment(ref notFoundCount); - else if (response.StatusCode == 400) - Interlocked.Increment(ref invalidIdCount); - } - finally - { - semaphore.Release(); + // Resolve a new instance of the helper from this isolated scope. + // This ensures each task gets its own thread-safe DbContext. + var reportHelper = scope.ServiceProvider.GetRequiredService(); + + try + { + // Ensure MailInfo and ProjectId are valid before proceeding + if (mailGroup.MailInfo == null || mailGroup.ProjectId == Guid.Empty) + { + Interlocked.Increment(ref invalidIdCount); + return; + } + + var response = await reportHelper.GetProjectStatistics( + mailGroup.ProjectId, + mailGroup.Recipients, + mailGroup.MailInfo.Body, + mailGroup.MailInfo.Subject, + tenantId); + + // Use a switch expression for cleaner counting + switch (response.StatusCode) + { + case 200: Interlocked.Increment(ref successCount); break; + case 404: Interlocked.Increment(ref notFoundCount); break; + case 400: Interlocked.Increment(ref invalidIdCount); break; + default: Interlocked.Increment(ref failureCount); break; + } + } + catch (Exception ex) + { + // 3. OPTIMIZATION: Make the process resilient. + // If one task fails unexpectedly, log it and continue with others. + _logger.LogError(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.SuccessResponse( - new { }, - $"Reports sent successfully: {successCount}. Projects not found: {notFoundCount}. Invalid IDs: {invalidIdCount}.", + new { successCount, notFoundCount, invalidIdCount, failureCount }, + summaryMessage, 200)); } - /// - /// Retrieves project statistics for a given project ID and sends an email report. - /// - /// The ID of the project. - /// The email address of the recipient. - /// An ApiResponse indicating the success or failure of retrieving statistics and sending the email. - private async Task> GetProjectStatistics(Guid projectId, List recipientEmails, string body, string subject, Guid tenantId) + + [HttpPost("add-report-mail")] + public async Task StoreProjectStatistics() { - DateTime reportDate = DateTime.UtcNow.AddDays(-1).Date; + Guid tenantId = _userHelper.GetTenantId(); - if (projectId == Guid.Empty) - { - _logger.LogError("Provided empty project ID while fetching project report."); - return ApiResponse.ErrorResponse("Provided empty Project ID.", "Provided empty Project ID.", 400); - } - - var project = await _context.Projects + // 1. Database-Side Grouping (Still the most efficient way to get initial data) + var projectMailGroups = await _context.MailDetails .AsNoTracking() - .FirstOrDefaultAsync(p => p.Id == projectId); - - if (project == null) - { - _logger.LogWarning("User attempted to fetch project progress for project ID {ProjectId} but not found.", projectId); - return ApiResponse.ErrorResponse("Project not found.", "Project not found.", 404); - } - - var statisticReport = new ProjectStatisticReport - { - Date = reportDate, - ProjectName = project.Name ?? "", - TimeStamp = DateTime.Now.ToString("dd-MMM-yyyy HH:mm:ss", CultureInfo.InvariantCulture) - }; - - // Preload relevant data - var projectAllocations = await _context.ProjectAllocations - .Include(p => p.Employee) - .Where(p => p.ProjectId == project.Id && p.IsActive) - .ToListAsync(); - - var assignedEmployeeIds = projectAllocations.Select(p => p.EmployeeId).ToHashSet(); - - var attendances = await _context.Attendes - .AsNoTracking() - .Where(a => a.ProjectID == project.Id && a.InTime != null && a.InTime.Value.Date == reportDate) - .ToListAsync(); - - var checkedInEmployeeIds = attendances.Select(a => a.EmployeeID).Distinct().ToHashSet(); - var checkoutPendingIds = attendances.Where(a => a.OutTime == null).Select(a => a.EmployeeID).Distinct().ToHashSet(); - var regularizationIds = attendances - .Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE) - .Select(a => a.EmployeeID).Distinct().ToHashSet(); - - // Preload buildings, floors, areas - var buildings = await _context.Buildings.Where(b => b.ProjectId == project.Id).ToListAsync(); - var buildingIds = buildings.Select(b => b.Id).ToList(); - - var floors = await _context.Floor.Where(f => buildingIds.Contains(f.BuildingId)).ToListAsync(); - var floorIds = floors.Select(f => f.Id).ToList(); - - var areas = await _context.WorkAreas.Where(a => floorIds.Contains(a.FloorId)).ToListAsync(); - var areaIds = areas.Select(a => a.Id).ToList(); - - var workItems = await _context.WorkItems - .Include(w => w.ActivityMaster) - .Where(w => areaIds.Contains(w.WorkAreaId)) - .ToListAsync(); - - var itemIds = workItems.Select(i => i.Id).ToList(); - - var tasks = await _context.TaskAllocations - .Where(t => itemIds.Contains(t.WorkItemId)) - .ToListAsync(); - - var taskIds = tasks.Select(t => t.Id).ToList(); - - var taskMembers = await _context.TaskMembers - .Include(m => m.Employee) - .Where(m => taskIds.Contains(m.TaskAllocationId)) - .ToListAsync(); - - // Aggregate data - double totalPlannedWork = workItems.Sum(w => w.PlannedWork); - double totalCompletedWork = workItems.Sum(w => w.CompletedWork); - - var todayAssignedTasks = tasks.Where(t => t.AssignmentDate.Date == reportDate).ToList(); - var reportPending = tasks.Where(t => t.ReportedDate == null).ToList(); - - double totalPlannedTask = todayAssignedTasks.Sum(t => t.PlannedTask); - double totalCompletedTask = todayAssignedTasks.Sum(t => t.CompletedTask); - - var jobRoles = await _context.JobRoles - .Where(j => j.TenantId == project.TenantId) - .ToListAsync(); - - // Team on site - var teamOnSite = jobRoles - .Select(role => + .Where(m => m.TenantId == tenantId && m.ProjectId != Guid.Empty) + .GroupBy(m => new { m.ProjectId, m.MailListId }) + .Select(g => new { - var count = projectAllocations.Count(p => p.JobRoleId == role.Id && checkedInEmployeeIds.Contains(p.EmployeeId)); - return new TeamOnSite { RoleName = role.Name, NumberofEmployees = count }; + 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() }) - .OrderByDescending(t => t.NumberofEmployees) - .ToList(); + .ToListAsync(); - // Task details - var performedTasks = todayAssignedTasks.Select(task => + if (!projectMailGroups.Any()) { - var workItem = workItems.FirstOrDefault(w => w.Id == task.WorkItemId); - var area = areas.FirstOrDefault(a => a.Id == workItem?.WorkAreaId); - var floor = floors.FirstOrDefault(f => f.Id == area?.FloorId); - var building = buildings.FirstOrDefault(b => b.Id == floor?.BuildingId); - - string activityName = workItem?.ActivityMaster?.ActivityName ?? ""; - string location = $"{building?.Name} > {floor?.FloorName}
{floor?.FloorName}-{area?.AreaName}"; - double pending = (workItem?.PlannedWork ?? 0) - (workItem?.CompletedWork ?? 0); - - var taskTeam = taskMembers - .Where(m => m.TaskAllocationId == task.Id) - .Select(m => - { - string name = $"{m.Employee?.FirstName ?? ""} {m.Employee?.LastName ?? ""}"; - var role = jobRoles.FirstOrDefault(r => r.Id == m.Employee?.JobRoleId); - return new TaskTeam { Name = name, RoleName = role?.Name ?? "" }; - }) - .ToList(); - - return new PerformedTask - { - Activity = activityName, - Location = location, - AssignedToday = task.PlannedTask, - CompletedToday = task.CompletedTask, - Pending = pending, - Comment = task.Description, - Team = taskTeam - }; - }).ToList(); - - // Attendance details - var performedAttendance = attendances.Select(att => - { - var alloc = projectAllocations.FirstOrDefault(p => p.EmployeeId == att.EmployeeID); - var role = jobRoles.FirstOrDefault(r => r.Id == alloc?.JobRoleId); - string name = $"{alloc?.Employee?.FirstName ?? ""} {alloc?.Employee?.LastName ?? ""}"; - - return new PerformedAttendance - { - Name = name, - RoleName = role?.Name ?? "", - InTime = att.InTime ?? DateTime.UtcNow, - OutTime = att.OutTime, - Comment = att.Comment - }; - }).ToList(); - - // Fill report - statisticReport.TodaysAttendances = checkedInEmployeeIds.Count; - statisticReport.TotalEmployees = assignedEmployeeIds.Count; - statisticReport.RegularizationPending = regularizationIds.Count; - statisticReport.CheckoutPending = checkoutPendingIds.Count; - statisticReport.TotalPlannedWork = totalPlannedWork; - statisticReport.TotalCompletedWork = totalCompletedWork; - statisticReport.TotalPlannedTask = totalPlannedTask; - statisticReport.TotalCompletedTask = totalCompletedTask; - statisticReport.CompletionStatus = totalPlannedWork > 0 ? totalCompletedWork / totalPlannedWork : 0; - statisticReport.TodaysAssignTasks = todayAssignedTasks.Count; - statisticReport.ReportPending = reportPending.Count; - statisticReport.TeamOnSite = teamOnSite; - statisticReport.PerformedTasks = performedTasks; - statisticReport.PerformedAttendance = performedAttendance; - - // Send Email - var emailBody = await _emailSender.SendProjectStatisticsEmail(recipientEmails, body, subject, statisticReport); - var employee = await _context.Employees.FirstOrDefaultAsync(e => e.Email != null && recipientEmails.Contains(e.Email)) ?? new Employee(); - - List mailLogs = new List(); - foreach (var recipientEmail in recipientEmails) - { - mailLogs.Add( - new MailLog - { - ProjectId = projectId, - EmailId = recipientEmail, - Body = emailBody, - EmployeeId = employee.Id, - TimeStamp = DateTime.UtcNow, - TenantId = tenantId - }); + _logger.LogInfo("No project mail details found for tenant {TenantId} to process.", tenantId); + return Ok(ApiResponse.SuccessResponse("No project reports to generate.", "No project reports to generate.", 200)); } - _context.MailLogs.AddRange(mailLogs); + string env = _configuration["environment:Title"] ?? string.Empty; - await _context.SaveChangesAsync(); - return ApiResponse.SuccessResponse(statisticReport, "Email sent successfully", 200); + // 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(); + var emailSender = scope.ServiceProvider.GetRequiredService(); + var cache = scope.ServiceProvider.GetRequiredService(); // e.g., IProjectReportCache + + // The rest of the logic is the same, but now it's thread-safe. + try + { + var projectId = group.ProjectId; + var statisticReport = await reportHelper.GetDailyProjectReport(projectId, tenantId); + + if (statisticReport == null) + { + _logger.LogWarning("Statistic report for project ID {ProjectId} not found. Skipping.", projectId); + return; + } + + if (group.MailInfo == null) + { + _logger.LogWarning("MailBody info for project ID {ProjectId} not found. Skipping.", projectId); + return; + } + + var date = statisticReport.Date.ToString("dd-MMM-yyyy", CultureInfo.InvariantCulture); + // Assuming the first param to SendProjectStatisticsEmail was just a placeholder + var emailBody = await emailSender.SendProjectStatisticsEmail(new List(), group.MailInfo.Body, string.Empty, statisticReport); + + string subject = group.MailInfo.Subject + .Replace("{{DATE}}", date) + .Replace("{{PROJECT_NAME}}", statisticReport.ProjectName); + + subject = string.IsNullOrWhiteSpace(env) ? subject : $"({env}) {subject}"; + + var mail = new ProjectReportEmailMongoDB + { + IsSent = false, + Body = emailBody, + Receivers = group.Recipients, + Subject = subject, + }; + + await cache.AddProjectReportMail(mail); + } + catch (Exception ex) + { + // It's good practice to log any unexpected errors within a concurrent task. + _logger.LogError(ex, "Failed to process project report for ProjectId {ProjectId}", group.ProjectId); + } + } + }); + + // Await all the concurrent, now thread-safe, tasks. + await Task.WhenAll(processingTasks); + + return Ok(ApiResponse.SuccessResponse( + $"{projectMailGroups.Count} Project Report Mail(s) are queued for storage.", + "Project Report Mail processing initiated.", + 200)); + } + + + [HttpGet("report-mail")] + public async Task GetProjectStatisticsFromCache() + { + var mailList = await _cache.GetProjectReportMail(false); + if (mailList == null) + { + return NotFound(ApiResponse.ErrorResponse("Not mail found", "Not mail found", 404)); + } + + return Ok(ApiResponse.SuccessResponse(mailList, "Fetched list of mail body successfully", 200)); } } } diff --git a/Marco.Pms.Services/Controllers/RolesController.cs b/Marco.Pms.Services/Controllers/RolesController.cs index 2ac2b07..a67ecaf 100644 --- a/Marco.Pms.Services/Controllers/RolesController.cs +++ b/Marco.Pms.Services/Controllers/RolesController.cs @@ -10,6 +10,7 @@ using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels; using Marco.Pms.Model.ViewModels.Master; using Marco.Pms.Model.ViewModels.Roles; +using Marco.Pms.Services.Helpers; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; @@ -29,14 +30,17 @@ namespace MarcoBMS.Services.Controllers private readonly UserHelper _userHelper; private readonly UserManager _userManager; private readonly ILoggingService _logger; + private readonly CacheUpdateHelper _cache; - public RolesController(UserManager userManager, ApplicationDbContext context, RolesHelper rolesHelper, UserHelper userHelper, ILoggingService logger) + public RolesController(UserManager userManager, ApplicationDbContext context, RolesHelper rolesHelper, UserHelper userHelper, ILoggingService logger, + CacheUpdateHelper cache) { _context = context; _userManager = userManager; _rolesHelper = rolesHelper; _userHelper = userHelper; _logger = logger; + _cache = cache; } private Guid GetTenantId() @@ -288,10 +292,16 @@ namespace MarcoBMS.Services.Controllers _context.RolePermissionMappings.Add(item); modified = true; } + if (item.FeaturePermissionId == Guid.Parse("172fc9b6-755b-4f62-ab26-55c34a330614")) + { + await _cache.ClearAllProjectIdsByRoleId(id); + } } if (modified) await _context.SaveChangesAsync(); + await _cache.ClearAllPermissionIdsByRoleId(id); + ApplicationRolesVM response = role.ToRoleVMFromApplicationRole(); List permissions = await _rolesHelper.GetFeaturePermissionByRoleID(response.Id); response.FeaturePermission = permissions.Select(c => c.ToFeaturePermissionVMFromFeaturePermission()).ToList(); @@ -424,12 +434,16 @@ namespace MarcoBMS.Services.Controllers if (role.IsEnabled == true) { _context.EmployeeRoleMappings.Add(mapping); + await _cache.AddApplicationRole(role.EmployeeId, [mapping.RoleId]); } } else if (role.IsEnabled == false) { _context.EmployeeRoleMappings.Remove(existingItem); + await _cache.RemoveRoleId(existingItem.EmployeeId, existingItem.RoleId); + await _cache.ClearAllPermissionIdsByEmployeeID(existingItem.EmployeeId); } + await _cache.ClearAllProjectIds(role.EmployeeId); } await _context.SaveChangesAsync(); diff --git a/Marco.Pms.Services/Controllers/TaskController.cs b/Marco.Pms.Services/Controllers/TaskController.cs index 6b55c3f..b764f00 100644 --- a/Marco.Pms.Services/Controllers/TaskController.cs +++ b/Marco.Pms.Services/Controllers/TaskController.cs @@ -6,11 +6,14 @@ using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Activities; +using Marco.Pms.Services.Helpers; +using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Service; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; using Microsoft.CodeAnalysis; using Microsoft.EntityFrameworkCore; using Document = Marco.Pms.Model.DocumentManager.Document; @@ -27,19 +30,20 @@ namespace MarcoBMS.Services.Controllers private readonly UserHelper _userHelper; private readonly S3UploadService _s3Service; private readonly ILoggingService _logger; + private readonly IHubContext _signalR; + private readonly CacheUpdateHelper _cache; private readonly PermissionServices _permissionServices; - private readonly Guid Approve_Task; - private readonly Guid Assign_Report_Task; - public TaskController(ApplicationDbContext context, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permissionServices) + public TaskController(ApplicationDbContext context, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permissionServices, + IHubContext signalR, CacheUpdateHelper cache) { _context = context; _userHelper = userHelper; _s3Service = s3Service; _logger = logger; + _signalR = signalR; + _cache = cache; _permissionServices = permissionServices; - Approve_Task = Guid.Parse("db4e40c5-2ba9-4b6d-b8a6-a16a250ff99c"); - Assign_Report_Task = Guid.Parse("6a32379b-8b3f-49a6-8c48-4b7ac1b55dc2"); } private Guid GetTenantId() @@ -67,7 +71,7 @@ namespace MarcoBMS.Services.Controllers var employee = await _userHelper.GetCurrentEmployeeAsync(); // Check for permission to approve tasks - var hasPermission = await _permissionServices.HasPermission(Assign_Report_Task, employee.Id); + var hasPermission = await _permissionServices.HasPermission(PermissionsMaster.AssignAndReportProgress, employee.Id); if (!hasPermission) { _logger.LogWarning("Employee {EmployeeId} attempted to assign Task without permission", employee.Id); @@ -81,6 +85,8 @@ namespace MarcoBMS.Services.Controllers _context.TaskAllocations.Add(taskAllocation); await _context.SaveChangesAsync(); + await _cache.UpdatePlannedAndCompleteWorksInWorkItem(taskAllocation.WorkItemId, todaysAssigned: taskAllocation.PlannedTask); + _logger.LogInfo("Task {TaskId} assigned by Employee {EmployeeId}", taskAllocation.Id, employee.Id); var response = taskAllocation.ToAssignTaskVMFromTaskAllocation(); @@ -131,7 +137,7 @@ namespace MarcoBMS.Services.Controllers var tenantId = GetTenantId(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var hasPermission = await _permissionServices.HasPermission(Assign_Report_Task, loggedInEmployee.Id); + var hasPermission = await _permissionServices.HasPermission(PermissionsMaster.AssignAndReportProgress, loggedInEmployee.Id); if (!hasPermission) { _logger.LogWarning("Unauthorized task report attempt by Employee {EmployeeId} for Task {TaskId}", loggedInEmployee.Id, reportTask.Id); @@ -194,16 +200,22 @@ namespace MarcoBMS.Services.Controllers var comment = reportTask.ToCommentFromReportTaskDto(tenantId, loggedInEmployee.Id); _context.TaskComments.Add(comment); + int numberofImages = 0; + + var workAreaId = taskAllocation.WorkItem?.WorkAreaId; + var workArea = await _context.WorkAreas.Include(a => a.Floor) + .FirstOrDefaultAsync(a => a.Id == workAreaId) ?? new WorkArea(); + + var buildingId = workArea.Floor?.BuildingId; + + var building = await _context.Buildings + .FirstOrDefaultAsync(b => b.Id == buildingId); + var batchId = Guid.NewGuid(); + var projectId = building?.ProjectId; + if (reportTask.Images?.Any() == true) { - var workAreaId = taskAllocation.WorkItem?.WorkAreaId; - var workArea = await _context.WorkAreas.Include(a => a.Floor) - .FirstOrDefaultAsync(a => a.Id == workAreaId) ?? new WorkArea(); - var buildingId = workArea.Floor?.BuildingId; - - var building = await _context.Buildings - .FirstOrDefaultAsync(b => b.Id == buildingId); foreach (var image in reportTask.Images) { @@ -219,16 +231,18 @@ namespace MarcoBMS.Services.Controllers var fileType = _s3Service.GetContentTypeFromBase64(base64); var fileName = _s3Service.GenerateFileName(fileType, tenantId, "task_report"); - var objectKey = $"tenant-{tenantId}/project-{building?.ProjectId}/Actitvity/{fileName}"; + var objectKey = $"tenant-{tenantId}/project-{projectId}/Actitvity/{fileName}"; await _s3Service.UploadFileAsync(base64, fileType, objectKey); var document = new Document { + BatchId = batchId, + UploadedById = loggedInEmployee.Id, FileName = image.FileName ?? "", ContentType = image.ContentType ?? "", S3Key = objectKey, - Base64Data = image.Base64Data, + //Base64Data = image.Base64Data, FileSize = image.FileSize, UploadedAt = DateTime.UtcNow, TenantId = tenantId @@ -241,10 +255,15 @@ namespace MarcoBMS.Services.Controllers ReferenceId = reportTask.Id }; _context.TaskAttachments.Add(attachment); + numberofImages += 1; } } await _context.SaveChangesAsync(); + var selectedWorkAreaId = taskAllocation.WorkItem?.WorkAreaId ?? Guid.Empty; + + await _cache.UpdatePlannedAndCompleteWorksInWorkItem(taskAllocation.WorkItemId, completedWork: taskAllocation.CompletedTask); + await _cache.UpdatePlannedAndCompleteWorksInBuilding(selectedWorkAreaId, completedWork: taskAllocation.CompletedTask); var response = taskAllocation.ToReportTaskVMFromTaskAllocation(); var comments = await _context.TaskComments @@ -254,6 +273,9 @@ namespace MarcoBMS.Services.Controllers response.Comments = comments.Select(c => c.ToCommentVMFromTaskComment()).ToList(); response.checkList = checkListVMs; + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Task_Report", NumberOfImages = numberofImages, ProjectId = projectId }; + await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + _logger.LogInfo("Task {TaskId} reported successfully by Employee {EmployeeId}", taskAllocation.Id, loggedInEmployee.Id); return Ok(ApiResponse.SuccessResponse(response, "Task reported successfully", 200)); @@ -265,7 +287,7 @@ namespace MarcoBMS.Services.Controllers _logger.LogInfo("AddCommentForTask called for TaskAllocationId: {TaskId}", createComment.TaskAllocationId); var tenantId = GetTenantId(); - var employee = await _userHelper.GetCurrentEmployeeAsync(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); // Validate Task Allocation and associated WorkItem var taskAllocation = await _context.TaskAllocations @@ -285,15 +307,18 @@ namespace MarcoBMS.Services.Controllers var buildingId = workArea.Floor?.BuildingId ?? Guid.Empty; var building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == buildingId); + var projectId = building?.ProjectId; // Save comment - var comment = createComment.ToCommentFromCommentDto(tenantId, employee.Id); + var comment = createComment.ToCommentFromCommentDto(tenantId, loggedInEmployee.Id); _context.TaskComments.Add(comment); await _context.SaveChangesAsync(); _logger.LogInfo("Comment saved with Id: {CommentId}", comment.Id); // Process image uploads var images = createComment.Images; + var batchId = Guid.NewGuid(); + int numberofImages = 0; if (images != null && images.Any()) { @@ -312,17 +337,19 @@ namespace MarcoBMS.Services.Controllers var fileType = _s3Service.GetContentTypeFromBase64(base64); var fileName = _s3Service.GenerateFileName(fileType, tenantId, "task_comment"); - var objectKey = $"tenant-{tenantId}/project-{building?.ProjectId}/Activity/{fileName}"; + var objectKey = $"tenant-{tenantId}/project-{projectId}/Activity/{fileName}"; await _s3Service.UploadFileAsync(base64, fileType, objectKey); _logger.LogInfo("Image uploaded to S3 with key: {ObjectKey}", objectKey); var document = new Document { + BatchId = batchId, + UploadedById = loggedInEmployee.Id, FileName = image.FileName ?? string.Empty, ContentType = image.ContentType ?? fileType, S3Key = objectKey, - Base64Data = image.Base64Data, + //Base64Data = image.Base64Data, FileSize = image.FileSize, UploadedAt = DateTime.UtcNow, TenantId = tenantId @@ -337,6 +364,7 @@ namespace MarcoBMS.Services.Controllers }; _context.TaskAttachments.Add(attachment); + numberofImages += 1; } await _context.SaveChangesAsync(); @@ -347,6 +375,9 @@ namespace MarcoBMS.Services.Controllers var response = comment.ToCommentVMFromTaskComment(); _logger.LogInfo("Returning response for commentId: {CommentId}", comment.Id); + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Task_Comment", NumberOfImages = numberofImages, ProjectId = projectId }; + await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + return Ok(ApiResponse.SuccessResponse(response, "Comment saved successfully", 200)); } @@ -653,6 +684,7 @@ namespace MarcoBMS.Services.Controllers /// /// DTO containing task approval details. /// IActionResult indicating success or failure. + [HttpPost("approve")] public async Task ApproveTask(ApproveTaskDto approveTask) { @@ -675,7 +707,7 @@ namespace MarcoBMS.Services.Controllers } // Check for permission to approve tasks - var hasPermission = await _permissionServices.HasPermission(Approve_Task, loggedInEmployee.Id); + var hasPermission = await _permissionServices.HasPermission(PermissionsMaster.ApproveTask, loggedInEmployee.Id); if (!hasPermission) { _logger.LogWarning("Employee {EmployeeId} attempted to approve Task {TaskId} without permission", loggedInEmployee.Id, approveTask.Id); @@ -719,17 +751,21 @@ namespace MarcoBMS.Services.Controllers }; _context.TaskComments.Add(comment); + var workAreaId = taskAllocation.WorkItem?.WorkAreaId; + var workArea = await _context.WorkAreas.Include(a => a.Floor) + .FirstOrDefaultAsync(a => a.Id == workAreaId) ?? new WorkArea(); + + var buildingId = workArea.Floor?.BuildingId; + + var building = await _context.Buildings + .FirstOrDefaultAsync(b => b.Id == buildingId); + var projectId = building?.ProjectId; + int numberofImages = 0; + // Handle image attachments, if any if (approveTask.Images?.Count > 0) { - var workAreaId = taskAllocation.WorkItem?.WorkAreaId; - var workArea = await _context.WorkAreas.Include(a => a.Floor) - .FirstOrDefaultAsync(a => a.Id == workAreaId) ?? new WorkArea(); - - var buildingId = workArea.Floor?.BuildingId; - - var building = await _context.Buildings - .FirstOrDefaultAsync(b => b.Id == buildingId); + var batchId = Guid.NewGuid(); foreach (var image in approveTask.Images) { @@ -743,16 +779,18 @@ namespace MarcoBMS.Services.Controllers var fileType = _s3Service.GetContentTypeFromBase64(base64); var fileName = _s3Service.GenerateFileName(fileType, tenantId, "task_comment"); - var objectKey = $"tenant-{tenantId}/project-{building?.ProjectId}/Activity/{fileName}"; + var objectKey = $"tenant-{tenantId}/project-{projectId}/Activity/{fileName}"; await _s3Service.UploadFileAsync(base64, fileType, objectKey); var document = new Document { + BatchId = batchId, + UploadedById = loggedInEmployee.Id, FileName = fileName, ContentType = image.ContentType ?? string.Empty, S3Key = objectKey, - Base64Data = image.Base64Data, + //Base64Data = image.Base64Data, FileSize = image.FileSize, UploadedAt = DateTime.UtcNow, TenantId = tenantId @@ -769,12 +807,16 @@ namespace MarcoBMS.Services.Controllers _context.TaskAttachments.Add(attachment); _logger.LogInfo("Attachment uploaded for Task {TaskId}: {FileName}", approveTask.Id, fileName); + numberofImages += 1; } } // Commit all changes to the database await _context.SaveChangesAsync(); + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Task_Report", NumberOfImages = numberofImages, ProjectId = projectId }; + await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + _logger.LogInfo("Task {TaskId} successfully approved by Employee {EmployeeId}", approveTask.Id, loggedInEmployee.Id); return Ok(ApiResponse.SuccessResponse("Task has been approved", "Task has been approved", 200)); diff --git a/Marco.Pms.Services/Controllers/UserController.cs b/Marco.Pms.Services/Controllers/UserController.cs index 2d33b15..8269d3e 100644 --- a/Marco.Pms.Services/Controllers/UserController.cs +++ b/Marco.Pms.Services/Controllers/UserController.cs @@ -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; } @@ -45,22 +46,23 @@ namespace MarcoBMS.Services.Controllers var user = await _userHelper.GetCurrentUserAsync(); Employee emp = new Employee { }; - if(user != null) + if (user != null) { emp = await _employeeHelper.GetEmployeeByApplicationUserID(user.Id); } - List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(emp.Id); + List 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 projects = await _projectsHelper.GetAllProjectByTanentID(emp.TenantId); - projectsId = projects.Select(c=>c.Id.ToString()).ToArray(); + if (featurePermission != null && featurePermission.Exists(c => c.Id.ToString() == "172fc9b6-755b-4f62-ab26-55c34a330614")) + { + List projects = await _projectServices.GetAllProjectByTanentID(emp.TenantId); + projectsId = projects.Select(c => c.Id.ToString()).ToArray(); } else { - List allocation = await _projectsHelper.GetProjectByEmployeeID(emp.Id); + List allocation = await _projectServices.GetProjectByEmployeeID(emp.Id); projectsId = allocation.Select(c => c.ProjectId.ToString()).ToArray(); } EmployeeProfile profile = new EmployeeProfile() { }; diff --git a/Marco.Pms.Services/Dockerfile b/Marco.Pms.Services/Dockerfile index 5444e56..2aa24ea 100644 --- a/Marco.Pms.Services/Dockerfile +++ b/Marco.Pms.Services/Dockerfile @@ -19,6 +19,7 @@ COPY ["Marco.Pms.Services/Marco.Pms.Services.csproj", "Marco.Pms.Services/"] COPY ["Marco.Pms.DataAccess/Marco.Pms.DataAccess.csproj", "Marco.Pms.DataAccess/"] COPY ["Marco.Pms.Model/Marco.Pms.Model.csproj", "Marco.Pms.Model/"] COPY ["Marco.Pms.Utility/Marco.Pms.Utility.csproj", "Marco.Pms.Utility/"] +COPY ["Marco.Pms.CacheHelper/Marco.Pms.CacheHelper.csproj", "Marco.Pms.CacheHelper/"] RUN dotnet restore "./Marco.Pms.Services/Marco.Pms.Services.csproj" COPY . . WORKDIR "/src/Marco.Pms.Services" diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs new file mode 100644 index 0000000..d942ab1 --- /dev/null +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -0,0 +1,867 @@ +using Marco.Pms.CacheHelper; +using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.Master; +using Marco.Pms.Model.MongoDBModels; +using Marco.Pms.Model.Projects; +using MarcoBMS.Services.Service; +using Microsoft.EntityFrameworkCore; +using Project = Marco.Pms.Model.Projects.Project; + +namespace Marco.Pms.Services.Helpers +{ + public class CacheUpdateHelper + { + private readonly ProjectCache _projectCache; + private readonly EmployeeCache _employeeCache; + private readonly ReportCache _reportCache; + private readonly ILoggingService _logger; + private readonly IDbContextFactory _dbContextFactory; + private readonly ApplicationDbContext _context; + private readonly GeneralHelper _generalHelper; + + public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache, ReportCache reportCache, ILoggingService logger, + IDbContextFactory dbContextFactory, ApplicationDbContext context, GeneralHelper generalHelper) + { + _projectCache = projectCache; + _employeeCache = employeeCache; + _reportCache = reportCache; + _logger = logger; + _dbContextFactory = dbContextFactory; + _context = context; + _generalHelper = generalHelper; + } + + // ------------------------------------ Project Details Cache --------------------------------------- + + public async Task AddProjectDetails(Project project) + { + // --- Step 1: Fetch all required data from the database in parallel --- + + // Each task uses its own DbContext instance to avoid concurrency issues. + var statusTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + return await context.StatusMasters + .AsNoTracking() + .Where(s => s.Id == project.ProjectStatusId) + .Select(s => new { s.Id, s.Status }) // Projection + .FirstOrDefaultAsync(); + }); + + var teamSizeTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + return await context.ProjectAllocations + .AsNoTracking() + .CountAsync(pa => pa.ProjectId == project.Id && pa.IsActive); // Server-side count is efficient + }); + + // This task fetches the entire infrastructure hierarchy and performs aggregations in the database. + var infrastructureTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + + // 1. Fetch all hierarchical data using projections. + // This is still a chain, but it's inside one task and much faster due to projections. + var buildings = await context.Buildings.AsNoTracking() + .Where(b => b.ProjectId == project.Id) + .Select(b => new { b.Id, b.ProjectId, b.Name, b.Description }) + .ToListAsync(); + var buildingIds = buildings.Select(b => b.Id).ToList(); + + var floors = await context.Floor.AsNoTracking() + .Where(f => buildingIds.Contains(f.BuildingId)) + .Select(f => new { f.Id, f.BuildingId, f.FloorName }) + .ToListAsync(); + var floorIds = floors.Select(f => f.Id).ToList(); + + var workAreas = await context.WorkAreas.AsNoTracking() + .Where(wa => floorIds.Contains(wa.FloorId)) + .Select(wa => new { wa.Id, wa.FloorId, wa.AreaName }) + .ToListAsync(); + var workAreaIds = workAreas.Select(wa => wa.Id).ToList(); + + // 2. THE KEY OPTIMIZATION: Aggregate work items in the database. + var workSummaries = await context.WorkItems.AsNoTracking() + .Where(wi => workAreaIds.Contains(wi.WorkAreaId)) + .GroupBy(wi => wi.WorkAreaId) // Group by parent on the DB server + .Select(g => new // Let the DB do the SUM + { + WorkAreaId = g.Key, + PlannedWork = g.Sum(i => i.PlannedWork), + CompletedWork = g.Sum(i => i.CompletedWork) + }) + .ToDictionaryAsync(x => x.WorkAreaId); // Return a ready-to-use dictionary + + return (buildings, floors, workAreas, workSummaries); + }); + + // Wait for all parallel database operations to complete. + await Task.WhenAll(statusTask, teamSizeTask, infrastructureTask); + + // Get the results from the completed tasks. + var status = await statusTask; + var teamSize = await teamSizeTask; + var (allBuildings, allFloors, allWorkAreas, workSummariesByWorkAreaId) = await infrastructureTask; + + // --- Step 2: Process the fetched data and build the MongoDB model --- + + var projectDetails = new ProjectMongoDB + { + Id = project.Id.ToString(), + Name = project.Name, + ShortName = project.ShortName, + ProjectAddress = project.ProjectAddress, + StartDate = project.StartDate, + EndDate = project.EndDate, + ContactPerson = project.ContactPerson, + TeamSize = teamSize + }; + + projectDetails.ProjectStatus = new StatusMasterMongoDB + { + Id = status!.Id.ToString(), + Status = status.Status + }; + + // Use fast in-memory lookups instead of .Where() in loops. + var floorsByBuildingId = allFloors.ToLookup(f => f.BuildingId); + var workAreasByFloorId = allWorkAreas.ToLookup(wa => wa.FloorId); + + double totalPlannedWork = 0, totalCompletedWork = 0; + var buildingMongoList = new List(); + + foreach (var building in allBuildings) + { + double buildingPlanned = 0, buildingCompleted = 0; + var floorMongoList = new List(); + + foreach (var floor in floorsByBuildingId[building.Id]) // Fast lookup + { + double floorPlanned = 0, floorCompleted = 0; + var workAreaMongoList = new List(); + + foreach (var wa in workAreasByFloorId[floor.Id]) // Fast lookup + { + // Get the pre-calculated summary from the dictionary. O(1) operation. + workSummariesByWorkAreaId.TryGetValue(wa.Id, out var summary); + var waPlanned = summary?.PlannedWork ?? 0; + var waCompleted = summary?.CompletedWork ?? 0; + + workAreaMongoList.Add(new WorkAreaMongoDB + { + Id = wa.Id.ToString(), + FloorId = wa.FloorId.ToString(), + AreaName = wa.AreaName, + PlannedWork = waPlanned, + CompletedWork = waCompleted + }); + + floorPlanned += waPlanned; + floorCompleted += waCompleted; + } + + floorMongoList.Add(new FloorMongoDB + { + Id = floor.Id.ToString(), + BuildingId = floor.BuildingId.ToString(), + FloorName = floor.FloorName, + PlannedWork = floorPlanned, + CompletedWork = floorCompleted, + WorkAreas = workAreaMongoList + }); + + buildingPlanned += floorPlanned; + buildingCompleted += floorCompleted; + } + + buildingMongoList.Add(new BuildingMongoDB + { + Id = building.Id.ToString(), + ProjectId = building.ProjectId.ToString(), + BuildingName = building.Name, + Description = building.Description, + PlannedWork = buildingPlanned, + CompletedWork = buildingCompleted, + Floors = floorMongoList + }); + + totalPlannedWork += buildingPlanned; + totalCompletedWork += buildingCompleted; + } + + projectDetails.Buildings = buildingMongoList; + projectDetails.PlannedWork = totalPlannedWork; + projectDetails.CompletedWork = totalCompletedWork; + + try + { + await _projectCache.AddProjectDetailsToCache(projectDetails); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occurred while adding project {ProjectId} to Cache", project.Id); + } + } + public async Task AddProjectDetailsList(List 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(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(); + + foreach (var building in buildingsByProjectId[project.Id]) + { + double buildingPlanned = 0, buildingCompleted = 0; + var floorMongoList = new List(); + + foreach (var floor in floorsByBuildingId[building.Id]) + { + double floorPlanned = 0, floorCompleted = 0; + var workAreaMongoList = new List(); + + 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 UpdateProjectDetailsOnly(Project project) + { + StatusMaster projectStatus = await _context.StatusMasters + .FirstOrDefaultAsync(s => s.Id == project.ProjectStatusId) ?? new StatusMaster(); + try + { + bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project, projectStatus); + return response; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occured while updating project {ProjectId} to Cache", project.Id); + return false; + } + } + public async Task GetProjectDetails(Guid projectId) + { + try + { + var response = await _projectCache.GetProjectDetailsFromCache(projectId); + return response; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occured while getting project {ProjectId} to Cache"); + return null; + } + } + public async Task GetProjectDetailsWithBuildings(Guid projectId) + { + try + { + var response = await _projectCache.GetProjectDetailsWithBuildingsFromCache(projectId); + return response; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occured while getting project {ProjectId} to Cache"); + return null; + } + } + public async Task?> GetProjectDetailsList(List projectIds) + { + try + { + var response = await _projectCache.GetProjectDetailsListFromCache(projectIds); + if (response.Any()) + { + return response; + } + else + { + return null; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occured while getting list of project details from to Cache"); + return null; + } + } + public async Task DeleteProjectByIdAsync(Guid projectId) + { + try + { + var response = await _projectCache.DeleteProjectByIdFromCacheAsync(projectId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occured while deleting project from to Cache"); + + } + } + public async Task RemoveProjectsAsync(List projectIds) + { + try + { + var response = await _projectCache.RemoveProjectsFromCacheAsync(projectIds); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occured while deleting project list from to Cache"); + + } + } + + // ------------------------------------ Project Infrastructure Cache --------------------------------------- + + public async Task AddBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) + { + try + { + await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while adding project infra for project {ProjectId} to Cache: {Error}", projectId, ex.Message); + } + } + public async Task UpdateBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) + { + try + { + var response = await _projectCache.UpdateBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + if (!response) + { + await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); + } + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while updating project infra for project {ProjectId} to Cache: {Error}", projectId, ex.Message); + } + } + public async Task?> GetBuildingInfra(Guid projectId) + { + try + { + var response = await _projectCache.GetBuildingInfraFromCache(projectId); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while getting project infra for project {ProjectId} form Cache: {Error}", projectId, ex.Message); + return null; + } + } + public async Task UpdatePlannedAndCompleteWorksInBuilding(Guid workAreaId, double plannedWork = 0, double completedWork = 0) + { + try + { + await _projectCache.UpdatePlannedAndCompleteWorksInBuildingFromCache(workAreaId, plannedWork, completedWork); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while updating planned work and completed work in building infra form Cache: {Error}", ex.Message); + } + } + public async Task GetBuildingAndFloorByWorkAreaId(Guid workAreaId) + { + try + { + var response = await _projectCache.GetBuildingAndFloorByWorkAreaIdFromCache(workAreaId); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while fetching workArea Details using its ID form Cache: {Error}", ex.Message); + return null; + } + } + + // ------------------------------------------------------- WorkItem ------------------------------------------------------- + + public async Task?> GetWorkItemsByWorkAreaIds(List workAreaIds) + { + try + { + var response = await _projectCache.GetWorkItemsByWorkAreaIdsFromCache(workAreaIds); + if (response.Count > 0) + { + return response; + } + return null; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while fetching workItems list using workArea IDs list form Cache: {Error}", ex.Message); + return null; + } + } + public async Task ManageWorkItemDetails(List 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 workItems) + { + try + { + await _projectCache.ManageWorkItemDetailsToCache(workItems); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while saving workItems form Cache: {Error}", ex.Message); + } + } + public async Task?> GetWorkItemDetailsByWorkArea(Guid workAreaId) + { + try + { + var workItems = await _projectCache.GetWorkItemDetailsByWorkAreaFromCache(workAreaId); + if (workItems.Count > 0) + { + return workItems; + } + else + { + return null; + } + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while fetching list of workItems form Cache: {Error}", ex.Message); + return null; + } + } + public async Task GetWorkItemDetailsById(Guid id) + { + try + { + var workItem = await _projectCache.GetWorkItemDetailsByIdFromCache(id); + if (workItem.Id != "") + { + return workItem; + } + else + { + return null; + } + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while fetching list of workItems form Cache: {Error}", ex.Message); + return null; + } + } + public async Task UpdatePlannedAndCompleteWorksInWorkItem(Guid id, double plannedWork = 0, double completedWork = 0, double todaysAssigned = 0) + { + try + { + var response = await _projectCache.UpdatePlannedAndCompleteWorksInWorkItemToCache(id, plannedWork, completedWork, todaysAssigned); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while updating planned work, completed work, and today's assigned work in workItems in Cache: {Error}", ex.Message); + } + } + public async Task DeleteWorkItemByIdAsync(Guid workItemId) + { + try + { + var response = await _projectCache.DeleteWorkItemByIdFromCacheAsync(workItemId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting work item from to Cache: {Error}", ex.Message); + + } + } + + + // ------------------------------------ Employee Profile Cache --------------------------------------- + public async Task AddApplicationRole(Guid employeeId, List 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> getPermissionIdsTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + + return await context.RolePermissionMappings + .Where(rp => roleIds.Contains(rp.ApplicationRoleId)) + .Select(p => p.FeaturePermissionId.ToString()) + .Distinct() + .ToListAsync(); + }); + + // 3. Prepare role IDs in parallel with the database query. + var newRoleIds = roleIds.Select(r => r.ToString()).ToList(); + + // 4. Await the database query result. + var newPermissionIds = await getPermissionIdsTask; + try + { + var response = await _employeeCache.AddApplicationRoleToCache(employeeId, newRoleIds, newPermissionIds); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while adding Application roleIds to Cache to employee {Employee}: {Error}", employeeId, ex.Message); + } + } + public async Task AddProjects(Guid employeeId, List projectIds) + { + try + { + var response = await _employeeCache.AddProjectsToCache(employeeId, projectIds); + return response; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while adding projectIds for employee {EmployeeId} to Cache: {Error}", employeeId, ex.Message); + return false; + } + } + public async Task?> GetProjects(Guid employeeId) + { + try + { + var response = await _employeeCache.GetProjectsFromCache(employeeId); + if (response.Count > 0) + { + return response; + } + return null; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while getting projectIds for employee {EmployeeId} from Cache: {Error}", employeeId, ex.Message); + return null; + } + } + public async Task?> GetPermissions(Guid employeeId) + { + try + { + var response = await _employeeCache.GetPermissionsFromCache(employeeId); + if (response.Count > 0) + { + return response; + } + return null; + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while getting permissionIds for employee {EmployeeId} from Cache: {Error}", employeeId, ex.Message); + return null; + } + } + public async Task ClearAllProjectIds(Guid employeeId) + { + try + { + var response = await _employeeCache.ClearAllProjectIdsFromCache(employeeId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting projectIds from Cache for employee {EmployeeId}: {Error}", employeeId, ex.Message); + } + } + public async Task ClearAllProjectIdsByRoleId(Guid roleId) + { + try + { + await _employeeCache.ClearAllProjectIdsByRoleIdFromCache(roleId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting projectIds from Cache for Application Role {RoleId}: {Error}", roleId, ex.Message); + } + } + public async Task ClearAllProjectIdsByPermissionId(Guid permissionId) + { + try + { + await _employeeCache.ClearAllProjectIdsByPermissionIdFromCache(permissionId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting projectIds from Cache for Permission {PermissionId}: {Error}", permissionId, ex.Message); + } + } + public async Task ClearAllPermissionIdsByEmployeeID(Guid employeeId) + { + try + { + var response = await _employeeCache.ClearAllPermissionIdsByEmployeeIDFromCache(employeeId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting permissionIds from Cache for employee {EmployeeId}: {Error}", employeeId, ex.Message); + } + } + public async Task ClearAllPermissionIdsByRoleId(Guid roleId) + { + try + { + var response = await _employeeCache.ClearAllPermissionIdsByRoleIdFromCache(roleId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting permissionIds from Cache for Application role {RoleId}: {Error}", roleId, ex.Message); + } + } + public async Task RemoveRoleId(Guid employeeId, Guid roleId) + { + try + { + var response = await _employeeCache.RemoveRoleIdFromCache(employeeId, roleId); + } + catch (Exception ex) + { + _logger.LogWarning("Error occured while deleting Application role {RoleId} from Cache for employee {EmployeeId}: {Error}", roleId, employeeId, ex.Message); + } + } + public async Task ClearAllEmployees() + { + try + { + var response = await _employeeCache.ClearAllEmployeesFromCache(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occured while deleting all employees from Cache"); + } + } + + + // ------------------------------------ Report Cache --------------------------------------- + + public async Task?> 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"); + } + } + } +} diff --git a/Marco.Pms.Services/Helpers/DirectoryHelper.cs b/Marco.Pms.Services/Helpers/DirectoryHelper.cs index bafa36f..cb169a1 100644 --- a/Marco.Pms.Services/Helpers/DirectoryHelper.cs +++ b/Marco.Pms.Services/Helpers/DirectoryHelper.cs @@ -1,6 +1,7 @@ using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Directory; using Marco.Pms.Model.Dtos.Directory; +using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; @@ -20,9 +21,6 @@ namespace Marco.Pms.Services.Helpers private readonly ILoggingService _logger; private readonly UserHelper _userHelper; private readonly PermissionServices _permissionServices; - private readonly Guid directoryAdmin; - private readonly Guid directoryManager; - private readonly Guid directoryUser; public DirectoryHelper(ApplicationDbContext context, ILoggingService logger, UserHelper userHelper, PermissionServices permissionServices) { @@ -30,13 +28,8 @@ namespace Marco.Pms.Services.Helpers _logger = logger; _userHelper = userHelper; _permissionServices = permissionServices; - directoryAdmin = Guid.Parse("4286a13b-bb40-4879-8c6d-18e9e393beda"); - directoryManager = Guid.Parse("62668630-13ce-4f52-a0f0-db38af2230c5"); - directoryUser = Guid.Parse("0f919170-92d4-4337-abd3-49b66fc871bb"); } - - public async Task> GetListOfContacts(string? search, bool active, ContactFilterDto? filterDto, Guid? projectId) { Guid tenantId = _userHelper.GetTenantId(); @@ -45,12 +38,12 @@ namespace Marco.Pms.Services.Helpers var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync(); List? employeeBuckets = await _context.EmployeeBucketMappings.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).ToListAsync(); List bucketIds = employeeBuckets.Select(c => c.BucketId).ToList(); - if (permissionIds.Contains(directoryAdmin)) + if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) { var buckets = await _context.Buckets.Where(b => b.TenantId == tenantId).ToListAsync(); bucketIds = buckets.Select(b => b.Id).ToList(); } - else if (permissionIds.Contains(directoryManager) || permissionIds.Contains(directoryUser)) + else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) || permissionIds.Contains(PermissionsMaster.DirectoryUser)) { var buckets = await _context.Buckets.Where(b => b.CreatedByID == LoggedInEmployee.Id).ToListAsync(); var createdBucketIds = buckets.Select(b => b.Id).ToList(); @@ -59,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.ErrorResponse("You don't have permission", "You don't have permission", 401); } @@ -199,17 +192,17 @@ namespace Marco.Pms.Services.Helpers var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync(); EmployeeBucketMapping? employeeBucket = null; - if (permissionIds.Contains(directoryAdmin)) + if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) { employeeBucket = employeeBuckets.FirstOrDefault(); } - else if (permissionIds.Contains(directoryManager) || permissionIds.Contains(directoryUser)) + else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) || permissionIds.Contains(PermissionsMaster.DirectoryUser)) { employeeBucket = employeeBuckets.FirstOrDefault(eb => eb.EmployeeId == LoggedInEmployee.Id); } 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.ErrorResponse("You don't have permission", "You don't have permission", 401); } @@ -483,12 +476,12 @@ namespace Marco.Pms.Services.Helpers var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync(); List? employeeBuckets = await _context.EmployeeBucketMappings.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).ToListAsync(); List bucketIds = employeeBuckets.Select(c => c.BucketId).ToList(); - if (permissionIds.Contains(directoryAdmin)) + if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) { var buckets = await _context.Buckets.Where(b => b.TenantId == tenantId).ToListAsync(); bucketIds = buckets.Select(b => b.Id).ToList(); } - else if (permissionIds.Contains(directoryManager) || permissionIds.Contains(directoryUser)) + else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) || permissionIds.Contains(PermissionsMaster.DirectoryUser)) { var buckets = await _context.Buckets.Where(b => b.CreatedByID == LoggedInEmployee.Id).ToListAsync(); var createdBucketIds = buckets.Select(b => b.Id).ToList(); @@ -497,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.ErrorResponse("You don't have permission", "You don't have permission", 401); } @@ -747,6 +740,7 @@ namespace Marco.Pms.Services.Helpers { Guid tenantId = _userHelper.GetTenantId(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var hasAdminPermission = await _permissionServices.HasPermission(PermissionsMaster.DirectoryAdmin, LoggedInEmployee.Id); if (id != Guid.Empty) { Contact? contact = await _context.Contacts.Include(c => c.ContactCategory).Include(c => c.CreatedBy).FirstOrDefaultAsync(c => c.Id == id && c.IsActive); @@ -806,11 +800,19 @@ namespace Marco.Pms.Services.Helpers } List? contactBuckets = await _context.ContactBucketMappings.Where(cb => cb.ContactId == contact.Id).ToListAsync(); List? employeeBuckets = await _context.EmployeeBucketMappings.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).ToListAsync(); - if (contactBuckets.Any() && employeeBuckets.Any()) + if (contactBuckets.Any() && (employeeBuckets.Any() || hasAdminPermission)) { List contactBucketIds = contactBuckets.Select(cb => cb.BucketId).ToList(); List employeeBucketIds = employeeBuckets.Select(eb => eb.BucketId).ToList(); - List? buckets = await _context.Buckets.Where(b => contactBucketIds.Contains(b.Id) && employeeBucketIds.Contains(b.Id)).ToListAsync(); + List? buckets = null; + if (hasAdminPermission) + { + buckets = await _context.Buckets.Where(b => contactBucketIds.Contains(b.Id)).ToListAsync(); + } + else + { + buckets = await _context.Buckets.Where(b => contactBucketIds.Contains(b.Id) && employeeBucketIds.Contains(b.Id)).ToListAsync(); + } List? bucketVMs = new List(); foreach (var bucket in buckets) { @@ -860,40 +862,101 @@ namespace Marco.Pms.Services.Helpers } public async Task> GetOrganizationList() { + // Step 1: Retrieve tenant and employee context Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var organizationList = await _context.Contacts.Where(c => c.TenantId == tenantId).Select(c => c.Organization).Distinct().ToListAsync(); - _logger.LogInfo("Employee {EmployeeId} fetched list of organizations in a tenant {TenantId}", LoggedInEmployee.Id, tenantId); - return ApiResponse.SuccessResponse(organizationList, $"{organizationList.Count} records of organization names fetched from contacts", 200); + _logger.LogInfo("GetOrganizationList called by EmployeeId: {EmployeeId} for TenantId: {TenantId}", + loggedInEmployee.Id, tenantId); + + // Step 2: Fetch distinct, non-empty organization names + var organizationList = await _context.Contacts + .Where(c => c.TenantId == tenantId && !string.IsNullOrWhiteSpace(c.Organization)) + .Select(c => c.Organization.Trim()) + .Distinct() + .ToListAsync(); + + _logger.LogInfo("EmployeeId: {EmployeeId} fetched {Count} organization names from TenantId: {TenantId}", + loggedInEmployee.Id, organizationList.Count, tenantId); + + // Step 3: Return success response + return ApiResponse.SuccessResponse( + organizationList, + $"{organizationList.Count} records of organization names fetched from contacts", + 200 + ); + } + public async Task> GetDesignationList() + { + // Step 1: Get tenant and logged-in employee details + Guid tenantId = _userHelper.GetTenantId(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + _logger.LogInfo("GetDesignationList called by EmployeeId: {EmployeeId} in TenantId: {TenantId}", + loggedInEmployee.Id, tenantId); + + // Step 2: Fetch distinct, non-null designations from contacts + var designationList = await _context.Contacts + .Where(c => c.TenantId == tenantId && !string.IsNullOrWhiteSpace(c.Designation)) + .Select(c => c.Designation.Trim()) + .Distinct() + .ToListAsync(); + + _logger.LogInfo("EmployeeId: {EmployeeId} fetched {Count} designations from TenantId: {TenantId}", + loggedInEmployee.Id, designationList.Count, tenantId); + + // Step 3: Return result + return ApiResponse.SuccessResponse( + designationList, + $"{designationList.Count} records of designation fetched from contacts", + 200 + ); } public async Task> DeleteContact(Guid id, bool active) { + // Step 1: Get tenant and logged-in employee info Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - if (id != Guid.Empty) + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + _logger.LogInfo("DeleteContact called by EmployeeId: {EmployeeId} for ContactId: {ContactId} with Active: {IsActive}", + loggedInEmployee.Id, id, active); + + // Step 2: Validate contact ID + if (id == Guid.Empty) { - Contact? contact = await _context.Contacts.FirstOrDefaultAsync(c => c.Id == id && c.TenantId == tenantId); - if (contact == null) - { - _logger.LogWarning("Employee with ID {LoggedInEmployeeId} tries to delete contact with ID {ContactId} is not found in database", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("Contact not found", "Contact not found", 404); - } - contact.IsActive = active; - - _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog - { - RefereanceId = contact.Id, - UpdatedById = LoggedInEmployee.Id, - UpdateAt = DateTime.UtcNow - }); - - await _context.SaveChangesAsync(); - _logger.LogInfo("Contact {ContactId} has been deleted by Employee {Employee}", id, LoggedInEmployee.Id); - return ApiResponse.SuccessResponse(new { }, "Contact is deleted Successfully", 200); + _logger.LogWarning("Empty contact ID received from EmployeeId: {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Contact ID is empty", "Contact ID is empty", 400); } - _logger.LogInfo("Employee ID {EmployeeId} sent an empty contact id", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("Contact ID is empty", "Contact ID is empty", 400); + + // Step 3: Check if contact exists under current tenant + var contact = await _context.Contacts + .FirstOrDefaultAsync(c => c.Id == id && c.TenantId == tenantId); + + if (contact == null) + { + _logger.LogWarning("EmployeeId {EmployeeId} attempted to delete non-existent contact Id: {ContactId}", loggedInEmployee.Id, id); + return ApiResponse.ErrorResponse("Contact not found", "Contact not found", 404); + } + + // Step 4: Soft delete or restore contact + contact.IsActive = active; + + // Step 5: Log the update in DirectoryUpdateLog + _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog + { + RefereanceId = contact.Id, + UpdatedById = loggedInEmployee.Id, + UpdateAt = DateTime.UtcNow + }); + + await _context.SaveChangesAsync(); + + string status = active ? "restored" : "deleted"; + _logger.LogInfo("Contact {ContactId} successfully {Status} by EmployeeId: {EmployeeId}", + contact.Id, status, loggedInEmployee.Id); + + // Step 6: Return success response + return ApiResponse.SuccessResponse(new { }, $"Contact {status} successfully", 200); } // -------------------------------- Contact Notes -------------------------------- @@ -919,9 +982,9 @@ namespace Marco.Pms.Services.Helpers } // --- Permission Checks --- - var hasAdminPermission = await _permissionServices.HasPermission(directoryAdmin, loggedInEmployee.Id); - var hasManagerPermission = await _permissionServices.HasPermission(directoryManager, loggedInEmployee.Id); - var hasUserPermission = await _permissionServices.HasPermission(directoryUser, loggedInEmployee.Id); + var hasAdminPermission = await _permissionServices.HasPermission(PermissionsMaster.DirectoryAdmin, loggedInEmployee.Id); + var hasManagerPermission = await _permissionServices.HasPermission(PermissionsMaster.DirectoryAdmin, loggedInEmployee.Id); + var hasUserPermission = await _permissionServices.HasPermission(PermissionsMaster.DirectoryUser, loggedInEmployee.Id); IQueryable notesQuery = _context.ContactNotes .Include(cn => cn.UpdatedBy) @@ -1093,7 +1156,7 @@ namespace Marco.Pms.Services.Helpers var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); if (noteDto != null && id == noteDto.Id) { - Contact? contact = await _context.Contacts.FirstOrDefaultAsync(c => c.Id == noteDto.ContactId && c.IsActive && c.TenantId == tenantId); + Contact? contact = await _context.Contacts.FirstOrDefaultAsync(c => c.Id == noteDto.ContactId && c.TenantId == tenantId); if (contact != null) { ContactNote? contactNote = await _context.ContactNotes.Include(cn => cn.Createdby).Include(cn => cn.Contact).FirstOrDefaultAsync(n => n.Id == noteDto.Id && n.ContactId == contact.Id && n.IsActive); @@ -1164,22 +1227,25 @@ namespace Marco.Pms.Services.Helpers List employeeBuckets = await _context.EmployeeBucketMappings.Where(b => b.EmployeeId == LoggedInEmployee.Id).ToListAsync(); var bucketIds = employeeBuckets.Select(b => b.BucketId).ToList(); - List employeeBucketVM = await _context.EmployeeBucketMappings.Where(b => bucketIds.Contains(b.BucketId)).ToListAsync(); + List bucketList = new List(); - if (permissionIds.Contains(directoryAdmin)) + 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(directoryManager) || permissionIds.Contains(directoryUser)) + else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) || permissionIds.Contains(PermissionsMaster.DirectoryUser)) { bucketList = await _context.Buckets.Include(b => b.CreatedBy).Where(b => bucketIds.Contains(b.Id) || b.CreatedByID == LoggedInEmployee.Id).ToListAsync(); } 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.ErrorResponse("You don't have permission", "You don't have permission", 401); } + List employeeBucketVM = await _context.EmployeeBucketMappings.Where(b => bucketIds.Contains(b.BucketId)).ToListAsync(); + List bucketVMs = new List(); if (bucketList.Any()) { @@ -1191,7 +1257,11 @@ namespace Marco.Pms.Services.Helpers var emplyeeIds = employeeBucketMappings.Select(eb => eb.EmployeeId).ToList(); List? 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); } @@ -1208,10 +1278,10 @@ namespace Marco.Pms.Services.Helpers { var assignedRoleIds = await _context.EmployeeRoleMappings.Where(r => r.EmployeeId == LoggedInEmployee.Id).Select(r => r.RoleId).ToListAsync(); var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync(); - var demo = !permissionIds.Contains(directoryUser); - if (!permissionIds.Contains(directoryAdmin) && !permissionIds.Contains(directoryManager) && !permissionIds.Contains(directoryUser)) + 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.ErrorResponse("You don't have permission", "You don't have permission", 401); } @@ -1266,15 +1336,15 @@ namespace Marco.Pms.Services.Helpers } Bucket? accessableBucket = null; - if (permissionIds.Contains(directoryAdmin)) + if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) { accessableBucket = bucket; } - else if (permissionIds.Contains(directoryManager) && bucketIds.Contains(id)) + else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && bucketIds.Contains(id)) { accessableBucket = bucket; } - else if (permissionIds.Contains(directoryUser)) + else if (permissionIds.Contains(PermissionsMaster.DirectoryUser)) { if (bucket.CreatedByID == LoggedInEmployee.Id) { @@ -1283,7 +1353,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.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 401); } @@ -1332,15 +1402,15 @@ namespace Marco.Pms.Services.Helpers var bucketIds = employeeBuckets.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).Select(eb => eb.BucketId).ToList(); var employeeBucketIds = employeeBuckets.Select(eb => eb.EmployeeId).ToList(); Bucket? accessableBucket = null; - if (permissionIds.Contains(directoryAdmin)) + if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) { accessableBucket = bucket; } - else if (permissionIds.Contains(directoryManager) && bucketIds.Contains(bucketId)) + else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && bucketIds.Contains(bucketId)) { accessableBucket = bucket; } - else if (permissionIds.Contains(directoryUser)) + else if (permissionIds.Contains(PermissionsMaster.DirectoryUser)) { if (bucket.CreatedByID == LoggedInEmployee.Id) { @@ -1349,7 +1419,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.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 401); } var employeeIds = await _context.Employees.Where(e => e.TenantId == tenantId && e.IsActive).Select(e => e.Id).ToListAsync(); @@ -1369,7 +1439,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) @@ -1403,7 +1473,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.SuccessResponse(bucketVM, "Details updated successfully", 200); } @@ -1433,15 +1503,15 @@ namespace Marco.Pms.Services.Helpers var bucketIds = employeeBuckets.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).Select(eb => eb.BucketId).ToList(); Bucket? accessableBucket = null; - if (permissionIds.Contains(directoryAdmin)) + if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) { accessableBucket = bucket; } - else if (permissionIds.Contains(directoryManager) && bucketIds.Contains(id)) + else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && bucketIds.Contains(id)) { accessableBucket = bucket; } - else if (permissionIds.Contains(directoryUser)) + else if (permissionIds.Contains(PermissionsMaster.DirectoryUser)) { if (bucket.CreatedByID == LoggedInEmployee.Id) { @@ -1450,7 +1520,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.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 401); } diff --git a/Marco.Pms.Services/Helpers/EmployeeHelper.cs b/Marco.Pms.Services/Helpers/EmployeeHelper.cs index 03184e5..09dcbe2 100644 --- a/Marco.Pms.Services/Helpers/EmployeeHelper.cs +++ b/Marco.Pms.Services/Helpers/EmployeeHelper.cs @@ -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(); } } @@ -76,14 +76,14 @@ namespace MarcoBMS.Services.Helpers try { List result = new List(); - if (ProjectId != null) + if (ProjectId.HasValue) { - result = await (from pa in _context.ProjectAllocations.Where(c => c.ProjectId == ProjectId) - join em in _context.Employees.Where(c => c.TenantId == TenentId && c.IsActive == true).Include(fp => fp.JobRole) // Include Feature - on pa.EmployeeId equals em.Id - select em.ToEmployeeVMFromEmployee() - ) + result = await _context.ProjectAllocations + .Include(pa => pa.Employee) + .ThenInclude(e => e!.JobRole) + .Where(c => c.ProjectId == ProjectId.Value && c.IsActive && c.Employee != null) + .Select(pa => pa.Employee!.ToEmployeeVMFromEmployee()) .ToListAsync(); } @@ -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(); } } diff --git a/Marco.Pms.Services/Helpers/GeneralHelper.cs b/Marco.Pms.Services/Helpers/GeneralHelper.cs new file mode 100644 index 0000000..c2f8fe4 --- /dev/null +++ b/Marco.Pms.Services/Helpers/GeneralHelper.cs @@ -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 _dbContextFactory; + private readonly ApplicationDbContext _context; // Keeping this for direct scoped context use where appropriate + private readonly ILoggingService _logger; + public GeneralHelper(IDbContextFactory 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> 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(); + foreach (var building in buildings) + { + double buildingPlanned = 0, buildingCompleted = 0; + var floorMongoList = new List(); + + foreach (var floor in floorsByBuildingId[building.Id]) // Fast lookup + { + double floorPlanned = 0, floorCompleted = 0; + var workAreaMongoList = new List(); + + 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; + } + + /// + /// 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. + /// + /// The ID of the work area. + /// A list of WorkItemMongoDB objects with calculated daily assignments. + public async Task> 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(); + } + } + } +} diff --git a/Marco.Pms.Services/Helpers/MasterHelper.cs b/Marco.Pms.Services/Helpers/MasterHelper.cs index 917206f..39bc86f 100644 --- a/Marco.Pms.Services/Helpers/MasterHelper.cs +++ b/Marco.Pms.Services/Helpers/MasterHelper.cs @@ -868,7 +868,7 @@ namespace Marco.Pms.Services.Helpers _logger.LogInfo("Contact tag master {ConatctTagId} updated successfully by employee {EmployeeId}", contactTagVm.Id, LoggedInEmployee.Id); return ApiResponse.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.ErrorResponse("Contact Tag master not found", "Contact tag master not found", 404); } _logger.LogWarning("Employee with ID {LoggedInEmployeeId} sended empty payload", LoggedInEmployee.Id); @@ -914,7 +914,7 @@ namespace Marco.Pms.Services.Helpers var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); // Step 2: Check permission to view master data - bool hasViewPermission = await _permissionService.HasPermission(View_Master, loggedInEmployee.Id); + bool hasViewPermission = await _permissionService.HasPermission(PermissionsMaster.ViewMasters, loggedInEmployee.Id); if (!hasViewPermission) { _logger.LogWarning("Access denied for employeeId: {EmployeeId}", loggedInEmployee.Id); @@ -944,7 +944,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.ErrorResponse("An error occurred", "Unable to fetch work status list", 500); } } @@ -959,7 +959,7 @@ namespace Marco.Pms.Services.Helpers var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); // Step 2: Check if user has permission to manage master data - var hasManageMasterPermission = await _permissionService.HasPermission(Manage_Master, loggedInEmployee.Id); + var hasManageMasterPermission = await _permissionService.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasManageMasterPermission) { _logger.LogWarning("Access denied for employeeId: {EmployeeId}", loggedInEmployee.Id); @@ -993,7 +993,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.ErrorResponse("An error occurred", "Unable to create work status", 500); } } @@ -1015,7 +1015,7 @@ namespace Marco.Pms.Services.Helpers var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); // Step 3: Check permissions - var hasManageMasterPermission = await _permissionService.HasPermission(Manage_Master, loggedInEmployee.Id); + var hasManageMasterPermission = await _permissionService.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasManageMasterPermission) { _logger.LogWarning("Access denied. EmployeeId: {EmployeeId} does not have Manage Master permission.", loggedInEmployee.Id); @@ -1053,7 +1053,7 @@ namespace Marco.Pms.Services.Helpers } catch (Exception ex) { - _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.ErrorResponse("An error occurred", "Unable to update the work status at this time", 500); } } @@ -1068,7 +1068,7 @@ namespace Marco.Pms.Services.Helpers var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); // Step 2: Check permission to manage master data - var hasManageMasterPermission = await _permissionService.HasPermission(Manage_Master, loggedInEmployee.Id); + var hasManageMasterPermission = await _permissionService.HasPermission(PermissionsMaster.ManageMasters, loggedInEmployee.Id); if (!hasManageMasterPermission) { _logger.LogWarning("Delete denied. EmployeeId: {EmployeeId} lacks Manage_Master permission.", loggedInEmployee.Id); @@ -1108,7 +1108,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.ErrorResponse("An error occurred", "Unable to delete work status", 500); } } diff --git a/Marco.Pms.Services/Helpers/ProjectHelper.cs b/Marco.Pms.Services/Helpers/ProjectHelper.cs deleted file mode 100644 index f1b688e..0000000 --- a/Marco.Pms.Services/Helpers/ProjectHelper.cs +++ /dev/null @@ -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> 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; - } - } - - - - } -} diff --git a/Marco.Pms.Services/Helpers/ProjectsHelper.cs b/Marco.Pms.Services/Helpers/ProjectsHelper.cs deleted file mode 100644 index 8ccbc85..0000000 --- a/Marco.Pms.Services/Helpers/ProjectsHelper.cs +++ /dev/null @@ -1,96 +0,0 @@ -using Marco.Pms.DataAccess.Data; -using Marco.Pms.Model.Employees; -using Marco.Pms.Model.Entitlements; -using Marco.Pms.Model.Projects; -using Marco.Pms.Model.Utilities; -using Marco.Pms.Model.ViewModels.Projects; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using ModelServices.Helpers; - -namespace MarcoBMS.Services.Helpers -{ - public class ProjectsHelper - { - private readonly ApplicationDbContext _context; - private readonly RolesHelper _rolesHelper; - - - public ProjectsHelper(ApplicationDbContext context, RolesHelper rolesHelper) - { - _context = context; - _rolesHelper = rolesHelper; - } - - public async Task> GetAllProjectByTanentID(Guid tanentID) - { - List alloc = await _context.Projects.Where(c => c.TenantId == tanentID).ToListAsync(); - return alloc; - } - - public async Task> GetProjectByEmployeeID(Guid employeeID) - { - List alloc = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeID && c.IsActive == true).Include(c => c.Project).ToListAsync(); - return alloc; - } - - public async Task> 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> GetMyProjects(Guid tenantId, Employee LoggedInEmployee) - { - List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeID(LoggedInEmployee.Id); - - string[] projectsId = []; - List projects = new List(); - - // Define a common queryable base for projects - IQueryable projectQuery = _context.Projects.Where(c => c.TenantId == tenantId); - - // 2. Optimized Project Retrieval Logic - // User with permission 'manage project' can see all projects - if (featurePermission != null && featurePermission.Exists(c => c.Id.ToString() == "172fc9b6-755b-4f62-ab26-55c34a330614")) - { - // If GetAllProjectByTanentID is already optimized and directly returns IQueryable or - // directly executes with ToListAsync(), keep it. - // If it does more complex logic or extra trips, consider inlining here. - projects = await projectQuery.ToListAsync(); // Directly query the context - } - else - { - // 3. Efficiently get project allocations and then filter projects - // Load allocations only once - var allocation = await GetProjectByEmployeeID(LoggedInEmployee.Id); - - // If there are no allocations, return an empty list early - if (allocation == null || !allocation.Any()) - { - return new List(); - } - - // Use LINQ's Contains for efficient filtering by ProjectId - var projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); // Get distinct Guids - - // Filter projects based on the retrieved ProjectIds - projects = await projectQuery.Where(c => projectIds.Contains(c.Id)).ToListAsync(); - } - - return projects; - } - - } -} diff --git a/Marco.Pms.Services/Helpers/ReportHelper.cs b/Marco.Pms.Services/Helpers/ReportHelper.cs new file mode 100644 index 0000000..35dcf8b --- /dev/null +++ b/Marco.Pms.Services/Helpers/ReportHelper.cs @@ -0,0 +1,365 @@ +using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.Dtos.Attendance; +using Marco.Pms.Model.Employees; +using Marco.Pms.Model.Mail; +using Marco.Pms.Model.MongoDBModels; +using Marco.Pms.Model.Utilities; +using Marco.Pms.Model.ViewModels.Report; +using MarcoBMS.Services.Service; +using Microsoft.EntityFrameworkCore; +using System.Globalization; + +namespace Marco.Pms.Services.Helpers +{ + public class ReportHelper + { + private readonly ApplicationDbContext _context; + private readonly IEmailSender _emailSender; + private readonly ILoggingService _logger; + private readonly CacheUpdateHelper _cache; + public ReportHelper(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, CacheUpdateHelper cache) + { + _context = context; + _emailSender = emailSender; + _logger = logger; + _cache = cache; + } + public async Task GetDailyProjectReport(Guid projectId, Guid tenantId) + { + // await _cache.GetBuildingAndFloorByWorkAreaId(); + DateTime reportDate = DateTime.UtcNow.AddDays(-1).Date; + var project = await _cache.GetProjectDetailsWithBuildings(projectId); + if (project == null) + { + var projectSQL = await _context.Projects + .AsNoTracking() + .FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId); + if (projectSQL != null) + { + project = new ProjectMongoDB + { + Id = projectSQL.Id.ToString(), + Name = projectSQL.Name, + ShortName = projectSQL.ShortName, + ProjectAddress = projectSQL.ProjectAddress, + ContactPerson = projectSQL.ContactPerson + }; + await _cache.AddProjectDetails(projectSQL); + } + } + if (project != null) + { + + var statisticReport = new ProjectStatisticReport + { + Date = reportDate, + ProjectName = project.Name ?? "", + TimeStamp = DateTime.Now.ToString("dd-MMM-yyyy HH:mm:ss", CultureInfo.InvariantCulture) + }; + + // Preload relevant data + var projectAllocations = await _context.ProjectAllocations + .Include(p => p.Employee) + .Where(p => p.ProjectId == projectId && p.IsActive) + .ToListAsync(); + + var assignedEmployeeIds = projectAllocations.Select(p => p.EmployeeId).ToHashSet(); + + var attendances = await _context.Attendes + .AsNoTracking() + .Where(a => a.ProjectID == projectId && a.InTime != null && a.InTime.Value.Date == reportDate) + .ToListAsync(); + + var checkedInEmployeeIds = attendances.Select(a => a.EmployeeID).Distinct().ToHashSet(); + var checkoutPendingIds = attendances.Where(a => a.OutTime == null).Select(a => a.EmployeeID).Distinct().ToHashSet(); + var regularizationIds = attendances + .Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE) + .Select(a => a.EmployeeID).Distinct().ToHashSet(); + + // Preload buildings, floors, areas + List? buildings = null; + List? floors = null; + List? areas = null; + List? workItems = null; + + // Fetch Buildings + buildings = project.Buildings + .Select(b => new BuildingMongoDBVM + { + Id = b.Id, + ProjectId = b.ProjectId, + BuildingName = b.BuildingName, + Description = b.Description + }).ToList(); + if (!buildings.Any()) + { + buildings = await _context.Buildings + .Where(b => b.ProjectId == projectId) + .Select(b => new BuildingMongoDBVM + { + Id = b.Id.ToString(), + ProjectId = b.ProjectId.ToString(), + BuildingName = b.Name, + Description = b.Description + }) + .ToListAsync(); + } + + // fetch Floors + floors = project.Buildings + .SelectMany(b => b.Floors.Select(f => new FloorMongoDBVM + { + Id = f.Id.ToString(), + BuildingId = f.BuildingId, + FloorName = f.FloorName + })).ToList(); + if (!floors.Any()) + { + var buildingIds = buildings.Select(b => Guid.Parse(b.Id)).ToList(); + floors = await _context.Floor + .Where(f => buildingIds.Contains(f.BuildingId)) + .Select(f => new FloorMongoDBVM + { + Id = f.Id.ToString(), + BuildingId = f.BuildingId.ToString(), + FloorName = f.FloorName + }) + .ToListAsync(); + } + + // fetch Work Areas + areas = project.Buildings + .SelectMany(b => b.Floors) + .SelectMany(f => f.WorkAreas).ToList(); + if (!areas.Any()) + { + var floorIds = floors.Select(f => Guid.Parse(f.Id)).ToList(); + areas = await _context.WorkAreas + .Where(a => floorIds.Contains(a.FloorId)) + .Select(wa => new WorkAreaMongoDB + { + Id = wa.Id.ToString(), + FloorId = wa.FloorId.ToString(), + AreaName = wa.AreaName, + }) + .ToListAsync(); + } + + var areaIds = areas.Select(a => Guid.Parse(a.Id)).ToList(); + + // fetch Work Items + workItems = await _cache.GetWorkItemsByWorkAreaIds(areaIds); + if (workItems == null || !workItems.Any()) + { + workItems = await _context.WorkItems + .Include(w => w.ActivityMaster) + .Where(w => areaIds.Contains(w.WorkAreaId)) + .Select(wi => new WorkItemMongoDB + { + Id = wi.Id.ToString(), + WorkAreaId = wi.WorkAreaId.ToString(), + PlannedWork = wi.PlannedWork, + CompletedWork = wi.CompletedWork, + Description = wi.Description, + TaskDate = wi.TaskDate, + ActivityMaster = new ActivityMasterMongoDB + { + ActivityName = wi.ActivityMaster != null ? wi.ActivityMaster.ActivityName : null, + UnitOfMeasurement = wi.ActivityMaster != null ? wi.ActivityMaster.UnitOfMeasurement : null + } + }) + .ToListAsync(); + } + + var itemIds = workItems.Select(i => Guid.Parse(i.Id)).ToList(); + + var tasks = await _context.TaskAllocations + .Where(t => itemIds.Contains(t.WorkItemId)) + .ToListAsync(); + + var taskIds = tasks.Select(t => t.Id).ToList(); + + var taskMembers = await _context.TaskMembers + .Include(m => m.Employee) + .Where(m => taskIds.Contains(m.TaskAllocationId)) + .ToListAsync(); + + // Aggregate data + double totalPlannedWork = workItems.Sum(w => w.PlannedWork); + double totalCompletedWork = workItems.Sum(w => w.CompletedWork); + + var todayAssignedTasks = tasks.Where(t => t.AssignmentDate.Date == reportDate).ToList(); + var reportPending = tasks.Where(t => t.ReportedDate == null).ToList(); + + double totalPlannedTask = todayAssignedTasks.Sum(t => t.PlannedTask); + double totalCompletedTask = todayAssignedTasks.Sum(t => t.CompletedTask); + var jobRoleIds = projectAllocations.Select(pa => pa.JobRoleId).ToList(); + var jobRoles = await _context.JobRoles + .Where(j => j.TenantId == tenantId && jobRoleIds.Contains(j.Id)) + .ToListAsync(); + + // Team on site + var teamOnSite = jobRoles + .Select(role => + { + var count = projectAllocations.Count(p => p.JobRoleId == role.Id && checkedInEmployeeIds.Contains(p.EmployeeId)); + return new TeamOnSite { RoleName = role.Name, NumberofEmployees = count }; + }) + .OrderByDescending(t => t.NumberofEmployees) + .ToList(); + + // Task details + var performedTasks = todayAssignedTasks.Select(task => + { + var workItem = workItems.FirstOrDefault(w => w.Id == task.WorkItemId.ToString()); + var area = areas.FirstOrDefault(a => a.Id == workItem?.WorkAreaId); + var floor = floors.FirstOrDefault(f => f.Id == area?.FloorId); + var building = buildings.FirstOrDefault(b => b.Id == floor?.BuildingId); + + string activityName = workItem?.ActivityMaster?.ActivityName ?? ""; + string location = $"{building?.BuildingName} > {floor?.FloorName}
{floor?.FloorName}-{area?.AreaName}"; + double pending = (workItem?.PlannedWork ?? 0) - (workItem?.CompletedWork ?? 0); + + var taskTeam = taskMembers + .Where(m => m.TaskAllocationId == task.Id) + .Select(m => + { + string name = $"{m.Employee?.FirstName ?? ""} {m.Employee?.LastName ?? ""}"; + var role = jobRoles.FirstOrDefault(r => r.Id == m.Employee?.JobRoleId); + return new TaskTeam { Name = name, RoleName = role?.Name ?? "" }; + }) + .ToList(); + + return new PerformedTask + { + Activity = activityName, + Location = location, + AssignedToday = task.PlannedTask, + CompletedToday = task.CompletedTask, + Pending = pending, + Comment = task.Description, + Team = taskTeam + }; + }).ToList(); + + // Attendance details + var performedAttendance = attendances.Select(att => + { + var alloc = projectAllocations.FirstOrDefault(p => p.EmployeeId == att.EmployeeID); + var role = jobRoles.FirstOrDefault(r => r.Id == alloc?.JobRoleId); + string name = $"{alloc?.Employee?.FirstName ?? ""} {alloc?.Employee?.LastName ?? ""}"; + + return new PerformedAttendance + { + Name = name, + RoleName = role?.Name ?? "", + InTime = att.InTime ?? DateTime.UtcNow, + OutTime = att.OutTime, + Comment = att.Comment + }; + }).ToList(); + + // Fill report + statisticReport.TodaysAttendances = checkedInEmployeeIds.Count; + statisticReport.TotalEmployees = assignedEmployeeIds.Count; + statisticReport.RegularizationPending = regularizationIds.Count; + statisticReport.CheckoutPending = checkoutPendingIds.Count; + statisticReport.TotalPlannedWork = totalPlannedWork; + statisticReport.TotalCompletedWork = totalCompletedWork; + statisticReport.TotalPlannedTask = totalPlannedTask; + statisticReport.TotalCompletedTask = totalCompletedTask; + statisticReport.CompletionStatus = totalPlannedWork > 0 ? totalCompletedWork / totalPlannedWork : 0; + statisticReport.TodaysAssignTasks = todayAssignedTasks.Count; + statisticReport.ReportPending = reportPending.Count; + statisticReport.TeamOnSite = teamOnSite; + statisticReport.PerformedTasks = performedTasks; + statisticReport.PerformedAttendance = performedAttendance; + return statisticReport; + } + return null; + } + /// + /// Retrieves project statistics for a given project ID and sends an email report. + /// + /// The ID of the project. + /// The email address of the recipient. + /// An ApiResponse indicating the success or failure of retrieving statistics and sending the email. + public async Task> GetProjectStatistics(Guid projectId, List 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.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.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.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.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.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.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.ErrorResponse("An unexpected error occurred.", "An unexpected error occurred.", 500); + } + } + } +} diff --git a/Marco.Pms.Services/Helpers/RolesHelper.cs b/Marco.Pms.Services/Helpers/RolesHelper.cs index b571d03..ef9f824 100644 --- a/Marco.Pms.Services/Helpers/RolesHelper.cs +++ b/Marco.Pms.Services/Helpers/RolesHelper.cs @@ -2,37 +2,94 @@ 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 _dbContextFactory; private readonly ApplicationDbContext _context; - public RolesHelper(ApplicationDbContext context) + private readonly CacheUpdateHelper _cache; + private readonly ILoggingService _logger; + public RolesHelper(ApplicationDbContext context, CacheUpdateHelper cache, ILoggingService logger, IDbContextFactory dbContextFactory) { _context = context; + _cache = cache; + _logger = logger; + _dbContextFactory = dbContextFactory; } - public async Task> GetFeaturePermissionByEmployeeID(Guid EmployeeID) + /// + /// Retrieves a unique list of enabled feature permissions for a given employee. + /// This method is optimized to use a single, composed database query. + /// + /// The ID of the employee. + /// A distinct list of FeaturePermission objects the employee is granted. + public async Task> GetFeaturePermissionByEmployeeId(Guid EmployeeId) { - List roleMappings = await _context.EmployeeRoleMappings.Where(c => c.EmployeeId == EmployeeID && c.IsEnabled == true).Select(c => c.RoleId).ToListAsync(); + _logger.LogInfo("Fetching feature permissions for EmployeeId: {EmployeeId}", EmployeeId); - // _context.RolePermissionMappings + try + { + // --- Step 1: Define the subquery using the main thread's context --- + // This is safe because the query is not executed yet. + var employeeRoleIdsQuery = _context.EmployeeRoleMappings + .Where(erm => erm.EmployeeId == EmployeeId && erm.IsEnabled) + .Select(erm => erm.RoleId); - var result = await (from rpm in _context.RolePermissionMappings - join fp in _context.FeaturePermissions.Where(c => c.IsEnabled == true).Include(fp => fp.Feature) // Include Feature - on rpm.FeaturePermissionId equals fp.Id - where roleMappings.Contains(rpm.ApplicationRoleId) - select fp) - .ToListAsync(); + // --- 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(); - return result; + // Now, re-create and execute the query using this new, isolated context. + var roleIds = await contextForCache.EmployeeRoleMappings + .Where(erm => erm.EmployeeId == EmployeeId && erm.IsEnabled) + .Select(erm => erm.RoleId) + .ToListAsync(); - // return null; + if (roleIds.Any()) + { + // The cache service might also need its own context, or you can pass the data directly. + // Assuming AddApplicationRole takes the data, not a context. + await _cache.AddApplicationRole(EmployeeId, roleIds); + _logger.LogInfo("Successfully queued cache update for EmployeeId: {EmployeeId}", EmployeeId); + } + } + catch (Exception ex) + { + _logger.LogWarning("Background cache update failed for EmployeeId {EmployeeId} : {Error}", EmployeeId, ex.Message); + } + }); + + // --- Step 3: Execute the main query on the main thread using its original context --- + // This is now safe because the background task is using a different DbContext instance. + var permissions = await ( + from rpm in _context.RolePermissionMappings + join fp in _context.FeaturePermissions.Include(f => f.Feature) + on rpm.FeaturePermissionId equals fp.Id + where employeeRoleIdsQuery.Contains(rpm.ApplicationRoleId) && fp.IsEnabled == true + select fp) + .Distinct() + .ToListAsync(); + + _logger.LogInfo("Successfully retrieved {PermissionCount} unique permissions for EmployeeId: {EmployeeId}", permissions.Count, EmployeeId); + return permissions; + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while fetching permissions for EmployeeId {EmployeeId}", EmployeeId); + return new List(); + } } - public async Task> GetFeaturePermissionByRoleID(Guid roleId) + public async Task> GetFeaturePermissionByRoleID1(Guid roleId) { List roleMappings = await _context.RolePermissionMappings.Where(c => c.ApplicationRoleId == roleId).Select(c => c.ApplicationRoleId).ToListAsync(); @@ -49,5 +106,49 @@ namespace MarcoBMS.Services.Helpers // return null; } + /// + /// 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. + /// + /// The ID of the role. + /// A distinct list of FeaturePermission objects granted to the role. + public async Task> 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(); + } + } + } } diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs new file mode 100644 index 0000000..bf3777c --- /dev/null +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -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(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap() + .ForMember( + dest => dest.Id, + // Explicitly and safely convert string Id to Guid Id + opt => opt.MapFrom(src => new Guid(src.Id)) + ); + + CreateMap() + .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(); + CreateMap(); + CreateMap(); + CreateMap() + .ForMember( + dest => dest.EmployeeId, + // Explicitly and safely convert string ProjectStatusId to Guid ProjectStatusId + opt => opt.MapFrom(src => src.EmpID)); + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap() + .ForMember( + dest => dest.Description, + opt => opt.MapFrom(src => src.Comment)); + #endregion + + #region ======================================================= Projects ======================================================= + CreateMap(); + #endregion + } + } +} diff --git a/Marco.Pms.Services/Marco.Pms.Services.csproj b/Marco.Pms.Services/Marco.Pms.Services.csproj index 7bef32f..2feafaf 100644 --- a/Marco.Pms.Services/Marco.Pms.Services.csproj +++ b/Marco.Pms.Services/Marco.Pms.Services.csproj @@ -11,6 +11,7 @@ + @@ -44,6 +45,7 @@ + diff --git a/Marco.Pms.Services/Middleware/LoggingMiddleware.cs b/Marco.Pms.Services/Middleware/LoggingMiddleware.cs index dd10d7d..c57f05c 100644 --- a/Marco.Pms.Services/Middleware/LoggingMiddleware.cs +++ b/Marco.Pms.Services/Middleware/LoggingMiddleware.cs @@ -24,7 +24,7 @@ namespace MarcoBMS.Services.Middleware var response = context.Response; var request = context.Request; var tenantId = context.User.FindFirst("TenantId")?.Value; - + string origin = request.Headers["Origin"].FirstOrDefault() ?? ""; using (LogContext.PushProperty("TenantId", tenantId)) using (LogContext.PushProperty("TraceId", context.TraceIdentifier)) @@ -33,6 +33,8 @@ namespace MarcoBMS.Services.Middleware using (LogContext.PushProperty("Timestamp", DateTime.UtcNow)) using (LogContext.PushProperty("IpAddress", context.Connection.RemoteIpAddress?.ToString())) using (LogContext.PushProperty("RequestPath", request.Path)) + using (LogContext.PushProperty("Origin", origin)) + try diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index 17eb5c7..5549702 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -1,4 +1,4 @@ -using System.Text; +using Marco.Pms.CacheHelper; using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Authentication; using Marco.Pms.Model.Entitlements; @@ -6,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; @@ -15,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 - .WriteTo.MongoDB( + 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("AllowedOrigins")?.Split(','); -var allowedMethods = corsSettings.GetValue("AllowedMethods")?.Split(','); -var allowedHeaders = corsSettings.GetValue("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() @@ -63,89 +52,51 @@ builder.Services.AddCors(options => .AllowAnyHeader() .WithExposedHeaders("Authorization"); }); -}); -// Add services to the container. -builder.Services.AddHostedService(); + // A stricter policy for production (loaded from config) + var corsSettings = builder.Configuration.GetSection("Cors"); + var allowedOrigins = corsSettings.GetValue("AllowedOrigins")?.Split(',') ?? Array.Empty(); + 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(); +#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(builder.Configuration.GetSection("SmtpSettings")); -builder.Services.AddTransient(); - -builder.Services.Configure(builder.Configuration.GetSection("AWS")); // For uploading images to aws s3 -builder.Services.AddTransient(); - -builder.Services.AddIdentity().AddEntityFrameworkStores().AddDefaultTokenProviders(); - - -string? connString = builder.Configuration.GetConnectionString("DefaultConnectionString"); +// This single call correctly registers BOTH the DbContext (scoped) AND the IDbContextFactory (singleton). +builder.Services.AddDbContextFactory(options => + options.UseMySql(connString, ServerVersion.AutoDetect(connString))); builder.Services.AddDbContext(options => -{ - options.UseMySql(connString, ServerVersion.AutoDetect(connString)); -}); + options.UseMySql(connString, ServerVersion.AutoDetect(connString))); +builder.Services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); +#endregion -builder.Services.AddMemoryCache(); - - -//builder.Services.AddScoped(); -//builder.Services.AddScoped(); -//builder.Services.AddScoped(); -//builder.Services.AddScoped(); -//builder.Services.AddScoped(); -//builder.Services.AddScoped(); - -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddSingleton(); - - -builder.Services.AddHttpContextAccessor(); - +#region Authentication (JWT) var jwtSettings = builder.Configuration.GetSection("Jwt").Get() ?? 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; @@ -163,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() + } + }); +}); +#endregion + +#region Application-Specific Services +// Configuration-bound services +builder.Services.Configure(builder.Configuration.GetSection("SmtpSettings")); +builder.Services.Configure(builder.Configuration.GetSection("AWS")); + +// Transient services (lightweight, created each time) +builder.Services.AddTransient(); +builder.Services.AddTransient(); + +// Scoped services (one instance per HTTP request) +#region Customs Services +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +#endregion + +#region Helpers +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +#endregion + +#region Cache Services +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +#endregion + +// Singleton services (one instance for the app's lifetime) +builder.Services.AddSingleton(); +#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(); app.UseMiddleware(); app.UseMiddleware(); +#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.UseAuthorization(); -app.MapHub("/hubs/marco"); +#region Endpoint Routing (Run Last) +// These map incoming requests to the correct controller actions or SignalR hubs. app.MapControllers(); +app.MapHub("/hubs/marco"); +#endregion -app.Run(); +#endregion + +app.Run(); \ No newline at end of file diff --git a/Marco.Pms.Services/Service/EmailSender.cs b/Marco.Pms.Services/Service/EmailSender.cs index 568510a..4d66a4f 100644 --- a/Marco.Pms.Services/Service/EmailSender.cs +++ b/Marco.Pms.Services/Service/EmailSender.cs @@ -150,18 +150,24 @@ namespace MarcoBMS.Services.Service emailBody = emailBody.Replace("{{TEAM_ON_SITE}}", BuildTeamOnSiteHtml(report.TeamOnSite)); emailBody = emailBody.Replace("{{PERFORMED_TASK}}", BuildPerformedTaskHtml(report.PerformedTasks, report.Date)); emailBody = emailBody.Replace("{{PERFORMED_ATTENDANCE}}", BuildPerformedAttendanceHtml(report.PerformedAttendance)); - var subjectReplacements = new Dictionary + if (!string.IsNullOrWhiteSpace(subject)) { - {"DATE", date }, - {"PROJECT_NAME", report.ProjectName} - }; - foreach (var item in subjectReplacements) - { - subject = subject.Replace($"{{{{{item.Key}}}}}", item.Value); + var subjectReplacements = new Dictionary + { + {"DATE", date }, + {"PROJECT_NAME", report.ProjectName} + }; + foreach (var item in subjectReplacements) + { + subject = subject.Replace($"{{{{{item.Key}}}}}", item.Value); + } + string env = _configuration["environment:Title"] ?? string.Empty; + subject = CheckSubject(subject); + } + if (toEmails.Count > 0) + { + await SendEmailAsync(toEmails, subject, emailBody); } - string env = _configuration["environment:Title"] ?? string.Empty; - subject = CheckSubject(subject); - await SendEmailAsync(toEmails, subject, emailBody); return emailBody; } public async Task SendOTP(List toEmails, string emailBody, string name, string otp, string subject) diff --git a/Marco.Pms.Services/Service/ILoggingService.cs b/Marco.Pms.Services/Service/ILoggingService.cs index 39dbb00..6d795cd 100644 --- a/Marco.Pms.Services/Service/ILoggingService.cs +++ b/Marco.Pms.Services/Service/ILoggingService.cs @@ -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); } } diff --git a/Marco.Pms.Services/Service/LoggingServices.cs b/Marco.Pms.Services/Service/LoggingServices.cs index 4328a2a..751f22c 100644 --- a/Marco.Pms.Services/Service/LoggingServices.cs +++ b/Marco.Pms.Services/Service/LoggingServices.cs @@ -11,17 +11,18 @@ 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); } - } + } public void LogInfo(string? message, params object[]? args) { @@ -35,6 +36,18 @@ namespace MarcoBMS.Services.Service _logger.LogInformation(message); } } + public void LogDebug(string? message, params object[]? args) + { + using (LogContext.PushProperty("LogLevel", "Information")) + if (args != null) + { + _logger.LogDebug(message, args); + } + else + { + _logger.LogDebug(message); + } + } public void LogWarning(string? message, params object[]? args) { @@ -49,6 +62,5 @@ namespace MarcoBMS.Services.Service } } } - } diff --git a/Marco.Pms.Services/Service/PermissionServices.cs b/Marco.Pms.Services/Service/PermissionServices.cs index f3ddb58..9758a5f 100644 --- a/Marco.Pms.Services/Service/PermissionServices.cs +++ b/Marco.Pms.Services/Service/PermissionServices.cs @@ -1,7 +1,7 @@ using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Entitlements; -using Marco.Pms.Model.Projects; +using Marco.Pms.Services.Helpers; using MarcoBMS.Services.Helpers; using Microsoft.EntityFrameworkCore; @@ -11,42 +11,50 @@ namespace Marco.Pms.Services.Service { private readonly ApplicationDbContext _context; private readonly RolesHelper _rolesHelper; - private readonly ProjectsHelper _projectsHelper; - public PermissionServices(ApplicationDbContext context, RolesHelper rolesHelper, ProjectsHelper projectsHelper) + private readonly CacheUpdateHelper _cache; + public PermissionServices(ApplicationDbContext context, RolesHelper rolesHelper, CacheUpdateHelper cache) { _context = context; _rolesHelper = rolesHelper; - _projectsHelper = projectsHelper; + _cache = cache; } public async Task HasPermission(Guid featurePermissionId, Guid employeeId) { - var hasPermission = await _context.EmployeeRoleMappings - .Where(er => er.EmployeeId == employeeId) - .Select(er => er.RoleId) - .Distinct() - .AnyAsync(roleId => _context.RolePermissionMappings - .Any(rp => rp.FeaturePermissionId == featurePermissionId && rp.ApplicationRoleId == roleId)); + var featurePermissionIds = await _cache.GetPermissions(employeeId); + if (featurePermissionIds == null) + { + List featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeId(employeeId); + featurePermissionIds = featurePermission.Select(fp => fp.Id).ToList(); + } + var hasPermission = featurePermissionIds.Contains(featurePermissionId); return hasPermission; } - public async Task HasProjectPermission(Employee emp, string projectId) + public async Task HasProjectPermission(Employee LoggedInEmployee, Guid projectId) { - List 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 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 + { + var allocation = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.IsActive).ToListAsync(); + if (!allocation.Any()) + { + return false; + } + projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); + } + await _cache.AddProjects(LoggedInEmployee.Id, projectIds); } - else - { - List allocation = await _projectsHelper.GetProjectByEmployeeID(emp.Id); - projectsId = allocation.Select(c => c.ProjectId.ToString()).ToArray(); - } - bool response = projectsId.Contains(projectId); - return response; + return projectIds.Contains(projectId); } } } diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs new file mode 100644 index 0000000..9406ec9 --- /dev/null +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -0,0 +1,1751 @@ +using AutoMapper; +using AutoMapper.QueryableExtensions; +using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.Activities; +using Marco.Pms.Model.Dtos.Project; +using Marco.Pms.Model.Employees; +using Marco.Pms.Model.Entitlements; +using Marco.Pms.Model.MongoDBModels; +using Marco.Pms.Model.Projects; +using Marco.Pms.Model.Utilities; +using Marco.Pms.Model.ViewModels.Employee; +using Marco.Pms.Model.ViewModels.Projects; +using Marco.Pms.Services.Helpers; +using Marco.Pms.Services.Service.ServiceInterfaces; +using MarcoBMS.Services.Service; +using Microsoft.CodeAnalysis; +using Microsoft.EntityFrameworkCore; +using Project = Marco.Pms.Model.Projects.Project; + +namespace Marco.Pms.Services.Service +{ + public class ProjectServices : IProjectServices + { + private readonly IDbContextFactory _dbContextFactory; + private readonly ApplicationDbContext _context; // Keeping this for direct scoped context use where appropriate + private readonly ILoggingService _logger; + private readonly PermissionServices _permission; + private readonly CacheUpdateHelper _cache; + private readonly IMapper _mapper; + private readonly GeneralHelper _generalHelper; + public ProjectServices( + IDbContextFactory dbContextFactory, + ApplicationDbContext context, + ILoggingService logger, + PermissionServices permission, + CacheUpdateHelper cache, + IMapper mapper, + GeneralHelper generalHelper) + { + _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); + _context = context ?? throw new ArgumentNullException(nameof(context)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _permission = permission ?? throw new ArgumentNullException(nameof(permission)); + _cache = cache ?? throw new ArgumentNullException(nameof(cache)); + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + _generalHelper = generalHelper ?? throw new ArgumentNullException(nameof(generalHelper)); + } + #region =================================================================== Project Get APIs =================================================================== + + public async Task> GetAllProjectsBasicAsync(Guid tenantId, Employee loggedInEmployee) + { + try + { + // Step 1: Verify the current user + if (loggedInEmployee == null) + { + return ApiResponse.ErrorResponse("Unauthorized", "User could not be identified.", 401); + } + + _logger.LogInfo("Basic project list requested by EmployeeId {EmployeeId}", loggedInEmployee.Id); + + // Step 2: Get the list of project IDs the user has access to + List accessibleProjectIds = await GetMyProjects(tenantId, loggedInEmployee); + + if (accessibleProjectIds == null || !accessibleProjectIds.Any()) + { + _logger.LogInfo("No accessible projects found for EmployeeId {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.SuccessResponse(new List(), "0 records of project fetchd successfully", 200); + } + + // Step 3: Fetch project ViewModels using the optimized, cache-aware helper + var projectVMs = await GetProjectInfosByIdsAsync(accessibleProjectIds); + + // Step 4: Return the final list + _logger.LogInfo("Successfully returned {ProjectCount} projects for EmployeeId {EmployeeId}", projectVMs.Count, loggedInEmployee.Id); + return ApiResponse.SuccessResponse(projectVMs, $"{projectVMs.Count} records of project fetchd successfully", 200); + } + catch (Exception ex) + { + // --- Step 5: Graceful Error Handling --- + _logger.LogError(ex, "An unexpected error occurred in GetAllProjectsBasic for tenant {TenantId}.", tenantId); + return ApiResponse.ErrorResponse("An internal server error occurred. Please try again later.", null, 500); + } + } + + public async Task> GetAllProjectsAsync(Guid tenantId, Employee loggedInEmployee) + { + try + { + _logger.LogInfo("Starting GetAllProjects for TenantId: {TenantId}, User: {UserId}", tenantId, loggedInEmployee.Id); + + // --- Step 1: Get a list of project IDs the user can access --- + List projectIds = await GetMyProjects(tenantId, loggedInEmployee); + if (!projectIds.Any()) + { + _logger.LogInfo("User has no assigned projects. Returning empty list."); + return ApiResponse.SuccessResponse(new List(), "No projects found for the current user.", 200); + } + + // --- Step 2: Efficiently handle partial cache hits --- + _logger.LogInfo("Attempting to fetch details for {ProjectCount} projects from cache.", projectIds.Count); + + // Fetch what we can from the cache. + var cachedDetails = await _cache.GetProjectDetailsList(projectIds) ?? new List(); + var cachedDictionary = cachedDetails.ToDictionary(p => Guid.Parse(p.Id)); + + // Identify which projects are missing from the cache. + var missingIds = projectIds.Where(id => !cachedDictionary.ContainsKey(id)).ToList(); + + // Start building the response with the items we found in the cache. + var responseVms = _mapper.Map>(cachedDictionary.Values); + + if (missingIds.Any()) + { + // --- Step 3: Fetch ONLY the missing items from the database --- + _logger.LogInfo("Cache partial MISS. Found {CachedCount}, fetching {MissingCount} projects from DB.", + cachedDictionary.Count, missingIds.Count); + + // Call our dedicated data-fetching method for the missing IDs. + var newMongoDetails = await FetchAndBuildProjectDetails(missingIds, tenantId); + + if (newMongoDetails.Any()) + { + // Map the newly fetched items and add them to our response list. + responseVms.AddRange(newMongoDetails); + } + } + else + { + _logger.LogInfo("Cache HIT. All {ProjectCount} projects found in cache.", projectIds.Count); + } + + // --- Step 4: Return the combined result --- + _logger.LogInfo("Successfully retrieved a total of {ProjectCount} projects.", responseVms.Count); + return ApiResponse.SuccessResponse(responseVms, "Projects retrieved successfully.", 200); + } + catch (Exception ex) + { + // --- Step 5: Graceful Error Handling --- + _logger.LogError(ex, "An unexpected error occurred in GetAllProjects for tenant {TenantId}.", tenantId); + return ApiResponse.ErrorResponse("An internal server error occurred. Please try again later.", null, 500); + } + } + + public async Task> GetProjectAsync(Guid id, Guid tenantId, Employee loggedInEmployee) + { + try + { + // --- Step 1: Run independent operations in PARALLEL --- + // We can check permissions and fetch data at the same time to reduce latency. + var permissionTask = _permission.HasProjectPermission(loggedInEmployee, id); + + // This helper method encapsulates the "cache-first, then database" logic. + var projectDataTask = GetProjectDataAsync(id, tenantId); + + // Await both tasks to complete. + await Task.WhenAll(permissionTask, projectDataTask); + + var hasPermission = await permissionTask; + var projectVm = await projectDataTask; + + // --- Step 2: Process results sequentially --- + + // 2a. Check for permission first. Forbid() is the idiomatic way to return 403. + if (!hasPermission) + { + _logger.LogWarning("Access denied for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, id); + return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to access this project.", 403); + } + + // 2b. Check if the project was found (either in cache or DB). + if (projectVm == null) + { + _logger.LogInfo("Project with ID {ProjectId} not found.", id); + return ApiResponse.ErrorResponse("Project not found.", $"No project found with ID {id}.", 404); + } + + // 2c. Success. Return the consistent ViewModel. + _logger.LogInfo("Successfully retrieved project {ProjectId}.", id); + return ApiResponse.SuccessResponse(projectVm, "Project retrieved successfully.", 200); + } + catch (Exception ex) + { + _logger.LogError(ex, "An unexpected error occurred while getting project {ProjectId}", id); + return ApiResponse.ErrorResponse("An internal server error occurred.", null, 500); + } + } + + public async Task> GetProjectDetailsAsync(Guid id, Guid tenantId, Employee loggedInEmployee) + { + try + { + _logger.LogInfo("Details requested by EmployeeId: {EmployeeId} for ProjectId: {ProjectId}", loggedInEmployee.Id, id); + + // Step 1: Check global view project permission + var hasViewProjectPermission = await _permission.HasPermission(PermissionsMaster.ViewProject, loggedInEmployee.Id); + if (!hasViewProjectPermission) + { + _logger.LogWarning("ViewProjects permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Access denied", "You don't have permission to view projects", 403); + } + + // Step 2: Check permission for this specific project + var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, id); + if (!hasProjectPermission) + { + _logger.LogWarning("Project-specific access denied. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployee.Id, id); + return ApiResponse.ErrorResponse("Access denied", "You don't have access to this project", 403); + } + + // Step 3: Fetch project with status + var projectDetails = await _cache.GetProjectDetails(id); + ProjectVM? projectVM = null; + if (projectDetails == null) + { + var project = await _context.Projects + .Include(c => c.ProjectStatus) + .FirstOrDefaultAsync(c => c.TenantId == tenantId && c.Id == id); + + projectVM = _mapper.Map(project); + + if (project != null) + { + await _cache.AddProjectDetails(project); + } + } + else + { + projectVM = _mapper.Map(projectDetails); + if (projectVM.ProjectStatus != null) + { + projectVM.ProjectStatus.TenantId = tenantId; + } + } + + if (projectVM == null) + { + _logger.LogWarning("Project not found. ProjectId: {ProjectId}", id); + return ApiResponse.ErrorResponse("Project not found", "Project not found", 404); + } + + // Step 4: Return result + + _logger.LogInfo("Project details fetched successfully. ProjectId: {ProjectId}", id); + return ApiResponse.SuccessResponse(projectVM, "Project details fetched successfully", 200); + } + catch (Exception ex) + { + // --- Step 5: Graceful Error Handling --- + _logger.LogError(ex, "An unexpected error occurred in Get Project Details for project {ProjectId} for tenant {TenantId}. ", id, tenantId); + return ApiResponse.ErrorResponse("An internal server error occurred. Please try again later.", null, 500); + } + } + + public async Task> GetProjectDetailsOldAsync(Guid id, Guid tenantId, Employee loggedInEmployee) + { + var project = await _context.Projects + .Where(c => c.TenantId == tenantId && c.Id == id) + .Include(c => c.ProjectStatus) + .SingleOrDefaultAsync(); + + if (project == null) + { + return ApiResponse.ErrorResponse("Project not found", "Project not found", 404); + + } + else + { + ProjectDetailsVM vm = await GetProjectViewModel(id, project); + + OldProjectVM projectVM = new OldProjectVM(); + if (vm.project != null) + { + projectVM.Id = vm.project.Id; + projectVM.Name = vm.project.Name; + projectVM.ShortName = vm.project.ShortName; + projectVM.ProjectAddress = vm.project.ProjectAddress; + projectVM.ContactPerson = vm.project.ContactPerson; + projectVM.StartDate = vm.project.StartDate; + projectVM.EndDate = vm.project.EndDate; + projectVM.ProjectStatusId = vm.project.ProjectStatusId; + } + projectVM.Buildings = new List(); + if (vm.buildings != null) + { + foreach (Building build in vm.buildings) + { + BuildingVM buildVM = new BuildingVM() { Id = build.Id, Description = build.Description, Name = build.Name }; + buildVM.Floors = new List(); + if (vm.floors != null) + { + foreach (Floor floorDto in vm.floors.Where(c => c.BuildingId == build.Id).ToList()) + { + FloorsVM floorVM = new FloorsVM() { FloorName = floorDto.FloorName, Id = floorDto.Id }; + floorVM.WorkAreas = new List(); + + if (vm.workAreas != null) + { + foreach (WorkArea workAreaDto in vm.workAreas.Where(c => c.FloorId == floorVM.Id).ToList()) + { + WorkAreaVM workAreaVM = new WorkAreaVM() { Id = workAreaDto.Id, AreaName = workAreaDto.AreaName, WorkItems = new List() }; + + if (vm.workItems != null) + { + foreach (WorkItem workItemDto in vm.workItems.Where(c => c.WorkAreaId == workAreaDto.Id).ToList()) + { + WorkItemVM workItemVM = new WorkItemVM() { WorkItemId = workItemDto.Id, WorkItem = workItemDto }; + + workItemVM.WorkItem.WorkArea = new WorkArea(); + + if (workItemVM.WorkItem.ActivityMaster != null) + { + workItemVM.WorkItem.ActivityMaster.Tenant = new Tenant(); + } + workItemVM.WorkItem.Tenant = new Tenant(); + + double todaysAssigned = 0; + if (vm.Tasks != null) + { + var tasks = vm.Tasks.Where(t => t.WorkItemId == workItemDto.Id).ToList(); + foreach (TaskAllocation task in tasks) + { + todaysAssigned += task.PlannedTask; + } + } + workItemVM.TodaysAssigned = todaysAssigned; + + workAreaVM.WorkItems.Add(workItemVM); + } + } + + floorVM.WorkAreas.Add(workAreaVM); + } + } + + buildVM.Floors.Add(floorVM); + } + } + projectVM.Buildings.Add(buildVM); + } + } + return ApiResponse.SuccessResponse(projectVM, "Success.", 200); + } + } + + #endregion + + #region =================================================================== Project Manage APIs =================================================================== + + public async Task> CreateProjectAsync(CreateProjectDto projectDto, Guid tenantId, Employee loggedInEmployee) + { + // 1. Prepare data without I/O + var loggedInUserId = loggedInEmployee.Id; + var project = _mapper.Map(projectDto); + project.TenantId = tenantId; + + // 2. Store it to database + try + { + _context.Projects.Add(project); + await _context.SaveChangesAsync(); + } + catch (Exception ex) + { + // Log the detailed exception + _logger.LogError(ex, "Failed to create project in database. Rolling back transaction."); + // Return a server error as the primary operation failed + return ApiResponse.ErrorResponse("An error occurred while saving the project.", ex.Message, 500); + } + + // 3. Perform non-critical side-effects (caching, notifications) concurrently + try + { + // These operations do not depend on each other, so they can run in parallel. + Task cacheAddDetailsTask = _cache.AddProjectDetails(project); + Task cacheClearListTask = _cache.ClearAllProjectIdsByPermissionId(PermissionsMaster.ManageProject); + + // Await all side-effect tasks to complete in parallel + await Task.WhenAll(cacheAddDetailsTask, cacheClearListTask); + } + catch (Exception ex) + { + // The project was created successfully, but a side-effect failed. + // Log this as a warning, as the primary operation succeeded. Don't return an error to the user. + _logger.LogError(ex, "Project {ProjectId} was created, but a post-creation side-effect (caching/notification) failed. ", project.Id); + } + + // 4. Return a success response to the user as soon as the critical data is saved. + return ApiResponse.SuccessResponse(_mapper.Map(project), "Project created successfully.", 200); + } + + /// + /// Updates an existing project's details. + /// This endpoint is secure, handles concurrency, and performs non-essential tasks in the background. + /// + /// The ID of the project to update. + /// The data to update the project with. + /// An ApiResponse confirming the update or an appropriate error. + public async Task> UpdateProjectAsync(Guid id, UpdateProjectDto updateProjectDto, Guid tenantId, Employee loggedInEmployee) + { + try + { + // --- Step 1: Fetch the Existing Entity from the Database --- + // This is crucial to avoid the data loss bug. We only want to modify an existing record. + var existingProject = await _context.Projects + .Where(p => p.Id == id && p.TenantId == tenantId) + .SingleOrDefaultAsync(); + + // 1a. Existence Check + if (existingProject == null) + { + _logger.LogWarning("Attempt to update non-existent project with ID {ProjectId} by user {UserId}.", id, loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Project not found.", $"No project found with ID {id}.", 404); + } + + // 1b. Security Check + var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, id); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} attempting to update project {ProjectId}.", loggedInEmployee.Id, id); + return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to modify this project.", 403); + } + + // --- Step 2: Apply Changes and Save --- + // Map the changes from the DTO onto the entity we just fetched from the database. + // This only modifies the properties defined in the mapping, preventing data loss. + _mapper.Map(updateProjectDto, existingProject); + + // Mark the entity as modified (if your mapping doesn't do it automatically). + _context.Entry(existingProject).State = EntityState.Modified; + + try + { + await _context.SaveChangesAsync(); + _logger.LogInfo("Successfully updated project {ProjectId} by user {UserId}.", id, loggedInEmployee.Id); + } + catch (DbUpdateConcurrencyException ex) + { + // --- Step 3: Handle Concurrency Conflicts --- + // This happens if another user modified the project after we fetched it. + _logger.LogError(ex, "Concurrency conflict while updating project {ProjectId} ", id); + return ApiResponse.ErrorResponse("Conflict occurred.", "This project has been modified by someone else. Please refresh and try again.", 409); + } + + // --- Step 4: Perform Side-Effects (Fire and Forget) --- + // Create a DTO of the updated project to pass to background tasks. + var projectDto = _mapper.Map(existingProject); + + // 4a. Update Cache + await UpdateCacheInBackground(existingProject); + + // --- Step 5: Return Success Response Immediately --- + // The client gets a fast response without waiting for caching or SignalR. + return ApiResponse.SuccessResponse(projectDto, "Project updated successfully.", 200); + } + catch (Exception ex) + { + // --- Step 6: Graceful Error Handling for Unexpected Errors --- + _logger.LogError(ex, "An unexpected error occurred while updating project {ProjectId} ", id); + return ApiResponse.ErrorResponse("An internal server error occurred.", null, 500); + } + } + + #endregion + + #region =================================================================== Project Allocation APIs =================================================================== + + /// + /// Retrieves a list of employees for a specific project. + /// This method is optimized to perform all filtering and mapping on the database server. + /// + /// The ID of the project. + /// Whether to include employees from inactive allocations. + /// The ID of the current tenant. + /// The current authenticated employee (used for permission checks). + /// An ApiResponse containing a list of employees or an error. + public async Task> GetEmployeeByProjectIdAsync(Guid? projectId, bool includeInactive, Guid tenantId, Employee loggedInEmployee) + { + // --- Step 1: Input Validation --- + if (projectId == null) + { + _logger.LogWarning("GetEmployeeByProjectID called with a null projectId."); + // 400 Bad Request is more appropriate for invalid input than 404 Not Found. + return ApiResponse.ErrorResponse("Project ID is required.", "Invalid Input Parameter", 400); + } + + _logger.LogInfo("Fetching employees for ProjectID: {ProjectId}, IncludeInactive: {IncludeInactive}", projectId, includeInactive); + + try + { + // --- CRITICAL: Security Check --- + // Before fetching data, you MUST verify the user has permission to see it. + // This is a placeholder for your actual permission logic. + var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.Value); + var hasAllEmployeePermission = await _permission.HasPermission(PermissionsMaster.ViewAllEmployees, loggedInEmployee.Id); + var hasviewTeamPermission = await _permission.HasPermission(PermissionsMaster.ViewTeamMembers, loggedInEmployee.Id); + + if (!(hasProjectPermission && (hasAllEmployeePermission || hasviewTeamPermission))) + { + _logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, projectId); + return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to view this project's team.", 403); + } + + // --- Step 2: Build a Single, Efficient IQueryable --- + // We start with the base query and conditionally add filters before executing it. + // This avoids code duplication and is highly performant. + var employeeQuery = _context.ProjectAllocations + .Where(pa => pa.ProjectId == projectId && pa.TenantId == tenantId); + + // Conditionally apply the filter for active allocations. + if (!includeInactive) + { + employeeQuery = employeeQuery.Where(pa => pa.IsActive); + } + + // --- Step 3: Project Directly to the ViewModel on the Database Server --- + // This is the most significant performance optimization. + // Instead of fetching full Employee entities, we select only the data needed for the EmployeeVM. + // AutoMapper's ProjectTo is perfect for this, as it translates the mapping configuration into an efficient SQL SELECT statement. + var resultVM = await employeeQuery + .Where(pa => pa.Employee != null) // Safety check for data integrity + .Select(pa => pa.Employee) // Navigate to the Employee entity + .ProjectTo(_mapper.ConfigurationProvider) // Let AutoMapper generate the SELECT + .ToListAsync(); + + _logger.LogInfo("Successfully fetched {EmployeeCount} employees for project {ProjectId}.", resultVM.Count, projectId); + + // Note: The original mapping loop is now completely gone, replaced by the single efficient query above. + + return ApiResponse.SuccessResponse(resultVM, "Successfully fetched the list of employees for the selected project.", 200); + } + catch (Exception ex) + { + // --- Step 4: Graceful Error Handling --- + _logger.LogError(ex, "An error occurred while fetching employees for project {ProjectId}. ", projectId); + return ApiResponse.ErrorResponse("An internal server error occurred.", "Database Query Failed", 500); + } + } + + /// + /// Retrieves project allocation details for a specific project. + /// This method is optimized for performance and includes security checks. + /// + /// The ID of the project. + /// The ID of the current tenant. + /// The current authenticated employee for permission checks. + /// An ApiResponse containing allocation details or an appropriate error. + public async Task> GetProjectAllocationAsync(Guid? projectId, Guid tenantId, Employee loggedInEmployee) + { + // --- Step 1: Input Validation --- + if (projectId == null) + { + _logger.LogWarning("GetProjectAllocation called with a null projectId."); + return ApiResponse.ErrorResponse("Project ID is required.", "Invalid Input Parameter", 400); + } + + _logger.LogInfo("Fetching allocations for ProjectID: {ProjectId} for user {UserId}", projectId, loggedInEmployee.Id); + + try + { + // --- Step 2: Security and Existence Checks --- + // Before fetching data, you MUST verify the user has permission to see it. + // This is a placeholder for your actual permission logic. + var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.Value); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, projectId); + return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to view this project's team.", 403); + } + + // --- Step 3: Execute a Single, Optimized Database Query --- + // This query projects directly to a new object on the database server, which is highly efficient. + var allocations = await _context.ProjectAllocations + // Filter down to the relevant records first. + .Where(pa => pa.ProjectId == projectId && pa.TenantId == tenantId && pa.Employee != null) + // Project directly to the final shape. This tells EF Core which columns to select. + // The redundant .Include() is removed as EF Core infers the JOIN from this Select. + .Select(pa => new + { + // Fields from ProjectAllocation + ID = pa.Id, + pa.EmployeeId, + pa.ProjectId, + pa.AllocationDate, + pa.ReAllocationDate, + pa.IsActive, + + // Fields from the joined Employee table (no null checks needed due to the 'Where' clause) + FirstName = pa.Employee!.FirstName, + LastName = pa.Employee.LastName, + MiddleName = pa.Employee.MiddleName, + + // Simplified JobRoleId logic: Use the allocation's role if it exists, otherwise fall back to the employee's default role. + JobRoleId = pa.JobRoleId ?? pa.Employee.JobRoleId + }) + .ToListAsync(); + + _logger.LogInfo("Successfully fetched {AllocationCount} allocations for project {ProjectId}.", allocations.Count, projectId); + + return ApiResponse.SuccessResponse(allocations, "Project allocations retrieved successfully.", 200); + } + catch (Exception ex) + { + // --- Step 4: Graceful Error Handling --- + // Log the full exception for debugging, but return a generic, safe error message. + _logger.LogError(ex, "An error occurred while fetching allocations for project {ProjectId}.", projectId); + return ApiResponse.ErrorResponse("An internal server error occurred.", "Database query failed.", 500); + } + } + + /// + /// Manages project allocations for a list of employees, either adding new allocations or deactivating existing ones. + /// This method is optimized to perform all database operations in a single transaction. + /// + /// The list of allocation changes to process. + /// The ID of the current tenant. + /// The current authenticated employee for permission checks. + /// An ApiResponse containing the list of processed allocations. + public async Task>> ManageAllocationAsync(List allocationsDto, Guid tenantId, Employee loggedInEmployee) + { + // --- Step 1: Input Validation --- + if (allocationsDto == null || !allocationsDto.Any()) + { + return ApiResponse>.ErrorResponse("Invalid details.", "Allocation details list cannot be null or empty.", 400); + } + + _logger.LogInfo("Starting to manage {AllocationCount} allocations for user {UserId}.", allocationsDto.Count, loggedInEmployee.Id); + + // --- (Placeholder) Security Check --- + // In a real application, you would check if the loggedInEmployee has permission + // to manage allocations for ALL projects involved in this batch. + var projectIdsInBatch = allocationsDto.Select(a => a.ProjectId).Distinct().ToList(); + var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageTeam, loggedInEmployee.Id); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} trying to manage allocations for projects.", loggedInEmployee.Id); + return ApiResponse>.ErrorResponse("Access Denied.", "You do not have permission to manage one or more projects in this request.", 403); + } + + // --- Step 2: Fetch all relevant existing data in ONE database call --- + var employeeProjectPairs = allocationsDto.Select(a => new { a.EmpID, a.ProjectId }).ToList(); + List employeeIds = allocationsDto.Select(a => a.EmpID).Distinct().ToList(); + + // Fetch all currently active allocations for the employees and projects in this batch. + // We use a dictionary for fast O(1) lookups inside the loop. + var existingAllocations = await _context.ProjectAllocations + .Where(pa => pa.TenantId == tenantId && + employeeIds.Contains(pa.EmployeeId) && + pa.ReAllocationDate == null) + .ToDictionaryAsync(pa => (pa.EmployeeId, pa.ProjectId)); + + var processedAllocations = new List(); + + // --- Step 3: Process logic IN MEMORY --- + foreach (var dto in allocationsDto) + { + var key = (dto.EmpID, dto.ProjectId); + existingAllocations.TryGetValue(key, out var existingAllocation); + + if (dto.Status == false) // User wants to DEACTIVATE the allocation + { + if (existingAllocation != null) + { + // Mark the existing allocation for deactivation + existingAllocation.ReAllocationDate = DateTime.UtcNow; // Use UtcNow for servers + existingAllocation.IsActive = false; + _context.ProjectAllocations.Update(existingAllocation); + processedAllocations.Add(existingAllocation); + } + // If it doesn't exist, we do nothing. The desired state is "not allocated". + } + else // User wants to ACTIVATE the allocation + { + if (existingAllocation == null) + { + // Create a new allocation because one doesn't exist + var newAllocation = _mapper.Map(dto); + newAllocation.TenantId = tenantId; + newAllocation.AllocationDate = DateTime.UtcNow; + newAllocation.IsActive = true; + _context.ProjectAllocations.Add(newAllocation); + processedAllocations.Add(newAllocation); + } + // If it already exists and is active, we do nothing. The state is already correct. + } + try + { + await _cache.ClearAllProjectIds(dto.EmpID); + _logger.LogInfo("Successfully completed cache invalidation for employee {EmployeeId}.", dto.EmpID); + } + catch (Exception ex) + { + // Log the error but don't fail the entire request, as the primary DB operation succeeded. + _logger.LogError(ex, "Cache invalidation failed for employees after a successful database update."); + } + } + + try + { + // --- Step 4: Save all changes in a SINGLE TRANSACTION --- + // All Adds and Updates are sent to the database in one batch. + // If any part fails, the entire transaction is rolled back. + await _context.SaveChangesAsync(); + _logger.LogInfo("Successfully saved {ChangeCount} allocation changes to the database.", processedAllocations.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to save allocation changes to the database."); + return ApiResponse>.ErrorResponse("Database Error.", "An error occurred while saving the changes.", 500); + } + + + // --- Step 5: Map results and return success --- + var resultVm = _mapper.Map>(processedAllocations); + return ApiResponse>.SuccessResponse(resultVm, "Allocations managed successfully.", 200); + } + + /// + /// Retrieves a list of active projects assigned to a specific employee. + /// + /// The ID of the employee whose projects are being requested. + /// The ID of the current tenant. + /// The current authenticated employee for permission checks. + /// An ApiResponse containing a list of basic project details or an error. + public async Task> GetProjectsByEmployeeAsync(Guid employeeId, Guid tenantId, Employee loggedInEmployee) + { + // --- Step 1: Input Validation --- + if (employeeId == Guid.Empty) + { + return ApiResponse.ErrorResponse("Invalid details.", "A valid employee ID is required.", 400); + } + + _logger.LogInfo("Fetching projects for Employee {EmployeeId} by User {UserId}", employeeId, loggedInEmployee.Id); + + try + { + // --- Step 2: Clarified Security Check --- + // The permission should be about viewing another employee's assignments, not a generic "Manage Team". + // This is a placeholder for your actual, more specific permission logic. + // It should also handle the case where a user is requesting their own projects (employeeId == loggedInEmployee.Id). + var hasPermission = await _permission.HasPermission(PermissionsMaster.ViewProject, loggedInEmployee.Id); + var projectIds = await GetMyProjects(tenantId, loggedInEmployee); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} trying to view projects for employee {TargetEmployeeId}.", loggedInEmployee.Id, employeeId); + return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to view this employee's projects.", 403); + } + + // --- Step 3: Execute a Single, Highly Efficient Database Query --- + // This query projects directly to the ViewModel on the database server. + var projects = await _context.ProjectAllocations + // 1. Filter the linking table down to the relevant records. + .Where(pa => + pa.TenantId == tenantId && + pa.EmployeeId == employeeId && // Target the specified employee + pa.IsActive && // Only active assignments + projectIds.Contains(pa.ProjectId) && + pa.Project != null) // Safety check for data integrity + + // 2. Navigate to the Project entity. + .Select(pa => pa.Project) + + // 3. Ensure the final result set is unique (in case of multiple active allocations to the same project). + .Distinct() + + // 4. Project directly to the ViewModel using AutoMapper's IQueryable Extensions. + // This generates an efficient SQL "SELECT Id, Name, Code FROM..." statement. + .ProjectTo(_mapper.ConfigurationProvider) + + // 5. Execute the query. + .ToListAsync(); + + _logger.LogInfo("Successfully retrieved {ProjectCount} projects for employee {EmployeeId}.", projects.Count, employeeId); + + // The original check for an empty list is still good practice. + if (!projects.Any()) + { + return ApiResponse.SuccessResponse(new List(), "No active projects found for this employee.", 200); + } + + return ApiResponse.SuccessResponse(projects, "Projects retrieved successfully.", 200); + } + catch (Exception ex) + { + // --- Step 4: Graceful Error Handling --- + _logger.LogError(ex, "An error occurred while fetching projects for employee {EmployeeId}.", employeeId); + return ApiResponse.ErrorResponse("An internal server error occurred.", "Database query failed.", 500); + } + } + + /// + /// Manages project assignments for a single employee, processing a batch of projects to activate or deactivate. + /// This method is optimized to perform all database operations in a single, atomic transaction. + /// + /// A list of projects to assign or un-assign. + /// The ID of the employee whose assignments are being managed. + /// The ID of the current tenant. + /// The current authenticated employee for permission checks. + /// An ApiResponse containing the list of processed allocations. + public async Task>> AssigneProjectsToEmployeeAsync(List allocationsDto, Guid employeeId, Guid tenantId, Employee loggedInEmployee) + { + // --- Step 1: Input Validation --- + if (allocationsDto == null || !allocationsDto.Any() || employeeId == Guid.Empty) + { + return ApiResponse>.ErrorResponse("Invalid details.", "A valid employee ID and a list of projects are required.", 400); + } + + _logger.LogInfo("Starting to manage {AllocationCount} project assignments for Employee {EmployeeId}.", allocationsDto.Count, employeeId); + + // --- (Placeholder) Security Check --- + // You MUST verify that the loggedInEmployee has permission to modify the assignments for the target employeeId. + var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageTeam, loggedInEmployee.Id); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} trying to manage assignments for employee {TargetEmployeeId}.", loggedInEmployee.Id, employeeId); + return ApiResponse>.ErrorResponse("Access Denied.", "You do not have permission to manage this employee's assignments.", 403); + } + + // --- Step 2: Fetch all relevant existing data in ONE database call --- + var projectIdsInDto = allocationsDto.Select(p => p.ProjectId).ToList(); + + // Fetch all currently active allocations for this employee for the projects in the request. + // We use a dictionary keyed by ProjectId for fast O(1) lookups inside the loop. + var existingActiveAllocations = await _context.ProjectAllocations + .Where(pa => pa.TenantId == tenantId && + pa.EmployeeId == employeeId && + projectIdsInDto.Contains(pa.ProjectId) && + pa.ReAllocationDate == null) // Only fetch active ones + .ToDictionaryAsync(pa => pa.ProjectId); + + var processedAllocations = new List(); + + // --- Step 3: Process all logic IN MEMORY, tracking changes --- + foreach (var dto in allocationsDto) + { + existingActiveAllocations.TryGetValue(dto.ProjectId, out var existingAllocation); + + if (dto.Status == false) // DEACTIVATE this project assignment + { + if (existingAllocation != null) + { + // Correct Update Pattern: Modify the fetched entity directly. + existingAllocation.ReAllocationDate = DateTime.UtcNow; // Use UTC for servers + existingAllocation.IsActive = false; + _context.ProjectAllocations.Update(existingAllocation); + processedAllocations.Add(existingAllocation); + } + // If it's not in our dictionary, it's already inactive. Do nothing. + } + else // ACTIVATE this project assignment + { + if (existingAllocation == null) + { + // Create a new allocation because an active one doesn't exist. + var newAllocation = _mapper.Map(dto); + newAllocation.EmployeeId = employeeId; + newAllocation.TenantId = tenantId; + newAllocation.AllocationDate = DateTime.UtcNow; + newAllocation.IsActive = true; + _context.ProjectAllocations.Add(newAllocation); + processedAllocations.Add(newAllocation); + } + // If it already exists in our dictionary, it's already active. Do nothing. + } + } + + try + { + // --- Step 4: Save all Adds and Updates in a SINGLE ATOMIC TRANSACTION --- + if (processedAllocations.Any()) + { + await _context.SaveChangesAsync(); + _logger.LogInfo("Successfully saved {ChangeCount} assignment changes for employee {EmployeeId}.", processedAllocations.Count, employeeId); + } + } + catch (DbUpdateException ex) + { + _logger.LogError(ex, "Failed to save assignment changes for employee {EmployeeId}.", employeeId); + return ApiResponse>.ErrorResponse("Database Error.", "An error occurred while saving the changes.", 500); + } + + // --- Step 5: Invalidate Cache ONCE after successful save --- + try + { + await _cache.ClearAllProjectIds(employeeId); + _logger.LogInfo("Successfully queued cache invalidation for employee {EmployeeId}.", employeeId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Background cache invalidation failed for employee {EmployeeId}", employeeId); + } + + // --- Step 6: Map results using AutoMapper and return success --- + var resultVm = _mapper.Map>(processedAllocations); + return ApiResponse>.SuccessResponse(resultVm, "Assignments managed successfully.", 200); + } + + #endregion + + #region =================================================================== Project InfraStructure Get APIs =================================================================== + + /// + /// Retrieves the full infrastructure hierarchy (Buildings, Floors, Work Areas) for a project, + /// including aggregated work summaries. + /// + public async Task> GetInfraDetailsAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee) + { + _logger.LogInfo("GetInfraDetails called for ProjectId: {ProjectId}", projectId); + + try + { + // --- Step 1: Run independent permission checks in PARALLEL --- + var projectPermissionTask = _permission.HasProjectPermission(loggedInEmployee, projectId); + var viewInfraPermissionTask = _permission.HasPermission(PermissionsMaster.ViewProjectInfra, loggedInEmployee.Id); + + await Task.WhenAll(projectPermissionTask, viewInfraPermissionTask); + + if (!await projectPermissionTask) + { + _logger.LogWarning("Project access denied for EmployeeId: {EmployeeId} on ProjectId: {ProjectId}", loggedInEmployee.Id, projectId); + return ApiResponse.ErrorResponse("Access denied", "You don't have access to this project", 403); + } + if (!await viewInfraPermissionTask) + { + _logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Access denied", "You don't have access to view this project's infrastructure", 403); + } + + // --- Step 2: Cache-First Strategy --- + var cachedResult = await _cache.GetBuildingInfra(projectId); + if (cachedResult != null) + { + _logger.LogInfo("Cache HIT for infra details for ProjectId: {ProjectId}", projectId); + return ApiResponse.SuccessResponse(cachedResult, "Infra details fetched successfully from cache.", 200); + } + + _logger.LogInfo("Cache MISS for infra details for ProjectId: {ProjectId}. Fetching from database.", projectId); + + // --- Step 3: Fetch all required data from the database --- + + var buildingMongoList = await _generalHelper.GetProjectInfraFromDB(projectId); + // --- Step 5: Proactively update the cache --- + //await _cache.SetBuildingInfra(projectId, buildingMongoList); + + _logger.LogInfo("Infra details fetched successfully for ProjectId: {ProjectId}, Buildings: {Count}", projectId, buildingMongoList.Count); + return ApiResponse.SuccessResponse(buildingMongoList, "Infra details fetched successfully", 200); + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while fetching infra details for ProjectId: {ProjectId}", projectId); + return ApiResponse.ErrorResponse("An internal server error occurred.", "An error occurred while processing your request.", 500); + } + } + + /// + /// Retrieves a list of work items for a specific work area, ensuring the user has appropriate permissions. + /// + /// The ID of the work area. + /// The ID of the current tenant. + /// The current authenticated employee for permission checks. + /// An ApiResponse containing a list of work items or an error. + public async Task> GetWorkItemsAsync(Guid workAreaId, Guid tenantId, Employee loggedInEmployee) + { + _logger.LogInfo("GetWorkItems called for WorkAreaId: {WorkAreaId} by User: {UserId}", workAreaId, loggedInEmployee.Id); + + try + { + // --- Step 1: Cache-First Strategy --- + var cachedWorkItems = await _cache.GetWorkItemDetailsByWorkArea(workAreaId); + if (cachedWorkItems != null) + { + _logger.LogInfo("Cache HIT for WorkAreaId: {WorkAreaId}. Returning {Count} items from cache.", workAreaId, cachedWorkItems.Count); + return ApiResponse.SuccessResponse(cachedWorkItems, $"{cachedWorkItems.Count} tasks retrieved successfully from cache.", 200); + } + + _logger.LogInfo("Cache MISS for WorkAreaId: {WorkAreaId}. Fetching from database.", workAreaId); + + // --- Step 2: Security Check First --- + // This pattern remains the most robust: verify permissions before fetching a large list. + var projectInfo = await _context.WorkAreas + .Where(wa => wa.Id == workAreaId && wa.TenantId == tenantId && wa.Floor != null && wa.Floor.Building != null) + .Select(wa => new { wa.Floor!.Building!.ProjectId }) + .FirstOrDefaultAsync(); + + if (projectInfo == null) + { + _logger.LogWarning("Work Area not found for WorkAreaId: {WorkAreaId}", workAreaId); + return ApiResponse.ErrorResponse("Not Found", $"Work Area with ID {workAreaId} not found.", 404); + } + + var hasProjectAccess = await _permission.HasProjectPermission(loggedInEmployee, projectInfo.ProjectId); + var hasGenericViewInfraPermission = await _permission.HasPermission(PermissionsMaster.ViewProjectInfra, loggedInEmployee.Id); + + if (!hasProjectAccess || !hasGenericViewInfraPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} on WorkAreaId {WorkAreaId}.", loggedInEmployee.Id, workAreaId); + return ApiResponse.ErrorResponse("Access Denied", "You do not have sufficient permissions to view these work items.", 403); + } + + // --- Step 3: Fetch Full Entities for Caching and Mapping --- + var workItemVMs = await _generalHelper.GetWorkItemsListFromDB(workAreaId); + + // --- Step 5: Proactively Update the Cache with the Correct Object Type --- + // We now pass the 'workItemsFromDb' list, which is the required List. + + try + { + await _cache.ManageWorkItemDetailsByVM(workItemVMs); + _logger.LogInfo("Successfully queued cache update for WorkAreaId: {WorkAreaId}", workAreaId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Background cache update failed for WorkAreaId: {WorkAreaId}", workAreaId); + } + + + _logger.LogInfo("{Count} work items fetched successfully for WorkAreaId: {WorkAreaId}", workItemVMs.Count, workAreaId); + return ApiResponse.SuccessResponse(workItemVMs, $"{workItemVMs.Count} tasks fetched successfully.", 200); + } + catch (Exception ex) + { + // --- Step 6: Graceful Error Handling --- + _logger.LogError(ex, "An unexpected error occurred while getting work items for WorkAreaId: {WorkAreaId}", workAreaId); + return ApiResponse.ErrorResponse("An internal server error occurred.", null, 500); + } + } + + #endregion + + #region =================================================================== Project Infrastructre Manage APIs =================================================================== + + public async Task ManageProjectInfraAsync(List infraDtos, Guid tenantId, Employee loggedInEmployee) + { + // 1. Guard Clause: Handle null or empty input gracefully. + if (infraDtos == null || !infraDtos.Any()) + { + return new ServiceResponse + { + Response = ApiResponse.ErrorResponse("Invalid details.", "No infrastructure details were provided.", 400) + }; + } + + var responseData = new InfraVM(); + var messages = new List(); + var projectIds = new HashSet(); // Use HashSet for automatic duplicate handling. + var cacheUpdateTasks = new List(); + + // --- Pre-fetch parent entities to avoid N+1 query problem --- + // 2. Gather all parent IDs needed for validation and context. + var requiredBuildingIds = infraDtos + .Where(i => i.Floor?.BuildingId != null) + .Select(i => i.Floor!.BuildingId) + .Distinct() + .ToList(); + + var requiredFloorIds = infraDtos + .Where(i => i.WorkArea?.FloorId != null) + .Select(i => i.WorkArea!.FloorId) + .Distinct() + .ToList(); + + // 3. Fetch all required parent entities in single batch queries. + var buildingsDict = await _context.Buildings + .Where(b => requiredBuildingIds.Contains(b.Id)) + .ToDictionaryAsync(b => b.Id); + + var floorsDict = await _context.Floor + .Include(f => f.Building) // Eagerly load Building for later use + .Where(f => requiredFloorIds.Contains(f.Id)) + .ToDictionaryAsync(f => f.Id); + // --- End Pre-fetching --- + + // 4. Process all entities and add them to the context's change tracker. + foreach (var item in infraDtos) + { + if (item.Building != null) + { + ProcessBuilding(item.Building, tenantId, responseData, messages, projectIds, cacheUpdateTasks); + } + if (item.Floor != null) + { + ProcessFloor(item.Floor, tenantId, responseData, messages, projectIds, cacheUpdateTasks, buildingsDict); + } + if (item.WorkArea != null) + { + ProcessWorkArea(item.WorkArea, tenantId, responseData, messages, projectIds, cacheUpdateTasks, floorsDict); + } + } + + // 5. Save all changes to the database in a single transaction. + var changedRecordCount = await _context.SaveChangesAsync(); + + // If no changes were actually made, we can exit early. + if (changedRecordCount == 0) + { + return new ServiceResponse + { + Response = ApiResponse.SuccessResponse(responseData, "No changes detected in the provided infrastructure details.", 200) + }; + } + + // 6. Execute all cache updates concurrently after the DB save is successful. + await Task.WhenAll(cacheUpdateTasks); + + // 7. Consolidate messages and create notification payload. + string finalResponseMessage = messages.LastOrDefault() ?? "Infrastructure managed successfully."; + string logMessage = $"{string.Join(", ", messages)} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds.ToList(), Message = logMessage }; + + // TODO: Dispatch the 'notification' object to your notification service. + + return new ServiceResponse + { + Notification = notification, + Response = ApiResponse.SuccessResponse(responseData, finalResponseMessage, 200) + }; + } + + /// + /// Creates or updates a batch of work items. + /// This method is optimized to perform all database operations in a single, atomic transaction. + /// + public async Task>> CreateProjectTaskAsync(List workItemDtos, Guid tenantId, Employee loggedInEmployee) + { + _logger.LogInfo("CreateProjectTask called with {Count} items by user {UserId}", workItemDtos?.Count ?? 0, loggedInEmployee.Id); + + // --- Step 1: Input Validation --- + if (workItemDtos == null || !workItemDtos.Any()) + { + _logger.LogWarning("No work items provided in the request."); + return ApiResponse>.ErrorResponse("Invalid details.", "Work Item details list cannot be empty.", 400); + } + + // --- Step 2: Fetch all required existing data in bulk --- + var workAreaIds = workItemDtos.Select(d => d.WorkAreaID).Distinct().ToList(); + var workItemIdsToUpdate = workItemDtos.Where(d => d.Id.HasValue).Select(d => d.Id!.Value).ToList(); + + // Fetch all relevant WorkAreas and their parent hierarchy in ONE query + var workAreasFromDb = await _context.WorkAreas + .Where(wa => wa.Floor != null && wa.Floor.Building != null && workAreaIds.Contains(wa.Id) && wa.TenantId == tenantId) + .Include(wa => wa.Floor!.Building) // Eagerly load the entire path + .ToDictionaryAsync(wa => wa.Id); // Dictionary for fast lookups + + // Fetch all existing WorkItems that need updating in ONE query + var existingWorkItemsToUpdate = await _context.WorkItems + .Where(wi => workItemIdsToUpdate.Contains(wi.Id) && wi.TenantId == tenantId) + .ToDictionaryAsync(wi => wi.Id); // Dictionary for fast lookups + + // --- (Placeholder) Security Check --- + // You MUST verify the user has permission to modify ALL WorkAreas in the batch. + var projectIdsInBatch = workAreasFromDb.Values.Select(wa => wa.Floor!.Building!.ProjectId).Distinct(); + var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageProjectInfra, loggedInEmployee.Id); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} trying to create/update tasks.", loggedInEmployee.Id); + return ApiResponse>.ErrorResponse("Access Denied.", "You do not have permission to modify tasks in one or more of the specified work areas.", 403); + } + + var workItemsToCreate = new List(); + var workItemsToModify = new List(); + var workDeltaForCache = new Dictionary(); // WorkAreaId -> (Delta) + string message = ""; + + // --- Step 3: Process all logic IN MEMORY, tracking changes --- + foreach (var dto in workItemDtos) + { + if (!workAreasFromDb.TryGetValue(dto.WorkAreaID, out var workArea)) + { + _logger.LogWarning("Skipping item because WorkAreaId {WorkAreaId} was not found or is invalid.", dto.WorkAreaID); + continue; // Skip this item as its parent WorkArea is invalid + } + + if (dto.Id.HasValue && existingWorkItemsToUpdate.TryGetValue(dto.Id.Value, out var existingWorkItem)) + { + // --- UPDATE Logic --- + var plannedDelta = dto.PlannedWork - existingWorkItem.PlannedWork; + var completedDelta = dto.CompletedWork - existingWorkItem.CompletedWork; + + // Apply changes from DTO to the fetched entity to prevent data loss + _mapper.Map(dto, existingWorkItem); + workItemsToModify.Add(existingWorkItem); + + // Track the change in work for cache update + workDeltaForCache[workArea.Id] = ( + workDeltaForCache.GetValueOrDefault(workArea.Id).Planned + plannedDelta, + workDeltaForCache.GetValueOrDefault(workArea.Id).Completed + completedDelta + ); + message = $"Task Updated in Building: {workArea.Floor?.Building?.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; + } + else + { + // --- CREATE Logic --- + var newWorkItem = _mapper.Map(dto); + newWorkItem.Id = Guid.NewGuid(); // Ensure new GUID is set + newWorkItem.TenantId = tenantId; + workItemsToCreate.Add(newWorkItem); + + // Track the change in work for cache update + workDeltaForCache[workArea.Id] = ( + workDeltaForCache.GetValueOrDefault(workArea.Id).Planned + newWorkItem.PlannedWork, + workDeltaForCache.GetValueOrDefault(workArea.Id).Completed + newWorkItem.CompletedWork + ); + message = $"Task Added in Building: {workArea.Floor?.Building?.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; + } + } + + try + { + // --- Step 4: Save all database changes in a SINGLE TRANSACTION --- + if (workItemsToCreate.Any()) _context.WorkItems.AddRange(workItemsToCreate); + if (workItemsToModify.Any()) _context.WorkItems.UpdateRange(workItemsToModify); // EF Core handles individual updates correctly here + + if (workItemsToCreate.Any() || workItemsToModify.Any()) + { + await _context.SaveChangesAsync(); + _logger.LogInfo("Successfully saved {CreatedCount} new and {UpdatedCount} updated work items.", workItemsToCreate.Count, workItemsToModify.Count); + + // --- Step 5: Update Cache and SignalR AFTER successful DB save --- + var allAffectedItems = workItemsToCreate.Concat(workItemsToModify).ToList(); + + await UpdateCacheAndNotify(workDeltaForCache, allAffectedItems); + } + } + catch (DbUpdateException ex) + { + _logger.LogError(ex, "A database error occurred while creating/updating tasks."); + return ApiResponse>.ErrorResponse("Database Error", "Failed to save changes.", 500); + } + + // --- Step 6: Prepare and return the response --- + var allProcessedItems = workItemsToCreate.Concat(workItemsToModify).ToList(); + var responseList = allProcessedItems.Select(wi => new WorkItemVM + { + WorkItemId = wi.Id, + WorkItem = wi + }).ToList(); + + + return ApiResponse>.SuccessResponse(responseList, message, 200); + } + + public async Task DeleteProjectTaskAsync(Guid id, Guid tenantId, Employee loggedInEmployee) + { + // 1. Fetch the task and its parent data in a single query. + // This is still a major optimization, avoiding a separate query for the floor/building. + WorkItem? task = await _context.WorkItems + .AsNoTracking() // Use AsNoTracking because we will re-attach for deletion later. + .Include(t => t.WorkArea) + .ThenInclude(wa => wa!.Floor) + .ThenInclude(f => f!.Building) + .FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId); + + // 2. Guard Clause: Handle non-existent task. + if (task == null) + { + _logger.LogWarning("Attempted to delete a non-existent task with ID {WorkItemId}", id); + return new ServiceResponse + { + Response = ApiResponse.ErrorResponse("Task not found.", $"A task with ID {id} was not found.", 404) + }; + } + + // 3. Guard Clause: Prevent deletion if work has started. + if (task.CompletedWork > 0) + { + double percentage = Math.Round((task.CompletedWork / task.PlannedWork) * 100, 2); + _logger.LogWarning("Task with ID {WorkItemId} is {CompletionPercentage}% complete and cannot be deleted.", task.Id, percentage); + return new ServiceResponse + { + Response = ApiResponse.ErrorResponse($"Task is {percentage}% complete and cannot be deleted.", "Deletion failed because the task has progress.", 400) + }; + } + + // 4. Guard Clause: Efficiently check if the task is assigned in a separate, optimized query. + // AnyAsync() is highly efficient and translates to a `SELECT TOP 1` or `EXISTS` in SQL. + bool isAssigned = await _context.TaskAllocations.AnyAsync(t => t.WorkItemId == id); + if (isAssigned) + { + _logger.LogWarning("Task with ID {WorkItemId} is currently assigned and cannot be deleted.", task.Id); + return new ServiceResponse + { + Response = ApiResponse.ErrorResponse("Task is currently assigned and cannot be deleted.", "Deletion failed because the task is assigned to an employee.", 400) + }; + } + + // --- Success Path: All checks passed, proceed with deletion --- + + var building = task.WorkArea?.Floor?.Building; + var notification = new + { + LoggedInUserId = loggedInEmployee.Id, + Keyword = "WorkItem", + WorkAreaIds = new[] { task.WorkAreaId }, + Message = $"Task Deleted in Building: {building?.Name ?? "N/A"}, on Floor: {task.WorkArea?.Floor?.FloorName ?? "N/A"}, in Area: {task.WorkArea?.AreaName ?? "N/A"} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}" + }; + + // 5. Perform the database deletion. + // We must attach a new instance or the original one without AsNoTracking. + // Since we used AsNoTracking, we create a 'stub' entity for deletion. + // This is more efficient than re-querying. + _context.WorkItems.Remove(new WorkItem { Id = task.Id }); + await _context.SaveChangesAsync(); + _logger.LogInfo("Task with ID {WorkItemId} has been successfully deleted.", task.Id); + + // 6. Perform cache operations concurrently. + var cacheTasks = new List + { + _cache.DeleteWorkItemByIdAsync(task.Id) + }; + + if (building?.ProjectId != null) + { + cacheTasks.Add(_cache.DeleteProjectByIdAsync(building.ProjectId)); + } + await Task.WhenAll(cacheTasks); + + // 7. Return the final success response. + return new ServiceResponse + { + Notification = notification, + Response = ApiResponse.SuccessResponse(new { id = task.Id }, "Task deleted successfully.", 200) + }; + } + #endregion + + #region =================================================================== Helper Functions =================================================================== + + public async Task> GetAllProjectByTanentID(Guid tanentId) + { + List alloc = await _context.Projects.Where(c => c.TenantId == tanentId).ToListAsync(); + return alloc; + } + + public async Task> GetProjectByEmployeeID(Guid employeeId) + { + List alloc = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.IsActive == true).Include(c => c.Project).ToListAsync(); + return alloc; + } + + public async Task> 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> GetMyProjects(Guid tenantId, Employee LoggedInEmployee) + { + var projectIds = await _cache.GetProjects(LoggedInEmployee.Id); + + if (projectIds == null) + { + var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageProject, LoggedInEmployee.Id); + if (hasPermission) + { + var projects = await _context.Projects.Where(c => c.TenantId == tenantId).ToListAsync(); + projectIds = projects.Select(p => p.Id).ToList(); + } + else + { + var allocation = await GetProjectByEmployeeID(LoggedInEmployee.Id); + if (!allocation.Any()) + { + return new List(); + } + projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); + } + await _cache.AddProjects(LoggedInEmployee.Id, projectIds); + } + return projectIds; + } + + public async Task> GetMyProjectIdsAsync(Guid tenantId, Employee loggedInEmployee) + { + // 1. Attempt to retrieve the list of project IDs from the cache first. + // This is the "happy path" and should be as fast as possible. + List? projectIds = await _cache.GetProjects(loggedInEmployee.Id); + + if (projectIds != null) + { + // Cache Hit: Return the cached list immediately. + return projectIds; + } + + // 2. Cache Miss: The list was not in the cache, so we must fetch it from the database. + List newProjectIds; + + // Check for the specific permission. + var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageProject, loggedInEmployee.Id); + + if (hasPermission) + { + // 3a. OPTIMIZATION: User has permission to see all projects. + // Fetch *only* the Ids directly from the database. This is far more efficient + // than fetching full Project objects and then selecting the Ids in memory. + newProjectIds = await _context.Projects + .Where(p => p.TenantId == tenantId) + .Select(p => p.Id) // This translates to `SELECT Id FROM Projects...` in SQL. + .ToListAsync(); + } + else + { + // 3b. OPTIMIZATION: User can only see projects they are allocated to. + // We go directly to the source (ProjectAllocations) and ask the database + // for a distinct list of ProjectIds. This is much better than calling a + // helper function that might return full allocation objects. + newProjectIds = await _context.ProjectAllocations + .Where(a => a.EmployeeId == loggedInEmployee.Id && a.ProjectId != Guid.Empty) + .Select(a => a.ProjectId) + .Distinct() // Pushes the DISTINCT operation to the database. + .ToListAsync(); + } + + // 4. Populate the cache with the newly fetched list (even if it's empty). + // This prevents repeated database queries for employees with no projects. + await _cache.AddProjects(loggedInEmployee.Id, newProjectIds); + + return newProjectIds; + } + + + /// + /// Retrieves a list of ProjectInfoVMs by their IDs, using an efficient partial cache-hit strategy. + /// It fetches what it can from the cache (as ProjectMongoDB), gets the rest from the + /// database (as Project), updates the cache, and returns a unified list of ViewModels. + /// + /// The list of project IDs to retrieve. + /// A list of ProjectInfoVMs. + private async Task> GetProjectInfosByIdsAsync(List projectIds) + { + // --- Step 1: Fetch from Cache --- + // The cache returns a list of MongoDB documents for the projects it found. + var cachedMongoDocs = await _cache.GetProjectDetailsList(projectIds) ?? new List(); + var finalViewModels = _mapper.Map>(cachedMongoDocs); + + _logger.LogDebug("Cache hit for {CacheCount} of {TotalCount} projects.", finalViewModels.Count, projectIds.Count); + + // --- Step 2: Identify Missing Projects --- + // If we found everything in the cache, we can return early. + if (finalViewModels.Count == projectIds.Count) + { + return finalViewModels; + } + + var cachedIds = cachedMongoDocs.Select(p => p.Id).ToHashSet(); // Assuming ProjectMongoDB has an Id + var missingIds = projectIds.Where(id => !cachedIds.Contains(id.ToString())).ToList(); + + // --- Step 3: Fetch Missing from Database --- + if (missingIds.Any()) + { + _logger.LogDebug("Cache miss for {MissingCount} projects. Querying database.", missingIds.Count); + + var projectsFromDb = await _context.Projects + .Where(p => missingIds.Contains(p.Id)) + .AsNoTracking() // Use AsNoTracking for read-only query performance + .ToListAsync(); + + if (projectsFromDb.Any()) + { + // Map the newly fetched projects (from SQL) to their ViewModel + var vmsFromDb = _mapper.Map>(projectsFromDb); + finalViewModels.AddRange(vmsFromDb); + + // --- Step 4: Update Cache with Missing Items in a new scope --- + _logger.LogDebug("Updating cache with {DbCount} newly fetched projects.", projectsFromDb.Count); + await _cache.AddProjectDetailsList(projectsFromDb); + } + } + + return finalViewModels; + } + + private async Task GetProjectViewModel(Guid? id, Project project) + { + ProjectDetailsVM vm = new ProjectDetailsVM(); + + // List buildings = _unitOfWork.Building.GetAll(c => c.ProjectId == id).ToList(); + List buildings = await _context.Buildings.Where(c => c.ProjectId == id).ToListAsync(); + List idList = buildings.Select(o => o.Id).ToList(); + // List floors = _unitOfWork.Floor.GetAll(c => idList.Contains(c.Id)).ToList(); + List floors = await _context.Floor.Where(c => idList.Contains(c.BuildingId)).ToListAsync(); + idList = floors.Select(o => o.Id).ToList(); + //List workAreas = _unitOfWork.WorkArea.GetAll(c => idList.Contains(c.Id), includeProperties: "WorkItems,WorkItems.ActivityMaster").ToList(); + + List workAreas = await _context.WorkAreas.Where(c => idList.Contains(c.FloorId)).ToListAsync(); + + idList = workAreas.Select(o => o.Id).ToList(); + List workItems = await _context.WorkItems.Include(c => c.WorkCategoryMaster).Where(c => idList.Contains(c.WorkAreaId)).Include(c => c.ActivityMaster).ToListAsync(); + // List workItems = _unitOfWork.WorkItem.GetAll(c => idList.Contains(c.WorkAreaId), includeProperties: "ActivityMaster").ToList(); + idList = workItems.Select(t => t.Id).ToList(); + List tasks = await _context.TaskAllocations.Where(t => idList.Contains(t.WorkItemId) && t.AssignmentDate.Date == DateTime.UtcNow.Date).ToListAsync(); + vm.project = project; + vm.buildings = buildings; + vm.floors = floors; + vm.workAreas = workAreas; + vm.workItems = workItems; + vm.Tasks = tasks; + return vm; + } + + /// + /// Fetches project details from the database for a given list of project IDs and assembles them into MongoDB models. + /// This method encapsulates the optimized, parallel database queries. + /// + /// The list of project IDs to fetch. + /// The current tenant ID for filtering. + /// A list of fully populated ProjectMongoDB objects. + private async Task> FetchAndBuildProjectDetails(List projectIdsToFetch, Guid tenantId) + { + // Task to get base project details for the MISSING projects + var projectsTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + return await context.Projects.AsNoTracking() + .Where(p => projectIdsToFetch.Contains(p.Id) && p.TenantId == tenantId) + .ToListAsync(); + }); + + // Task to get team sizes for the MISSING projects + var teamSizesTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + return await context.ProjectAllocations.AsNoTracking() + .Where(pa => pa.TenantId == tenantId && projectIdsToFetch.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); + }); + + // Task to get work summaries for the MISSING projects + var workSummariesTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + return await context.WorkItems.AsNoTracking() + .Where(wi => wi.TenantId == tenantId && + wi.WorkArea != null && + wi.WorkArea.Floor != null && + wi.WorkArea.Floor.Building != null && + projectIdsToFetch.Contains(wi.WorkArea.Floor.Building.ProjectId)) + .GroupBy(wi => wi.WorkArea!.Floor!.Building!.ProjectId) + .Select(g => new { ProjectId = g.Key, PlannedWork = g.Sum(i => i.PlannedWork), CompletedWork = g.Sum(i => i.CompletedWork) }) + .ToDictionaryAsync(x => x.ProjectId); + }); + + // Await all parallel tasks to complete + await Task.WhenAll(projectsTask, teamSizesTask, workSummariesTask); + + var projects = await projectsTask; + var teamSizes = await teamSizesTask; + var workSummaries = await workSummariesTask; + + // Proactively update the cache with the items we just fetched. + _logger.LogInfo("Updating cache with {NewItemCount} newly fetched projects.", projects.Count); + await _cache.AddProjectDetailsList(projects); + + // This section would build the full ProjectMongoDB objects, similar to your AddProjectDetailsList method. + // For brevity, assuming you have a mapper or a builder for this. Here's a simplified representation: + var mongoDetailsList = new List(); + foreach (var project in projects) + { + // This is a placeholder for the full build logic from your other methods. + // In a real scenario, you would fetch all hierarchy levels (buildings, floors, etc.) + // for the `projectIdsToFetch` and build the complete MongoDB object. + var mongoDetail = _mapper.Map(project); + mongoDetail.Id = project.Id; + mongoDetail.TeamSize = teamSizes.GetValueOrDefault(project.Id, 0); + if (workSummaries.TryGetValue(project.Id, out var summary)) + { + mongoDetail.PlannedWork = summary.PlannedWork; + mongoDetail.CompletedWork = summary.CompletedWork; + } + mongoDetailsList.Add(mongoDetail); + } + + return mongoDetailsList; + } + + /// + /// Private helper to encapsulate the cache-first data retrieval logic. + /// + /// A ProjectDetailVM if found, otherwise null. + private async Task GetProjectDataAsync(Guid projectId, Guid tenantId) + { + // --- Cache First --- + _logger.LogDebug("Attempting to fetch project {ProjectId} from cache.", projectId); + var cachedProject = await _cache.GetProjectDetails(projectId); + if (cachedProject != null) + { + _logger.LogInfo("Cache HIT for project {ProjectId}.", projectId); + // Map from the cache model (e.g., ProjectMongoDB) to the response ViewModel. + return _mapper.Map(cachedProject); + } + + // --- Database Second (on Cache Miss) --- + _logger.LogInfo("Cache MISS for project {ProjectId}. Fetching from database.", projectId); + var dbProject = await _context.Projects + .AsNoTracking() // Use AsNoTracking for read-only queries. + .Where(p => p.Id == projectId && p.TenantId == tenantId) + .SingleOrDefaultAsync(); + + if (dbProject == null) + { + return null; // The project doesn't exist. + } + + // --- Proactively Update Cache --- + // The next request for this project will now be a cache hit. + try + { + // Map the DB entity to the cache model (e.g., ProjectMongoDB) before caching. + await _cache.AddProjectDetails(dbProject); + _logger.LogInfo("Updated cache with project {ProjectId}.", projectId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update cache for project {ProjectId} : ", projectId); + } + + // Map from the database entity to the response ViewModel. + return dbProject; + } + + private async Task UpdateCacheInBackground(Project project) + { + try + { + // This logic can be more complex, but the idea is to update or add. + var demo = await _cache.UpdateProjectDetailsOnly(project); + if (!demo) + { + await _cache.AddProjectDetails(project); + } + _logger.LogInfo("Background cache update succeeded for project {ProjectId}.", project.Id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Background cache update failed for project {ProjectId} ", project.Id); + } + } + + private async Task UpdateCacheAndNotify(Dictionary workDelta, List affectedItems) + { + try + { + // Update planned/completed work totals + var cacheUpdateTasks = workDelta.Select(kvp => + _cache.UpdatePlannedAndCompleteWorksInBuilding(kvp.Key, kvp.Value.Planned, kvp.Value.Completed)); + await Task.WhenAll(cacheUpdateTasks); + _logger.LogInfo("Background cache work totals update completed for {AreaCount} areas.", workDelta.Count); + + // Update the details of the individual work items in the cache + await _cache.ManageWorkItemDetails(affectedItems); + _logger.LogInfo("Background cache work item details update completed for {ItemCount} items.", affectedItems.Count); + + // Add SignalR notification logic here if needed + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred during background cache update/notification."); + } + } + + private void ProcessBuilding(BuildingDto dto, Guid tenantId, InfraVM responseData, List messages, ISet projectIds, List cacheTasks) + { + Building building = _mapper.Map(dto); + building.TenantId = tenantId; + + bool isNew = dto.Id == null; + if (isNew) + { + _context.Buildings.Add(building); + messages.Add("Building Added"); + cacheTasks.Add(_cache.AddBuildngInfra(building.ProjectId, building)); + } + else + { + _context.Buildings.Update(building); + messages.Add("Building Updated"); + cacheTasks.Add(_cache.UpdateBuildngInfra(building.ProjectId, building)); + } + + responseData.building = building; + projectIds.Add(building.ProjectId); + } + + private void ProcessFloor(FloorDto dto, Guid tenantId, InfraVM responseData, List messages, ISet projectIds, List cacheTasks, IDictionary buildings) + { + Floor floor = _mapper.Map(dto); + floor.TenantId = tenantId; + + // Use the pre-fetched dictionary for parent lookup. + Building? parentBuilding = buildings.TryGetValue(dto.BuildingId, out var b) ? b : null; + + bool isNew = dto.Id == null; + if (isNew) + { + _context.Floor.Add(floor); + messages.Add($"Floor Added in Building: {parentBuilding?.Name ?? "Unknown"}"); + cacheTasks.Add(_cache.AddBuildngInfra(parentBuilding?.ProjectId ?? Guid.Empty, floor: floor)); + } + else + { + _context.Floor.Update(floor); + messages.Add($"Floor Updated in Building: {parentBuilding?.Name ?? "Unknown"}"); + cacheTasks.Add(_cache.UpdateBuildngInfra(parentBuilding?.ProjectId ?? Guid.Empty, floor: floor)); + } + + responseData.floor = floor; + if (parentBuilding != null) projectIds.Add(parentBuilding.ProjectId); + } + + private void ProcessWorkArea(WorkAreaDto dto, Guid tenantId, InfraVM responseData, List messages, ISet projectIds, List cacheTasks, IDictionary floors) + { + WorkArea workArea = _mapper.Map(dto); + workArea.TenantId = tenantId; + + // Use the pre-fetched dictionary for parent lookup. + Floor? parentFloor = floors.TryGetValue(dto.FloorId, out var f) ? f : null; + var parentBuilding = parentFloor?.Building; + + bool isNew = dto.Id == null; + if (isNew) + { + _context.WorkAreas.Add(workArea); + messages.Add($"Work Area Added in Building: {parentBuilding?.Name ?? "Unknown"}, on Floor: {parentFloor?.FloorName ?? "Unknown"}"); + cacheTasks.Add(_cache.AddBuildngInfra(parentBuilding?.ProjectId ?? Guid.Empty, workArea: workArea, buildingId: parentBuilding?.Id)); + } + else + { + _context.WorkAreas.Update(workArea); + messages.Add($"Work Area Updated in Building: {parentBuilding?.Name ?? "Unknown"}, on Floor: {parentFloor?.FloorName ?? "Unknown"}"); + cacheTasks.Add(_cache.UpdateBuildngInfra(parentBuilding?.ProjectId ?? Guid.Empty, workArea: workArea, buildingId: parentBuilding?.Id)); + } + + responseData.workArea = workArea; + if (parentBuilding != null) projectIds.Add(parentBuilding.ProjectId); + } + + #endregion + } +} diff --git a/Marco.Pms.Services/Service/RefreshTokenService.cs b/Marco.Pms.Services/Service/RefreshTokenService.cs index 018de68..84ef3fd 100644 --- a/Marco.Pms.Services/Service/RefreshTokenService.cs +++ b/Marco.Pms.Services/Service/RefreshTokenService.cs @@ -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 - Console.WriteLine($"Token validation failed: {ex.Message}"); + _logger.LogError(ex, "Token validation failed"); return null; } } diff --git a/Marco.Pms.Services/Service/S3UploadService.cs b/Marco.Pms.Services/Service/S3UploadService.cs index c29cfdd..4ce7a4b 100644 --- a/Marco.Pms.Services/Service/S3UploadService.cs +++ b/Marco.Pms.Services/Service/S3UploadService.cs @@ -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; } } diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs new file mode 100644 index 0000000..b5acccc --- /dev/null +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs @@ -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> GetAllProjectsBasicAsync(Guid tenantId, Employee loggedInEmployee); + Task> GetAllProjectsAsync(Guid tenantId, Employee loggedInEmployee); + Task> GetProjectAsync(Guid id, Guid tenantId, Employee loggedInEmployee); + Task> GetProjectDetailsAsync(Guid id, Guid tenantId, Employee loggedInEmployee); + Task> GetProjectDetailsOldAsync(Guid id, Guid tenantId, Employee loggedInEmployee); + Task> CreateProjectAsync(CreateProjectDto projectDto, Guid tenantId, Employee loggedInEmployee); + Task> UpdateProjectAsync(Guid id, UpdateProjectDto updateProjectDto, Guid tenantId, Employee loggedInEmployee); + Task> GetEmployeeByProjectIdAsync(Guid? projectId, bool includeInactive, Guid tenantId, Employee loggedInEmployee); + Task> GetProjectAllocationAsync(Guid? projectId, Guid tenantId, Employee loggedInEmployee); + Task>> ManageAllocationAsync(List projectAllocationDots, Guid tenantId, Employee loggedInEmployee); + Task> GetProjectsByEmployeeAsync(Guid employeeId, Guid tenantId, Employee loggedInEmployee); + Task>> AssigneProjectsToEmployeeAsync(List projectAllocationDtos, Guid employeeId, Guid tenantId, Employee loggedInEmployee); + Task> GetInfraDetailsAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee); + Task> GetWorkItemsAsync(Guid workAreaId, Guid tenantId, Employee loggedInEmployee); + Task ManageProjectInfraAsync(List infraDtos, Guid tenantId, Employee loggedInEmployee); + Task>> CreateProjectTaskAsync(List workItemDtos, Guid tenantId, Employee loggedInEmployee); + Task DeleteProjectTaskAsync(Guid id, Guid tenantId, Employee loggedInEmployee); + + Task> GetAllProjectByTanentID(Guid tanentId); + Task> GetProjectByEmployeeID(Guid employeeId); + Task> GetTeamByProject(Guid TenantId, Guid ProjectId, bool IncludeInactive); + Task> GetMyProjectIdsAsync(Guid tenantId, Employee LoggedInEmployee); + + } +} diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/ISignalRService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/ISignalRService.cs new file mode 100644 index 0000000..c37322b --- /dev/null +++ b/Marco.Pms.Services/Service/ServiceInterfaces/ISignalRService.cs @@ -0,0 +1,7 @@ +namespace Marco.Pms.Services.Service.ServiceInterfaces +{ + public interface ISignalRService + { + Task SendNotificationAsync(object notification); + } +} diff --git a/Marco.Pms.Services/Service/SignalRService.cs b/Marco.Pms.Services/Service/SignalRService.cs new file mode 100644 index 0000000..fecc9b0 --- /dev/null +++ b/Marco.Pms.Services/Service/SignalRService.cs @@ -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 _signalR; + private readonly ILoggingService _logger; + public SignalRService(IHubContext 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"); + } + } + } +} diff --git a/Marco.Pms.Services/appsettings.Development.json b/Marco.Pms.Services/appsettings.Development.json index 1565018..030c450 100644 --- a/Marco.Pms.Services/appsettings.Development.json +++ b/Marco.Pms.Services/appsettings.Development.json @@ -47,6 +47,7 @@ "BucketName": "testenv-marco-pms-documents" }, "MongoDB": { - "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs" + "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs", + "ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches?socketTimeoutMS=500&serverSelectionTimeoutMS=500&connectTimeoutMS=500" } } diff --git a/Marco.Pms.Services/appsettings.Production.json b/Marco.Pms.Services/appsettings.Production.json index 81aa998..0abe3f1 100644 --- a/Marco.Pms.Services/appsettings.Production.json +++ b/Marco.Pms.Services/appsettings.Production.json @@ -6,7 +6,7 @@ }, "Environment": { "Name": "Production", - "Title": "" + "Title": "" }, "ConnectionStrings": { "DefaultConnectionString": "Server=147.93.98.152;User ID=devuser;Password=AppUser@123$;Database=MarcoBMS1" @@ -40,6 +40,7 @@ "BucketName": "testenv-marco-pms-documents" }, "MongoDB": { - "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs" + "SerilogDatabaseUrl": "mongodb://localhost:27017/DotNetLogs", + "ConnectionString": "mongodb://localhost:27017/MarcoBMS_Caches" } } \ No newline at end of file diff --git a/marco.pms.api.sln b/marco.pms.api.sln index 49d3e8c..424b709 100644 --- a/marco.pms.api.sln +++ b/marco.pms.api.sln @@ -11,6 +11,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Marco.Pms.Utility", "Marco. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Marco.Pms.Services", "Marco.Pms.Services\Marco.Pms.Services.csproj", "{27A83653-5B7F-4135-9886-01594D54AFAE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Marco.Pms.CacheHelper", "Marco.Pms.CacheHelper\Marco.Pms.CacheHelper.csproj", "{1A105C22-4ED7-4F54-8834-6923DDD96852}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,6 +35,10 @@ Global {27A83653-5B7F-4135-9886-01594D54AFAE}.Debug|Any CPU.Build.0 = Debug|Any CPU {27A83653-5B7F-4135-9886-01594D54AFAE}.Release|Any CPU.ActiveCfg = Release|Any CPU {27A83653-5B7F-4135-9886-01594D54AFAE}.Release|Any CPU.Build.0 = Release|Any CPU + {1A105C22-4ED7-4F54-8834-6923DDD96852}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A105C22-4ED7-4F54-8834-6923DDD96852}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A105C22-4ED7-4F54-8834-6923DDD96852}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A105C22-4ED7-4F54-8834-6923DDD96852}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE