diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index 23df64c..9b2036d 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -106,6 +106,7 @@ namespace Marco.Pms.CacheHelper workAreaMongoList.Add(new WorkAreaMongoDB { Id = wa.Id.ToString(), + FloorId = wa.FloorId.ToString(), AreaName = wa.AreaName, PlannedWork = waPlanned, CompletedWork = waCompleted @@ -118,6 +119,7 @@ namespace Marco.Pms.CacheHelper floorMongoList.Add(new FloorMongoDB { Id = floor.Id.ToString(), + BuildingId = floor.BuildingId.ToString(), FloorName = floor.FloorName, PlannedWork = floorPlanned, CompletedWork = floorCompleted, @@ -131,6 +133,7 @@ namespace Marco.Pms.CacheHelper buildingMongoList.Add(new BuildingMongoDB { Id = building.Id.ToString(), + ProjectId = building.ProjectId.ToString(), BuildingName = building.Name, Description = building.Description, PlannedWork = buildingPlanned, @@ -477,7 +480,59 @@ namespace Marco.Pms.CacheHelper var result = await _projetCollection.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 _projetCollection.Aggregate(pipeline).FirstOrDefaultAsync(); + if (result == null) + return null; + return result; + } + 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; + } // ------------------------------------------------------- WorkItem ------------------------------------------------------- @@ -485,12 +540,14 @@ namespace Marco.Pms.CacheHelper { var activityIds = workItems.Select(wi => wi.ActivityId).ToList(); var workCategoryIds = workItems.Select(wi => wi.WorkCategoryId).ToList(); + var workItemIds = workItems.Select(wi => wi.Id).ToList(); // fetching Activity master var activities = await _context.ActivityMasters.Where(a => activityIds.Contains(a.Id)).ToListAsync() ?? new List(); // Fetching Work Category var workCategories = await _context.WorkCategoryMasters.Where(wc => workCategoryIds.Contains(wc.Id)).ToListAsync() ?? new List(); - + var task = await _context.TaskAllocations.Where(t => workItemIds.Contains(t.WorkItemId) && t.AssignmentDate == DateTime.UtcNow).ToListAsync(); + var todaysAssign = task.Sum(t => t.PlannedTask); foreach (WorkItem workItem in workItems) { var activity = activities.FirstOrDefault(a => a.Id == workItem.ActivityId) ?? new ActivityMaster(); @@ -501,10 +558,11 @@ namespace Marco.Pms.CacheHelper 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, 0), + Builders.Update.Set(r => r.TodaysAssigned, todaysAssign), 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, new ActivityMasterMongoDB { Id = activity.Id.ToString(), @@ -520,6 +578,16 @@ namespace Marco.Pms.CacheHelper ); 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) diff --git a/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs b/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs index 64ccbce..786ceb5 100644 --- a/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/BuildingMongoDB.cs @@ -7,12 +7,16 @@ 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? Name { get; set; } + 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/FloorMongoDB.cs b/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs index 57257a4..15d3060 100644 --- a/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/FloorMongoDB.cs @@ -3,6 +3,7 @@ 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; } @@ -12,6 +13,9 @@ 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/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 index d17f52c..412c940 100644 --- a/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/WorkAreaMongoDB.cs @@ -3,6 +3,7 @@ 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; } diff --git a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs index 850300d..cf798f3 100644 --- a/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/WorkItemMongoDB.cs @@ -12,5 +12,6 @@ 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.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index fbc9bf6..fde715f 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -237,6 +237,10 @@ namespace MarcoBMS.Services.Controllers .Include(c => c.ProjectStatus) .FirstOrDefaultAsync(c => c.TenantId == tenantId && c.Id == id); projectVM = GetProjectViewModel(project); + if (project != null) + { + await _cache.AddProjectDetails(project); + } } else { @@ -927,7 +931,7 @@ namespace MarcoBMS.Services.Controllers var responseList = new List(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); string message = ""; - List projectIds = new List(); + List workAreaIds = new List(); var workItemIds = workItemDtos.Where(wi => wi.Id != null && wi.Id != Guid.Empty).Select(wi => wi.Id).ToList(); var workItems = await _context.WorkItems.AsNoTracking().Where(wi => workItemIds.Contains(wi.Id)).ToListAsync(); @@ -980,7 +984,7 @@ namespace MarcoBMS.Services.Controllers WorkItemId = workItem.Id, WorkItem = workItem }); - projectIds.Add(building.ProjectId); + workAreaIds.Add(workItem.WorkAreaId); } string responseMessage = ""; @@ -1007,7 +1011,7 @@ namespace MarcoBMS.Services.Controllers - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = message }; + var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = message }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); @@ -1019,7 +1023,7 @@ namespace MarcoBMS.Services.Controllers { Guid tenantId = _userHelper.GetTenantId(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - List projectIds = new List(); + List workAreaIds = new List(); WorkItem? task = await _context.WorkItems.AsNoTracking().Include(t => t.WorkArea).FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId); if (task != null) { @@ -1036,9 +1040,9 @@ namespace MarcoBMS.Services.Controllers var floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == floorId); - projectIds.Add(floor?.Building?.ProjectId ?? Guid.Empty); + workAreaIds.Add(task.WorkAreaId); - 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}" }; + var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, 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 diff --git a/Marco.Pms.Services/Controllers/ReportController.cs b/Marco.Pms.Services/Controllers/ReportController.cs index 8f8a790..11dec58 100644 --- a/Marco.Pms.Services/Controllers/ReportController.cs +++ b/Marco.Pms.Services/Controllers/ReportController.cs @@ -1,12 +1,10 @@ using System.Data; -using System.Globalization; using Marco.Pms.DataAccess.Data; -using Marco.Pms.Model.Dtos.Attendance; using Marco.Pms.Model.Dtos.Mail; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Mail; 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; @@ -26,13 +24,15 @@ 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; + public ReportController(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, UserHelper userHelper, IWebHostEnvironment env, ReportHelper reportHelper) { _context = context; _emailSender = emailSender; _logger = logger; _userHelper = userHelper; _env = env; + _reportHelper = reportHelper; } [HttpPost("set-mail")] @@ -151,7 +151,6 @@ namespace Marco.Pms.Services.Controllers /// 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) { - DateTime reportDate = DateTime.UtcNow.AddDays(-1).Date; if (projectId == Guid.Empty) { @@ -159,161 +158,15 @@ namespace Marco.Pms.Services.Controllers return ApiResponse.ErrorResponse("Provided empty Project ID.", "Provided empty Project ID.", 400); } - var project = await _context.Projects - .AsNoTracking() - .FirstOrDefaultAsync(p => p.Id == projectId); - if (project == null) + var statisticReport = await _reportHelper.GetDailyProjectReport(projectId, tenantId); + + if (statisticReport == null) { _logger.LogWarning("User attempted to fetch project progress for project ID {ProjectId} but not found.", projectId); return ApiResponse.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 jobRoleIds = projectAllocations.Select(pa => pa.JobRoleId).ToList(); - var jobRoles = await _context.JobRoles - .Where(j => j.TenantId == project.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); - 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(); diff --git a/Marco.Pms.Services/Dockerfile b/Marco.Pms.Services/Dockerfile index 77311ee..2aa24ea 100644 --- a/Marco.Pms.Services/Dockerfile +++ b/Marco.Pms.Services/Dockerfile @@ -19,7 +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.Utility/Marco.Pms.CacheHelper.csproj", "Marco.Pms.CacheHelper/"] +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 index 03fd397..216ec6e 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -120,6 +120,36 @@ namespace Marco.Pms.Services.Helpers _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; + } + } + 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; + } + } // ------------------------------------------------------- WorkItem ------------------------------------------------------- diff --git a/Marco.Pms.Services/Helpers/ReportHelper.cs b/Marco.Pms.Services/Helpers/ReportHelper.cs new file mode 100644 index 0000000..e7632fd --- /dev/null +++ b/Marco.Pms.Services/Helpers/ReportHelper.cs @@ -0,0 +1,274 @@ +using System.Globalization; +using Marco.Pms.DataAccess.Data; +using Marco.Pms.Model.Dtos.Attendance; +using Marco.Pms.Model.MongoDBModels; +using Marco.Pms.Model.ViewModels.Report; +using Microsoft.EntityFrameworkCore; + +namespace Marco.Pms.Services.Helpers +{ + public class ReportHelper + { + private readonly ApplicationDbContext _context; + private readonly CacheUpdateHelper _cache; + public ReportHelper(CacheUpdateHelper cache, ApplicationDbContext context) + { + _cache = cache; + _context = context; + } + public async Task GetDailyProjectReport(Guid projectId, Guid tenantId) + { + // await _cache.GetBuildingAndFloorByWorkAreaId(); + DateTime reportDate = DateTime.UtcNow.AddDays(-1).Date; + var project = await _cache.GetProjectDetails(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 == null) + { + 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 == null) + { + 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 == null) + { + 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 = 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; + } + } +} 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 1d9b4b3..30831c6 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -137,6 +137,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Marco.Pms.Services/Service/RefreshTokenService.cs b/Marco.Pms.Services/Service/RefreshTokenService.cs index 018de68..231e27c 100644 --- a/Marco.Pms.Services/Service/RefreshTokenService.cs +++ b/Marco.Pms.Services/Service/RefreshTokenService.cs @@ -218,7 +218,7 @@ namespace MarcoBMS.Services.Service catch (Exception ex) { // Token is invalid - Console.WriteLine($"Token validation failed: {ex.Message}"); + _logger.LogError($"Token validation failed: {ex.Message}"); return null; } }