From 2e299c615239b5e03d80374bd3af2a1d3b02e7c9 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 15 Oct 2025 13:00:17 +0530 Subject: [PATCH] Added the expense related dashboards --- .../Controllers/DashboardController.cs | 970 +++++++++++++----- 1 file changed, 718 insertions(+), 252 deletions(-) diff --git a/Marco.Pms.Services/Controllers/DashboardController.cs b/Marco.Pms.Services/Controllers/DashboardController.cs index b720eca..0afd219 100644 --- a/Marco.Pms.Services/Controllers/DashboardController.cs +++ b/Marco.Pms.Services/Controllers/DashboardController.cs @@ -1,8 +1,7 @@ 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.Projects; +using Marco.Pms.Model.Entitlements; +using Marco.Pms.Model.Expenses; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.DashBoard; using Marco.Pms.Services.Service; @@ -12,6 +11,7 @@ using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using System.Globalization; namespace Marco.Pms.Services.Controllers { @@ -25,145 +25,185 @@ namespace Marco.Pms.Services.Controllers 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"); - public DashboardController(ApplicationDbContext context, UserHelper userHelper, IProjectServices projectServices, ILoggingService logger, PermissionServices permissionServices) + 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(); } + /// + /// Fetches project progression data (planned and completed tasks) in graph form for a tenant and specified (or all) projects over a date range. + /// + /// Number of days back to fetch data for + /// Starting date for the graph, or today if null + /// Optionally, the project to filter on [HttpGet("progression")] - public async Task GetGraph([FromQuery] double days, [FromQuery] string FromDate, [FromQuery] Guid? projectId) + public async Task GetGraphAsync([FromQuery] double days, [FromQuery] string FromDate, [FromQuery] Guid? projectId) { - var tenantId = _userHelper.GetTenantId(); - 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) + if (tenantId == Guid.Empty) { + _logger.LogWarning("Invalid request: TenantId is empty on progression endpoint"); + return BadRequest(ApiResponse.ErrorResponse("Invalid TenantId", "Provided Invalid TenantId", 400)); + } + + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // Step 1: Parse and validate incoming date, fallback to today if null. + if (!string.IsNullOrWhiteSpace(FromDate) && !DateTime.TryParse(FromDate, out DateTime fromDate)) + { + _logger.LogWarning("Invalid starting date provided for progression graph by employee {EmployeeId}", loggedInEmployee.Id); 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 }; + DateTime fromDateValue = string.IsNullOrWhiteSpace(FromDate) ? DateTime.UtcNow.Date : DateTime.Parse(FromDate); + // Step 2: Get earliest task date for tenant, fallback to today + var firstTask = await _context.TaskAllocations + .Where(t => t.TenantId == tenantId) + .OrderBy(t => t.AssignmentDate) + .Select(t => t.AssignmentDate) + .FirstOrDefaultAsync(); + if (firstTask == default) firstTask = DateTime.UtcNow; - if (days >= 0) + // Step 3: Determine toDate (the oldest date in range) + double negativeDays = -Math.Abs(days); + DateTime toDate = fromDateValue.AddDays(negativeDays); + if (firstTask.Date >= toDate.Date) + toDate = firstTask; + + var projectProgressionVMs = new List(); + + // Step 4: Query depends on whether filtering for specific project + if (projectId == null) { - double negativeDays = 0 - days; - toDate = fromDate.AddDays(negativeDays); + // All projects for the tenant in range + var tasks = await _context.TaskAllocations + .Where(t => t.TenantId == tenantId && t.AssignmentDate.Date <= fromDateValue.Date && t.AssignmentDate.Date >= toDate.Date) + .ToListAsync(); - if (firstTask != null && (firstTask.AssignmentDate.Date >= toDate.Date)) + for (double flagDays = 0; negativeDays < flagDays; flagDays -= 1) { - 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) + var date = fromDateValue.AddDays(flagDays); + if (date >= firstTask.Date) { - 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); + projectProgressionVMs.Add(new ProjectProgressionVM { - 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; + ProjectId = Guid.Empty, + ProjectName = "", + Date = date, + PlannedTask = todayTasks.Sum(t => t.PlannedTask), + CompletedTask = todayTasks.Sum(t => t.CompletedTask), + }); } - _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); } + _logger.LogInfo("Project progression report for all projects fetched successfully by employee {EmployeeId}", loggedInEmployee.Id); } - return Ok(ApiResponse.SuccessResponse(projectProgressionVMs, "Success", 200)); + else + { + // Specified project: Fetch hierarchical data efficiently + var project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId); + if (project == null) + { + _logger.LogWarning("Project not found. ProjectId: {ProjectId}, TenantId: {TenantId}, Requested by: {EmployeeId}", projectId, tenantId, loggedInEmployee.Id); + return NotFound(ApiResponse.ErrorResponse("Project not found", "Specified project not found", 404)); + } + var tasks = await _context.TaskAllocations + .Include(t => t.WorkItem) + .ThenInclude(wi => wi!.WorkArea) + .ThenInclude(wa => wa!.Floor) + .ThenInclude(f => f!.Building) + .Where(t => + t.WorkItem != null && + t.WorkItem.WorkArea != null && + t.WorkItem.WorkArea.Floor != null && + t.WorkItem.WorkArea.Floor.Building != null && + t.WorkItem.WorkArea.Floor.Building.ProjectId == projectId && + t.TenantId == tenantId && + t.AssignmentDate.Date <= fromDateValue.Date && + t.AssignmentDate.Date >= toDate.Date) + .ToListAsync(); + + for (double flagDays = 0; negativeDays < flagDays; flagDays -= 1) + { + var date = fromDateValue.AddDays(flagDays); + if (date >= firstTask.Date) + { + var todayTasks = tasks.Where(t => t.AssignmentDate.Date == date.Date); + projectProgressionVMs.Add(new ProjectProgressionVM + { + ProjectId = projectId.Value, + ProjectName = project.Name, + Date = date, + PlannedTask = todayTasks.Sum(t => t.PlannedTask), + CompletedTask = todayTasks.Sum(t => t.CompletedTask), + }); + } + } + _logger.LogInfo("Project progression report for project {ProjectId} fetched successfully by employee {EmployeeId}", projectId, loggedInEmployee.Id); + } + + return Ok(ApiResponse.SuccessResponse(projectProgressionVMs, "Project progression data fetched successfully.", 200)); } + /// + /// Gets the count of total and ongoing projects for the current tenant, + /// using properly optimized queries and structured logging. + /// [HttpGet("projects")] - public async Task GetProjectCount() + public async Task GetProjectCountAsync() { - var tenantId = _userHelper.GetTenantId(); - 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 + // Step 1: Tenant validation (defensive coding for multi-tenancy) + if (tenantId == Guid.Empty) { - TotalProjects = projects.Count(), - OngoingProjects = ongoingProjects.Count() + _logger.LogWarning("Project count request denied: Empty TenantId."); + return BadRequest(ApiResponse.ErrorResponse("Invalid TenantId", "Provided Invalid TenantId", 400)); + } + + // Step 2: Get logged-in employee for logging/auditing purposes + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // Step 3: Fetch relevant status IDs only (query optimized for DB filtering) + var projectStatusIds = await _context.StatusMasters + .Where(s => s.Status == "Active" || s.Status == "In Progress") + .Select(s => s.Id) + .ToListAsync(); + + // Step 4: Query total and ongoing project counts directly (avoid .ToList()) + var totalProjectsCount = await _context.Projects + .CountAsync(p => p.TenantId == tenantId); + + var ongoingProjectsCount = await _context.Projects + .CountAsync(p => p.TenantId == tenantId && projectStatusIds.Contains(p.ProjectStatusId)); + + var dashboardVM = new ProjectDashboardVM + { + TotalProjects = totalProjectsCount, + OngoingProjects = ongoingProjectsCount }; - _logger.LogInfo("Number of total ongoing projects fetched by employee {EmployeeId}", LoggedInEmployee.Id); - return Ok(ApiResponse.SuccessResponse(projectDashboardVM, "Success", 200)); + + _logger.LogInfo("Fetched project counts: {TotalProjects} total, {OngoingProjects} ongoing. Employee: {EmployeeId}, TenantId: {TenantId}", + totalProjectsCount, ongoingProjectsCount, loggedInEmployee.Id, tenantId); + + return Ok(ApiResponse.SuccessResponse(dashboardVM, "Project counts fetched successfully.", 200)); } /// @@ -176,7 +216,12 @@ namespace Marco.Pms.Services.Controllers { try { - var tenantId = _userHelper.GetTenantId(); + if (tenantId == Guid.Empty) + { + _logger.LogWarning("Invalid request: TenantId is empty on progression endpoint"); + return BadRequest(ApiResponse.ErrorResponse("Invalid TenantId", "Provided Invalid TenantId", 400)); + } + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); _logger.LogInfo("GetTotalEmployees called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty); @@ -265,11 +310,16 @@ namespace Marco.Pms.Services.Controllers /// 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 + public async Task GetTotalTasks([FromQuery] Guid? projectId) // Changed to FromQuery as it's optional { try { - var tenantId = _userHelper.GetTenantId(); + if (tenantId == Guid.Empty) + { + _logger.LogWarning("Invalid request: TenantId is empty on progression endpoint"); + return BadRequest(ApiResponse.ErrorResponse("Invalid TenantId", "Provided Invalid TenantId", 400)); + } + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); _logger.LogInfo("GetTotalTasks called by user {UserId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId ?? Guid.Empty); @@ -348,238 +398,326 @@ namespace Marco.Pms.Services.Controllers return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred.", null, 500)); } } - [HttpGet("pending-attendance")] - public async Task GetPendingAttendance() - { - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var attendance = await _context.Attendes.Where(a => a.EmployeeId == LoggedInEmployee.Id && a.TenantId == tenantId).ToListAsync(); - if (attendance.Any()) + /// + /// Retrieves counts of pending attendance regularizations and check-outs for the logged-in employee. + /// + [HttpGet("pending-attendance")] + public async Task GetPendingAttendanceAsync() + { + // Step 1: Validate tenant context to prevent processing invalid requests + if (tenantId == Guid.Empty) { - 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("Invalid request: TenantId is empty when fetching pending attendance"); + return BadRequest(ApiResponse.ErrorResponse("Invalid TenantId", "Provided Invalid TenantId", 400)); } - _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)); + + // Step 2: Get currently logged-in employee for scoped attendance query + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // Step 3: Query attendance entries for employee scoped to tenant + var attendanceEntries = await _context.Attendes + .Where(a => a.EmployeeId == loggedInEmployee.Id && a.TenantId == tenantId) + .ToListAsync(); + + if (!attendanceEntries.Any()) + { + // Step 4: No attendance entries found for this employee, log and return 404 + _logger.LogWarning("No attendance entries found for employee {EmployeeId}", loggedInEmployee.Id); + return NotFound(ApiResponse.ErrorResponse("No attendance entry found.", "No attendance entry was found for this employee.", 404)); + } + // Step 5: Calculate counts for pending regularizations and check-outs efficiently + int pendingRegularizationCount = attendanceEntries.Count(a => a.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE); + int pendingCheckOutCount = attendanceEntries.Count(a => a.OutTime == null); + + var response = new + { + PendingRegularization = pendingRegularizationCount, + PendingCheckOut = pendingCheckOutCount + }; + + _logger.LogInfo("Pending regularization: {PendingRegularization}, Pending check-out: {PendingCheckOut} for employee {EmployeeId}", + pendingRegularizationCount, pendingCheckOutCount, loggedInEmployee.Id); + + return Ok(ApiResponse.SuccessResponse(response, "Pending regularization and pending check-out fetched successfully.", 200)); } + /// + /// Retrieves attendance records for a specific project on a given date. + /// + /// The project identifier + /// Optional date filter (defaults to current UTC date if null or invalid) [HttpGet("project-attendance/{projectId}")] public async Task GetProjectAttendance(Guid projectId, [FromQuery] string? date) { - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - DateTime currentDate = DateTime.UtcNow; - List? projectProgressionVMs = new List(); - if (date != null && DateTime.TryParse(date, out currentDate) == false) + // Step 1: Validate tenant context + if (tenantId == Guid.Empty) { - _logger.LogWarning($"user send invalid date"); - return BadRequest(ApiResponse.ErrorResponse("Invalid date.", "Invalid date.", 400)); - + _logger.LogWarning("GetProjectAttendance failed: TenantId is empty."); + return BadRequest(ApiResponse.ErrorResponse("Invalid TenantId", "Provided Invalid TenantId", 400)); } - Project? project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == projectId); + + // Step 2: Get logged-in employee for audit logging + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // Step 3: Parse and validate date parameter, default to UTC today if invalid or null + DateTime currentDate = DateTime.UtcNow.Date; + if (!string.IsNullOrWhiteSpace(date) && !DateTime.TryParse(date, out currentDate)) + { + _logger.LogWarning("Invalid date parameter '{Date}' sent by employee {EmployeeId}", date, loggedInEmployee.Id); + return BadRequest(ApiResponse.ErrorResponse("Invalid date.", "Invalid date format.", 400)); + } + + // Step 4: Verify project existence within tenant context + var project = await _context.Projects + .FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId); + 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)); + _logger.LogWarning("Project {ProjectId} not found for employee {EmployeeId} on date {Date}", projectId, loggedInEmployee.Id, currentDate); + return NotFound(ApiResponse.ErrorResponse("Project not found", "Specified project does not exist.", 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) + // Step 5: Fetch active employee assignments for the project and tenant + var employeeIds = await _context.ProjectAllocations + .Where(pa => pa.ProjectId == projectId && pa.IsActive && pa.TenantId == tenantId) + .Select(pa => pa.EmployeeId) + .Distinct() + .ToListAsync(); + + if (!employeeIds.Any()) { - - Employee? employee = employees.FirstOrDefault(e => e.Id == attendance.EmployeeId); - if (employee != null) - { - EmployeeAttendanceVM employeeAttendanceVM = new EmployeeAttendanceVM + _logger.LogInfo("No active employee assignments found for project {ProjectId} on date {Date}", projectId, currentDate); + return Ok(ApiResponse.SuccessResponse( + new ProjectAttendanceVM { - FirstName = employee.FirstName, - LastName = employee.LastName, - MiddleName = employee.MiddleName, - Comment = attendance.Comment, - InTime = attendance.InTime, - OutTime = attendance.OutTime - }; - - employeeAttendanceVMs.Add(employeeAttendanceVM); - } + AttendanceTable = new List(), + CheckedInEmployee = 0, + AssignedEmployee = 0 + }, + $"No active employee assignments found for project {project.Name} on {currentDate:d}.", 200)); } - 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)); + // Step 6: Bulk-fetch employees and their attendance on the specified date + var 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) + .ToListAsync(); + + // Step 7: Map attendance data to VM, joining attendance with employee info efficiently + var employeeAttendanceVMs = attendances + .Join(employees, + attendance => attendance.EmployeeId, + employee => employee.Id, + (attendance, employee) => new EmployeeAttendanceVM + { + FirstName = employee.FirstName, + LastName = employee.LastName, + MiddleName = employee.MiddleName, + Comment = attendance.Comment, + InTime = attendance.InTime, + OutTime = attendance.OutTime + }) + .ToList(); + + // Step 8: Prepare response VM with attendance counts + var projectAttendanceVM = new ProjectAttendanceVM + { + AttendanceTable = employeeAttendanceVMs, + CheckedInEmployee = employeeAttendanceVMs.Count, + AssignedEmployee = employeeIds.Count + }; + + _logger.LogInfo("Attendance record retrieved for project {ProjectId} on date {Date} by employee {EmployeeId}", projectId, currentDate, loggedInEmployee.Id); + + return Ok(ApiResponse.SuccessResponse(projectAttendanceVM, $"Attendance record for project {project.Name} on {currentDate:d} fetched successfully.", 200)); } + /// + /// Retrieves detailed performed activity records for a specific project on a given date. + /// + /// The project identifier + /// Optional date filter (defaults to current UTC date if null or invalid) [HttpGet("activities/{projectId}")] public async Task GetActivities(Guid projectId, [FromQuery] string? date) { - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - DateTime currentDate = DateTime.UtcNow; - if (date != null && DateTime.TryParse(date, out currentDate) == false) + // Step 1: Validate tenant context for security and data isolation + if (tenantId == Guid.Empty) { - _logger.LogWarning($"user send invalid date"); - return BadRequest(ApiResponse.ErrorResponse("Invalid date.", "Invalid date.", 400)); - + _logger.LogWarning("GetActivities request failed with empty TenantId."); + return BadRequest(ApiResponse.ErrorResponse("Invalid TenantId", "Provided Invalid TenantId", 400)); } - Project? project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == projectId); + + // Step 2: Fetch logged-in employee for audit + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + // Step 3: Parse and validate date parameter, defaulting to UTC today if invalid or not provided + DateTime currentDate = DateTime.UtcNow.Date; + if (!string.IsNullOrWhiteSpace(date) && !DateTime.TryParse(date, out currentDate)) + { + _logger.LogWarning("Invalid date parameter '{Date}' supplied by employee {EmployeeId}", date, loggedInEmployee.Id); + return BadRequest(ApiResponse.ErrorResponse("Invalid date.", "Invalid date format.", 400)); + } + + // Step 4: Verify project existence and tenant scope + var project = await _context.Projects + .FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId); + 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)); + _logger.LogWarning("ProjectId {ProjectId} not found for employee {EmployeeId} during activity retrieval on {Date}", projectId, loggedInEmployee.Id, currentDate); + return NotFound(ApiResponse.ErrorResponse("Project not found", "Specified project does not exist.", 404)); } - var buildings = await _context.Buildings.Where(b => b.ProjectId == project.Id).ToListAsync(); - var buildingIds = buildings.Select(b => b.Id).Distinct().ToList(); + // Step 5: Fetch tasks for the filtered work items on the specified date + var tasks = await _context.TaskAllocations + .Include(t => t.WorkItem) + .ThenInclude(wi => wi!.WorkArea) + .ThenInclude(wa => wa!.Floor) + .ThenInclude(f => f!.Building) + .Include(t => t.WorkItem) + .ThenInclude(wi => wi!.ActivityMaster) + .Where(t => t.WorkItem != null && + t.WorkItem.WorkArea != null && + t.WorkItem.WorkArea.Floor != null && + t.WorkItem.WorkArea.Floor.Building != null && + t.WorkItem.WorkArea.Floor.Building.ProjectId == projectId && + t.WorkItem.ActivityMaster != null && + t.AssignmentDate.Date == currentDate.Date && t.TenantId == tenantId) + .ToListAsync(); - 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(); + // Step 6: Aggregate totals and prepare performed activities list double totalPlannedTask = 0; double totalCompletedTask = 0; - List performedActivites = new List(); + var performedActivities = 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 + performedActivities.Add(new PerformedActivites { - ActivityName = activityName, - BuldingName = buildingName, - FloorName = floorName, - WorkAreaName = areaName, + ActivityName = task.WorkItem?.ActivityMaster?.ActivityName, + BuldingName = task.WorkItem?.WorkArea?.Floor?.Building?.Name, + FloorName = task.WorkItem?.WorkArea?.Floor?.FloorName, + WorkAreaName = task.WorkItem?.WorkArea?.AreaName, AssignedToday = task.PlannedTask, CompletedToday = task.CompletedTask, - }; - performedActivites.Add(performedTask); + }); } - var pendingReport = tasks.Where(t => t.ReportedDate == null).ToList().Count; - ActivityReport report = new ActivityReport + // Step 7: Count tasks with no reported date + int pendingReportCount = tasks.Count(t => t.ReportedDate == null); + + // Step 8: Assemble final activity report + var activityReport = new ActivityReport { - PerformedActivites = performedActivites, + PerformedActivites = performedActivities, TotalCompletedWork = totalCompletedTask, TotalPlannedWork = totalPlannedTask, - ReportPending = pendingReport, + ReportPending = pendingReportCount, 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)); + + // Step 9: Log success and respond + _logger.LogInfo("Activities retrieved for project {ProjectId} on {Date} by employee {EmployeeId}", projectId, currentDate, loggedInEmployee.Id); + + return Ok(ApiResponse.SuccessResponse(activityReport, $"Performed activities for project {project.Name} on {currentDate:d} retrieved successfully.", 200)); } + /// + /// Retrieves attendance overview grouped by job roles for a project over a specified number of days. + /// + /// The project identifier + /// Number of past days to include (string input, parsed as int) [HttpGet("attendance-overview/{projectId}")] public async Task GetAttendanceOverView(Guid projectId, [FromQuery] string days) { + // Step 1: Validate tenant context for multi-tenant data isolation + if (tenantId == Guid.Empty) + { + _logger.LogWarning("Empty TenantId in AttendanceOverview request."); + return BadRequest(ApiResponse.ErrorResponse("Invalid TenantId", "Provided Invalid TenantId", 400)); + } + _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); + // Step 2: Validate project existence under tenant scope + var project = await _context.Projects + .AsNoTracking() + .FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId); if (project == null) { - _logger.LogWarning("Project not found for ProjectId: {ProjectId}", projectId); + _logger.LogWarning("Project not found: {ProjectId}", projectId); return BadRequest(ApiResponse.ErrorResponse("Project not found", "Project not found", 400)); } - // Step 2: Permission check + // Step 3: Check if logged-in employee has permission for this project var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - bool hasAssigned = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId); - - if (!hasAssigned) + bool hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee!, projectId); + if (!hasPermission) { - _logger.LogWarning("Unauthorized access attempt. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployee.Id, projectId); + _logger.LogWarning("Unauthorized access by EmployeeId: {EmployeeId} to 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)); + "Permission Denied", "You don't have permission to access this feature", 403)); } - // Step 3: Validate and parse days input + // Step 4: Validate and parse 'days' query parameter if (!int.TryParse(days, out int dayCount) || dayCount <= 0) { - _logger.LogWarning("Invalid days input received: {Days}", days); + _logger.LogWarning("Invalid 'days' parameter: {Days}", days); return BadRequest(ApiResponse.ErrorResponse("Invalid number of days", "Days must be a positive integer", 400)); } - // Step 4: Define date range + // Step 5: Define date range for attendance fetching DateTime today = DateTime.UtcNow.Date; DateTime startDate = today.AddDays(-dayCount); - // Step 5: Load project allocations and related job roles + // Step 6: Retrieve allocations and job roles for project and tenant var allocations = await _context.ProjectAllocations - .Where(pa => pa.ProjectId == projectId) + .Where(pa => pa.ProjectId == projectId && pa.TenantId == tenantId) .ToListAsync(); if (!allocations.Any()) { - _logger.LogInfo("No employee allocations found for project: {ProjectId}", projectId); + _logger.LogInfo("No allocations found for ProjectId: {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)) + .Where(jr => jobRoleIds.Contains(jr.Id) && jr.TenantId == tenantId) .ToListAsync(); - // Step 6: Load attendance records for given date range + // Step 7: Fetch attendance records within date range filtered by project and tenant var attendances = await _context.Attendes - .Where(a => - a.ProjectID == projectId && - a.InTime.HasValue && - a.InTime.Value.Date >= startDate && - a.InTime.Value.Date <= today) + .Where(a => a.ProjectID == projectId && + a.InTime.HasValue && + a.InTime.Value.Date >= startDate && + a.InTime.Value.Date <= today && + a.TenantId == tenantId) .ToListAsync(); + // Step 8: Group attendance by date and job role 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 + var employeeIdsByRole = 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); + .Count(a => employeeIdsByRole.Contains(a.EmployeeId) && a.InTime!.Value.Date == date); overviewList.Add(new AttendanceOverviewVM { @@ -590,15 +728,343 @@ namespace Marco.Pms.Services.Controllers } } - // Step 8: Order result for consistent presentation + // Step 9: Sort results by date desc and present count desc var sortedResult = overviewList .OrderByDescending(r => r.Date) .ThenByDescending(r => r.Present) .ToList(); - _logger.LogInfo("Attendance overview fetched. ProjectId: {ProjectId}, Records: {Count}", projectId, sortedResult.Count); + _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 + { + if (tenantId == Guid.Empty) + { + _logger.LogWarning("Invalid request: TenantId is empty on progression endpoint"); + return BadRequest(ApiResponse.ErrorResponse("Invalid TenantId", "Provided Invalid TenantId", 400)); + } + + // 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] + + if (tenantId == Guid.Empty) + { + _logger.LogWarning("Invalid request: TenantId is empty on progression endpoint"); + return BadRequest(ApiResponse.ErrorResponse("Invalid TenantId", "Provided Invalid TenantId", 400)); + } + + 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] + + // 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 + + var report = await query.ToListAsync(); // Single round-trip + + 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] + + return Ok(ApiResponse.SuccessResponse(response, "Expense report by expense type fetched successfully", 200)); // [Success Response] + } + 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] + } + catch (Exception ex) + { + _logger.LogError(ex, + "GetExpenseReportByExpenseType failed. TenantId={TenantId}, ProjectId={ProjectId}, StartDate={StartDate}, EndDate={EndDate}", + tenantId, projectId ?? Guid.Empty, startDate, endDate); // [Error Log] + return StatusCode(StatusCodes.Status500InternalServerError, + ApiResponse.ErrorResponse("An error occurred while fetching the expense report.", 500)); // [Error Response] + } + } + + [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 + { + if (tenantId == Guid.Empty) + { + _logger.LogWarning("Invalid request: TenantId is empty on progression endpoint"); + return BadRequest(ApiResponse.ErrorResponse("Invalid TenantId", "Provided Invalid TenantId", 400)); + } + + // 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] + } + } } }