using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Activities; using Marco.Pms.Model.Dtos.Attendance; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Expenses; 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; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Globalization; namespace Marco.Pms.Services.Controllers { [Authorize] [Route("api/[controller]")] [ApiController] public class DashboardController : ControllerBase { private readonly ApplicationDbContext _context; private readonly UserHelper _userHelper; private readonly IProjectServices _projectServices; private readonly ILoggingService _logger; private readonly PermissionServices _permissionServices; private readonly IServiceScopeFactory _serviceScopeFactory; public static readonly Guid ActiveId = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731"); private static readonly Guid Draft = Guid.Parse("297e0d8f-f668-41b5-bfea-e03b354251c8"); private static readonly Guid Review = Guid.Parse("6537018f-f4e9-4cb3-a210-6c3b2da999d7"); private static readonly Guid Approve = Guid.Parse("4068007f-c92f-4f37-a907-bc15fe57d4d8"); private static readonly Guid ProcessPending = Guid.Parse("f18c5cfd-7815-4341-8da2-2c2d65778e27"); private static readonly Guid Processed = Guid.Parse("61578360-3a49-4c34-8604-7b35a3787b95"); private static readonly Guid RejectedByReviewer = Guid.Parse("965eda62-7907-4963-b4a1-657fb0b2724b"); private static readonly Guid RejectedByApprover = Guid.Parse("d1ee5eec-24b6-4364-8673-a8f859c60729"); private readonly Guid tenantId; public DashboardController(ApplicationDbContext context, UserHelper userHelper, IProjectServices projectServices, IServiceScopeFactory serviceScopeFactory, ILoggingService logger, PermissionServices permissionServices) { _context = context; _userHelper = userHelper; _projectServices = projectServices; _logger = logger; _serviceScopeFactory = serviceScopeFactory; _permissionServices = permissionServices; tenantId = userHelper.GetTenantId(); } [HttpGet("progression")] public async Task GetGraph([FromQuery] double days, [FromQuery] string FromDate, [FromQuery] Guid? projectId) { var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); DateTime fromDate = new DateTime(); DateTime toDate = new DateTime(); List? projectProgressionVMs = new List(); if (FromDate != null && DateTime.TryParse(FromDate, out fromDate) == false) { return BadRequest(ApiResponse.ErrorResponse("Invalid starting date.", "Invalid starting date.", 400)); } var firstTask = await _context.TaskAllocations.Select(t => new { t.TenantId, t.AssignmentDate }).FirstOrDefaultAsync(t => t.TenantId == tenantId); if (FromDate == null) fromDate = DateTime.UtcNow.Date; if (firstTask == null) firstTask = new { TenantId = tenantId, AssignmentDate = DateTime.UtcNow }; if (days >= 0) { double negativeDays = 0 - days; toDate = fromDate.AddDays(negativeDays); if (firstTask != null && (firstTask.AssignmentDate.Date >= toDate.Date)) { toDate = firstTask.AssignmentDate; } if (projectId == null) { List tasks = await _context.TaskAllocations.Where(t => t.AssignmentDate.Date <= fromDate.Date && t.AssignmentDate.Date >= toDate.Date && t.TenantId == tenantId).ToListAsync(); double flagDays = 0; while (negativeDays < flagDays) { ProjectProgressionVM ProjectProgressionVM = new ProjectProgressionVM(); ProjectProgressionVM.ProjectId = projectId != null ? projectId.Value : Guid.Empty; ProjectProgressionVM.ProjectName = ""; var date = fromDate.AddDays(flagDays); if (date >= (firstTask != null ? firstTask.AssignmentDate.Date : null)) { var todayTasks = tasks.Where(t => t.AssignmentDate.Date == date.Date).ToList(); double plannedTaks = 0; double completedTasks = 0; ProjectProgressionVM.Date = date; foreach (var task in todayTasks) { plannedTaks += task.PlannedTask; completedTasks += task.CompletedTask; } ProjectProgressionVM.PlannedTask = plannedTaks; ProjectProgressionVM.CompletedTask = completedTasks; projectProgressionVMs.Add(ProjectProgressionVM); } flagDays -= 1; } _logger.LogInfo("Project Progression report for all projects fetched successfully by employee {EmployeeId}", LoggedInEmployee.Id); } else { var project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == projectId); List buildings = await _context.Buildings.Where(b => b.ProjectId == projectId && 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).ToListAsync(); idList = workItems.Select(i => i.Id).ToList(); List tasks = await _context.TaskAllocations.Where(t => idList.Contains(t.WorkItemId) && t.AssignmentDate.Date <= fromDate.Date && t.AssignmentDate.Date >= toDate.Date && t.TenantId == tenantId).ToListAsync(); if (project != null) { double flagDays = 0; while (negativeDays < flagDays) { ProjectProgressionVM projectProgressionVM = new ProjectProgressionVM(); projectProgressionVM.ProjectId = projectId.Value; projectProgressionVM.ProjectName = project.Name; var date = fromDate.AddDays(flagDays); if (date >= (firstTask != null ? firstTask.AssignmentDate.Date : null)) { var todayTasks = tasks.Where(t => t.AssignmentDate.Date == date.Date).ToList(); double plannedTaks = 0; double completedTasks = 0; projectProgressionVM.Date = date; foreach (var task in todayTasks) { plannedTaks += task.PlannedTask; completedTasks += task.CompletedTask; } projectProgressionVM.PlannedTask = plannedTaks; projectProgressionVM.CompletedTask = completedTasks; projectProgressionVMs.Add(projectProgressionVM); } flagDays -= 1; } } _logger.LogInfo("Project Progression for project {ProjectId} fetched successfully by employee {EmployeeId}", projectId, LoggedInEmployee.Id); } } return Ok(ApiResponse.SuccessResponse(projectProgressionVMs, "Success", 200)); } [HttpGet("projects")] public async Task GetProjectCount() { var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var projects = await _context.Projects.Where(p => p.TenantId == tenantId).ToListAsync(); var projectStatus = await _context.StatusMasters.Where(s => s.Status == "Active" || s.Status == "In Progress").ToListAsync(); var projectStatusIds = projectStatus.Select(s => s.Id).ToList(); var ongoingProjects = projects.Where(p => projectStatusIds.Contains(p.ProjectStatusId)).ToList(); ProjectDashboardVM projectDashboardVM = new ProjectDashboardVM { TotalProjects = projects.Count(), OngoingProjects = ongoingProjects.Count() }; _logger.LogInfo("Number of total ongoing projects fetched by employee {EmployeeId}", LoggedInEmployee.Id); 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([FromQuery] Guid? projectId) { try { var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); _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)); } } /// /// 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 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() { var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var attendance = await _context.Attendes.Where(a => a.EmployeeId == LoggedInEmployee.Id && a.TenantId == tenantId).ToListAsync(); if (attendance.Any()) { var pendingRegularization = attendance.Where(a => a.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE).ToList().Count; var pendingCheckOut = attendance.Where(a => a.OutTime == null).ToList().Count; var response = new { PendingRegularization = pendingRegularization, PendingCheckOut = pendingCheckOut }; _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.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)); } [HttpGet("project-attendance/{projectId}")] public async Task GetProjectAttendance(Guid projectId, [FromQuery] string? date) { var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); DateTime currentDate = DateTime.UtcNow; List? projectProgressionVMs = new List(); if (date != null && DateTime.TryParse(date, out currentDate) == false) { _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.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(); var employeeIds = projectAllocation.Select(p => p.EmployeeId).Distinct().ToList(); List? employees = await _context.Employees.Where(e => employeeIds.Contains(e.Id)).ToListAsync(); var attendances = await _context.Attendes.Where(a => employeeIds.Contains(a.EmployeeId) && a.ProjectID == projectId && a.InTime.HasValue && a.InTime.Value.Date == currentDate.Date).ToListAsync(); List employeeAttendanceVMs = new List(); foreach (var attendance in attendances) { Employee? employee = employees.FirstOrDefault(e => e.Id == attendance.EmployeeId); if (employee != null) { EmployeeAttendanceVM employeeAttendanceVM = new EmployeeAttendanceVM { FirstName = employee.FirstName, LastName = employee.LastName, MiddleName = employee.MiddleName, Comment = attendance.Comment, InTime = attendance.InTime, OutTime = attendance.OutTime }; employeeAttendanceVMs.Add(employeeAttendanceVM); } } ProjectAttendanceVM projectAttendanceVM = new ProjectAttendanceVM(); projectAttendanceVM.AttendanceTable = employeeAttendanceVMs; projectAttendanceVM.CheckedInEmployee = attendances.Count; projectAttendanceVM.AssignedEmployee = employeeIds.Count; _logger.LogInfo($"Attendance record for project {projectId} for date {currentDate.Date} by employee {LoggedInEmployee.Id}"); return Ok(ApiResponse.SuccessResponse(projectAttendanceVM, $"Attendance record for project {project.Name} for date {currentDate.Date}", 200)); } [HttpGet("activities/{projectId}")] public async Task GetActivities(Guid projectId, [FromQuery] string? date) { var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); DateTime currentDate = DateTime.UtcNow; if (date != null && DateTime.TryParse(date, out currentDate) == false) { _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.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)); } var buildings = await _context.Buildings.Where(b => b.ProjectId == project.Id).ToListAsync(); var buildingIds = buildings.Select(b => b.Id).Distinct().ToList(); var floors = await _context.Floor.Where(f => buildingIds.Contains(f.BuildingId)).ToListAsync(); var floorIds = floors.Select(f => f.Id).Distinct().ToList(); var areas = await _context.WorkAreas.Where(a => floorIds.Contains(a.FloorId)).ToListAsync(); var areaIds = areas.Select(a => a.Id).Distinct().ToList(); var workItems = await _context.WorkItems.Include(i => i.ActivityMaster).Where(i => areaIds.Contains(i.WorkAreaId)).ToListAsync(); var itemIds = workItems.Select(i => i.Id).Distinct().ToList(); var tasks = await _context.TaskAllocations.Where(t => itemIds.Contains(t.WorkItemId) && t.AssignmentDate.Date == currentDate.Date).ToListAsync(); double totalPlannedTask = 0; double totalCompletedTask = 0; List performedActivites = new List(); foreach (var task in tasks) { totalPlannedTask += task.PlannedTask; totalCompletedTask += task.CompletedTask; WorkItem workItem = workItems.FirstOrDefault(i => i.Id == task.WorkItemId) ?? new WorkItem(); string activityName = (workItem.ActivityMaster != null ? workItem.ActivityMaster.ActivityName : "") ?? ""; WorkArea workArea = areas.FirstOrDefault(a => a.Id == workItem.WorkAreaId) ?? new WorkArea(); string areaName = workArea.AreaName ?? ""; Floor floor = floors.FirstOrDefault(f => f.Id == workArea.FloorId) ?? new Floor(); string floorName = floor.FloorName ?? ""; Building building = buildings.FirstOrDefault(b => b.Id == floor.BuildingId) ?? new Building(); string buildingName = building.Name ?? ""; PerformedActivites performedTask = new PerformedActivites { ActivityName = activityName, BuldingName = buildingName, FloorName = floorName, WorkAreaName = areaName, AssignedToday = task.PlannedTask, CompletedToday = task.CompletedTask, }; performedActivites.Add(performedTask); } var pendingReport = tasks.Where(t => t.ReportedDate == null).ToList().Count; ActivityReport report = new ActivityReport { PerformedActivites = performedActivites, TotalCompletedWork = totalCompletedTask, TotalPlannedWork = totalPlannedTask, ReportPending = pendingReport, TodaysAssigned = tasks.Count }; _logger.LogInfo($"Record of performed activities for project {projectId} for date {currentDate.Date} by employee {LoggedInEmployee.Id}"); return Ok(ApiResponse.SuccessResponse(report, $"Record of performed activities for project {project.Name} for date {currentDate.Date}", 200)); } [HttpGet("attendance-overview/{projectId}")] public async Task GetAttendanceOverView(Guid projectId, [FromQuery] string days) { _logger.LogInfo("GetAttendanceOverView called for ProjectId: {ProjectId}, Days: {Days}", projectId, days); // Step 1: Validate project existence var project = await _context.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == projectId); if (project == null) { _logger.LogWarning("Project not found for ProjectId: {ProjectId}", projectId); return BadRequest(ApiResponse.ErrorResponse("Project not found", "Project not found", 400)); } // Step 2: Permission check var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); bool hasAssigned = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId); if (!hasAssigned) { _logger.LogWarning("Unauthorized access attempt. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployee.Id, projectId); return StatusCode(403, ApiResponse.ErrorResponse( "You don't have permission to access this feature", "You don't have permission to access this feature", 403)); } // Step 3: Validate and parse days input if (!int.TryParse(days, out int dayCount) || dayCount <= 0) { _logger.LogWarning("Invalid days input received: {Days}", days); return BadRequest(ApiResponse.ErrorResponse("Invalid number of days", "Days must be a positive integer", 400)); } // Step 4: Define date range DateTime today = DateTime.UtcNow.Date; DateTime startDate = today.AddDays(-dayCount); // Step 5: Load project allocations and related job roles var allocations = await _context.ProjectAllocations .Where(pa => pa.ProjectId == projectId) .ToListAsync(); if (!allocations.Any()) { _logger.LogInfo("No employee allocations found for project: {ProjectId}", projectId); return Ok(ApiResponse.SuccessResponse(new List(), "No allocations found", 200)); } var jobRoleIds = allocations.Select(pa => pa.JobRoleId).Distinct().ToList(); var jobRoles = await _context.JobRoles .Where(jr => jobRoleIds.Contains(jr.Id)) .ToListAsync(); // Step 6: Load attendance records for given date range var attendances = await _context.Attendes .Where(a => a.ProjectID == projectId && a.InTime.HasValue && a.InTime.Value.Date >= startDate && a.InTime.Value.Date <= today) .ToListAsync(); var overviewList = new List(); // Step 7: Process attendance per date per role for (DateTime date = today; date > startDate; date = date.AddDays(-1)) { foreach (var jobRole in jobRoles) { var employeeIds = allocations .Where(pa => pa.JobRoleId == jobRole.Id) .Select(pa => pa.EmployeeId) .ToList(); int presentCount = attendances .Count(a => employeeIds.Contains(a.EmployeeId) && a.InTime!.Value.Date == date); overviewList.Add(new AttendanceOverviewVM { Role = jobRole.Name, Date = date.ToString("yyyy-MM-dd"), Present = presentCount }); } } // Step 8: Order result for consistent presentation var sortedResult = overviewList .OrderByDescending(r => r.Date) .ThenByDescending(r => r.Present) .ToList(); _logger.LogInfo("Attendance overview fetched. ProjectId: {ProjectId}, Records: {Count}", projectId, sortedResult.Count); return Ok(ApiResponse.SuccessResponse(sortedResult, $"{sortedResult.Count} records fetched for attendance overview", 200)); } [HttpGet("expense/monthly")] public async Task GetExpenseReportByProjectsAsync([FromQuery] Guid? projectId, [FromQuery] Guid? categoryId, [FromQuery] int months) { try { // Read-only base filter with tenant scope and non-draft var baseQuery = _context.Expenses .AsNoTracking() .Where(e => e.TenantId == tenantId && e.IsActive && e.StatusId != Draft); // [Server Filters] if (months != 0) { months = 0 - months; var end = DateTime.UtcNow.Date; var start = end.AddMonths(months); // inclusive EOD baseQuery = baseQuery.Where(e => e.TransactionDate >= start && e.TransactionDate <= end); } if (projectId.HasValue) baseQuery = baseQuery.Where(e => e.ProjectId == projectId); if (categoryId.HasValue) baseQuery = baseQuery.Where(e => e.ExpensesTypeId == categoryId); // Single server-side group/aggregate by project var report = await baseQuery .AsNoTracking() .GroupBy(e => new { e.TransactionDate.Year, e.TransactionDate.Month }) .Select(g => new { Year = g.Key.Year, Month = g.Key.Month, Total = g.Sum(x => x.Amount), Count = g.Count() }) .OrderBy(x => x.Year).ThenBy(x => x.Month) .ToListAsync(); var culture = CultureInfo.GetCultureInfo("en-IN"); // pick desired locale var response = report .Select(x => new { MonthName = culture.DateTimeFormat.GetMonthName(x.Month), // e.g., "January" Year = x.Year, Total = x.Total, Count = x.Count }).ToList(); _logger.LogInfo( "GetExpenseReportByProjects completed. TenantId={TenantId}, Rows={Rows}", tenantId, report.Count); // [Completion Log] return Ok(ApiResponse.SuccessResponse(response, "Expense report by project fetched successfully", 200)); // [Success Response] } catch (OperationCanceledException) { _logger.LogWarning("GetExpenseReportByProjects canceled by client. TenantId={TenantId}", tenantId); // [Cancel Log] return StatusCode(499, ApiResponse.ErrorResponse("Client has canceled the opration", "Client has canceled the opration", 499)); // [Cancel Response] } catch (Exception ex) { _logger.LogError(ex, "GetExpenseReportByProjects failed. TenantId={TenantId}", tenantId); // [Error Log] return StatusCode(500, ApiResponse.ErrorResponse("An error occurred while fetching the expense report.", 500)); // [Error Response] } } [HttpGet("expense/type")] public async Task GetExpenseReportByExpenseTypeAsync([FromQuery] Guid? projectId, [FromQuery] DateTime startDate, [FromQuery] DateTime endDate) { // Structured log: entering action with filters _logger.LogDebug( "GetExpenseReportByExpenseType started. TenantId={TenantId}, ProjectId={ProjectId}, StartDate={StartDate}, EndDate={EndDate}", tenantId, projectId ?? Guid.Empty, startDate, endDate); // [Start Log] [memory:4][memory:1] try { // Compose base query: push filters to DB, avoid client evaluation IQueryable baseQuery = _context.Expenses .AsNoTracking() // Reduce tracking overhead for read-only endpoint .Where(e => e.TenantId == tenantId && e.IsActive && e.StatusId != Draft && e.TransactionDate >= startDate && e.TransactionDate <= endDate.AddDays(1).AddTicks(-1)); if (projectId.HasValue) baseQuery = baseQuery.Where(e => e.ProjectId == projectId.Value); // [Filter] [memory:7] // Project to a minimal shape before grouping to avoid loading navigation graphs // Group by expense type name; adjust to the correct key if ExpensesCategory is an enum or navigation var query = baseQuery .Where(e => e.ExpensesType != null) .Select(e => new { ExpenseTypeName = e.ExpensesType!.Name, // If enum, use e.ExpensesCategory.ToString() Amount = e.Amount, StatusId = e.StatusId }) .GroupBy(x => x.ExpenseTypeName) .Select(g => new { ProjectName = g.Key, // Original code used g.Key!.Name; here the grouping key is already a string TotalApprovedAmount = g.Where(x => x.StatusId == Processed || x.StatusId == ProcessPending).Sum(x => x.Amount), TotalPendingAmount = g.Where(x => x.StatusId != Processed && x.StatusId != RejectedByReviewer && x.StatusId != RejectedByApprover) .Sum(x => x.Amount), TotalRejectedAmount = g.Where(x => x.StatusId == RejectedByReviewer || x.StatusId == RejectedByApprover) .Sum(x => x.Amount), TotalProcessedAmount = g.Where(x => x.StatusId == Processed) .Sum(x => x.Amount) }) .OrderBy(r => r.ProjectName); // Server-side order [memory:7] var report = await query.ToListAsync(); // Single round-trip [memory:7] var response = new { Report = report, TotalAmount = report.Sum(r => r.TotalApprovedAmount) }; _logger.LogInfo( "GetExpenseReportByExpenseType completed. TenantId={TenantId}, Filters: ProjectId={ProjectId}, StartDate={StartDate}, EndDate={EndDate}, Rows={RowCount}, TotalAmount={TotalAmount}", tenantId, projectId ?? Guid.Empty, startDate, endDate, report.Count, response.TotalAmount); // [Completion Log] [memory:4] return Ok(ApiResponse.SuccessResponse(response, "Expense report by expense type fetched successfully", 200)); // [Success Response] [memory:1] } catch (OperationCanceledException) { _logger.LogWarning("GetExpenseReportByExpenseType canceled by client. TenantId={TenantId}", tenantId); // [Cancel Log] [memory:4] return StatusCode(499, ApiResponse.ErrorResponse("Client has canceled the opration", "Client has canceled the opration", 499)); // [Cancel Response] [memory:1] } catch (Exception ex) { _logger.LogError(ex, "GetExpenseReportByExpenseType failed. TenantId={TenantId}, ProjectId={ProjectId}, StartDate={StartDate}, EndDate={EndDate}", tenantId, projectId ?? Guid.Empty, startDate, endDate); // [Error Log] [memory:4] return StatusCode(StatusCodes.Status500InternalServerError, ApiResponse.ErrorResponse("An error occurred while fetching the expense report.", 500)); // [Error Response] [memory:1] } } [HttpGet("expense/pendings")] public async Task GetPendingExpenseListAsync([FromQuery] Guid? projectId) { // Start log with correlation fields _logger.LogDebug( "GetPendingExpenseListAsync started. Project={ProjectId} TenantId={TenantId}", projectId ?? Guid.Empty, tenantId); // [Start Log] try { // Resolve current employee once; avoid using scoped services inside Task.Run var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); // [User Context] // Resolve permission service from current scope once using var scope = _serviceScopeFactory.CreateScope(); // Fire permission checks concurrently without Task.Run; these are async I/O methods var hasReviewPermissionTask = Task.Run(async () => { var _permission = scope.ServiceProvider.GetRequiredService(); return await _permission.HasPermission(PermissionsMaster.ExpenseReview, loggedInEmployee.Id); }); var hasApprovePermissionTask = Task.Run(async () => { var _permission = scope.ServiceProvider.GetRequiredService(); return await _permission.HasPermission(PermissionsMaster.ExpenseApprove, loggedInEmployee.Id); }); var hasProcessPermissionTask = Task.Run(async () => { var _permission = scope.ServiceProvider.GetRequiredService(); return await _permission.HasPermission(PermissionsMaster.ExpenseProcess, loggedInEmployee.Id); }); var hasManagePermissionTask = Task.Run(async () => { var _permission = scope.ServiceProvider.GetRequiredService(); return await _permission.HasPermission(PermissionsMaster.ExpenseManage, loggedInEmployee.Id); }); await Task.WhenAll(hasReviewPermissionTask, hasApprovePermissionTask, hasProcessPermissionTask, hasManagePermissionTask); // [Parallel Await] var hasReviewPermission = hasReviewPermissionTask.Result; var hasApprovePermission = hasApprovePermissionTask.Result; var hasProcessPermission = hasProcessPermissionTask.Result; var hasManagePermission = hasManagePermissionTask.Result; _logger.LogInfo( "Permissions resolved: Review={Review}, Approve={Approve}, Process={Process}", hasReviewPermission, hasApprovePermission, hasProcessPermission); // [Permissions Log] // Build base query: read-only, tenant-scoped var baseQuery = _context.Expenses .Include(e => e.Status) .AsNoTracking() // Reduce tracking overhead for read-only list .Where(e => e.IsActive && e.TenantId == tenantId && e.StatusId != Processed && e.Status != null); // [Base Filter] // Project to DTO in SQL to avoid heavy Include graph. if (projectId.HasValue) baseQuery = baseQuery.Where(e => e.ProjectId == projectId); // Prefer ProjectTo when profiles exist; otherwise project minimal fields var expenses = await baseQuery .ToListAsync(); // Single round-trip; no Include needed for this shape var draftExpenses = expenses.Where(e => e.StatusId == Draft && e.CreatedById == loggedInEmployee.Id).ToList(); var reviewExpenses = expenses.Where(e => (hasReviewPermission || e.CreatedById == loggedInEmployee.Id) && e.StatusId == Review).ToList(); var approveExpenses = expenses.Where(e => (hasApprovePermission || e.CreatedById == loggedInEmployee.Id) && e.StatusId == Approve).ToList(); var processPendingExpenses = expenses.Where(e => (hasProcessPermission || e.CreatedById == loggedInEmployee.Id) && e.StatusId == ProcessPending).ToList(); var submitedExpenses = expenses.Where(e => e.StatusId != Draft && e.CreatedById == loggedInEmployee.Id).ToList(); var totalAmount = expenses.Where(e => e.StatusId != Draft).Sum(e => e.Amount); if (hasManagePermission) { var response = new { Draft = new { Count = draftExpenses.Count, TotalAmount = draftExpenses.Sum(e => e.Amount) }, ReviewPending = new { Count = reviewExpenses.Count, TotalAmount = reviewExpenses.Sum(e => e.Amount) }, ApprovePending = new { Count = approveExpenses.Count, TotalAmount = approveExpenses.Sum(e => e.Amount) }, ProcessPending = new { Count = processPendingExpenses.Count, TotalAmount = processPendingExpenses.Sum(e => e.Amount) }, Submited = new { Count = submitedExpenses.Count, TotalAmount = submitedExpenses.Sum(e => e.Amount) }, TotalAmount = totalAmount }; _logger.LogInfo( "GetPendingExpenseListAsync completed. TenantId={TenantId}", tenantId); // [Completion Log] return Ok(ApiResponse.SuccessResponse(response, "Pending Expenses fetched successfully", 200)); // [Success Response] } else { var response = new { Draft = new { Count = draftExpenses.Count }, ReviewPending = new { Count = reviewExpenses.Count }, ApprovePending = new { Count = approveExpenses.Count }, ProcessPending = new { Count = processPendingExpenses.Count }, Submited = new { Count = submitedExpenses.Count }, TotalAmount = totalAmount }; _logger.LogInfo( "GetPendingExpenseListAsync completed. TenantId={TenantId}", tenantId); // [Completion Log] return Ok(ApiResponse.SuccessResponse(response, "Pending Expenses fetched successfully", 200)); // [Success Response] } } catch (OperationCanceledException) { _logger.LogWarning("GetPendingExpenseListAsync canceled by client. TenantId={TenantId}", tenantId); // [Cancel Log] return StatusCode(499, ApiResponse.ErrorResponse("Client has canceled the opration", "Client has canceled the opration", 499)); // [Cancel Response] } catch (Exception ex) { _logger.LogError(ex, "GetPendingExpenseListAsync failed. TenantId={TenantId}", tenantId); // [Error Log] return StatusCode(500, ApiResponse.ErrorResponse("An error occurred while fetching pending expenses.", "An error occurred while fetching pending expenses.", 500)); // [Error Response] } } } }