From 777a72c71fb3d85ad69a945f3038c703165d0cde Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Sun, 21 Sep 2025 16:11:23 +0530 Subject: [PATCH] Filtered the employees in attendance by organization --- .../AttendanceVM/EmployeeAttendanceVM.cs | 1 + .../Controllers/AttendanceController.cs | 290 ++++++++++++------ Marco.Pms.Services/Service/ProjectServices.cs | 21 +- .../ServiceInterfaces/IProjectServices.cs | 2 +- 4 files changed, 204 insertions(+), 110 deletions(-) diff --git a/Marco.Pms.Model/ViewModels/AttendanceVM/EmployeeAttendanceVM.cs b/Marco.Pms.Model/ViewModels/AttendanceVM/EmployeeAttendanceVM.cs index e0d8893..9c2bfc8 100644 --- a/Marco.Pms.Model/ViewModels/AttendanceVM/EmployeeAttendanceVM.cs +++ b/Marco.Pms.Model/ViewModels/AttendanceVM/EmployeeAttendanceVM.cs @@ -9,6 +9,7 @@ namespace Marco.Pms.Model.ViewModels.AttendanceVM public string? FirstName { get; set; } public string? LastName { get; set; } public string? EmployeeAvatar { get; set; } + public string? OrganizationName { get; set; } public DateTime? CheckInTime { get; set; } public DateTime? CheckOutTime { get; set; } public string? JobRoleName { get; set; } diff --git a/Marco.Pms.Services/Controllers/AttendanceController.cs b/Marco.Pms.Services/Controllers/AttendanceController.cs index 0c9a67d..121b8a2 100644 --- a/Marco.Pms.Services/Controllers/AttendanceController.cs +++ b/Marco.Pms.Services/Controllers/AttendanceController.cs @@ -143,12 +143,21 @@ namespace MarcoBMS.Services.Controllers /// ProjectID /// YYYY-MM-dd /// + [HttpGet("project/log")] - public async Task EmployeeAttendanceByDateRange([FromQuery] Guid projectId, [FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null) + public async Task EmployeeAttendanceByDateRange([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null) { - Guid TenantId = GetTenantId(); + Guid tenantId = GetTenantId(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + var project = await _context.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId); + if (project == null) + { + _logger.LogWarning("Project {ProjectId} not found in database", projectId); + return NotFound(ApiResponse.ErrorResponse("Project not found.")); + } + var hasTeamAttendancePermission = await _permission.HasPermission(PermissionsMaster.TeamAttendance, LoggedInEmployee.Id); var hasSelfAttendancePermission = await _permission.HasPermission(PermissionsMaster.SelfAttendance, LoggedInEmployee.Id); var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId); @@ -188,10 +197,10 @@ namespace MarcoBMS.Services.Controllers if (hasTeamAttendancePermission) { - List lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date >= fromDate.Date && c.AttendanceDate.Date <= toDate.Date && c.TenantId == TenantId).ToListAsync(); + List lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date >= fromDate.Date && c.AttendanceDate.Date <= toDate.Date && c.TenantId == tenantId).ToListAsync(); - List projectteam = await _projectServices.GetTeamByProject(TenantId, projectId, true); + List projectteam = await _projectServices.GetTeamByProject(tenantId, projectId, organizationId, true); var jobRole = await _context.JobRoles.ToListAsync(); foreach (Attendance? attendance in lstAttendance) { @@ -212,12 +221,14 @@ namespace MarcoBMS.Services.Controllers result1.FirstName = teamMember.Employee.FirstName; result1.LastName = teamMember.Employee.LastName; result1.JobRoleName = teamMember.Employee.JobRole != null ? teamMember.Employee.JobRole.Name : null; + result1.OrganizationName = teamMember.Employee.Organization?.Name; } else { result1.FirstName = null; result1.LastName = null; result1.JobRoleName = null; + result1.OrganizationName = null; } result.Add(result1); @@ -227,8 +238,22 @@ namespace MarcoBMS.Services.Controllers } else if (hasSelfAttendancePermission) { - List lstAttendances = await _context.Attendes.Where(c => c.ProjectID == projectId && c.EmployeeID == LoggedInEmployee.Id && c.AttendanceDate.Date >= fromDate.Date && c.AttendanceDate.Date <= toDate.Date && c.TenantId == TenantId).ToListAsync(); - ProjectAllocation? projectAllocation = await _context.ProjectAllocations.Include(pa => pa.Employee).FirstOrDefaultAsync(pa => pa.ProjectId == projectId && pa.EmployeeId == LoggedInEmployee.Id && pa.TenantId == TenantId && pa.IsActive); + List lstAttendances = await _context.Attendes + .Where(c => c.ProjectID == projectId && c.EmployeeID == LoggedInEmployee.Id && c.AttendanceDate.Date >= fromDate.Date && c.AttendanceDate.Date <= toDate.Date && c.TenantId == tenantId) + .ToListAsync(); + + var projectAllocationQuery = _context.ProjectAllocations + .Include(pa => pa.Employee) + .ThenInclude(e => e!.Organization) + .Where(pa => pa.ProjectId == projectId && pa.EmployeeId == LoggedInEmployee.Id && pa.TenantId == tenantId && pa.IsActive); + + if (organizationId.HasValue) + { + projectAllocationQuery = projectAllocationQuery.Where(pa => pa.Employee != null && pa.Employee.OrganizationId == organizationId); + } + + var projectAllocation = await projectAllocationQuery.FirstOrDefaultAsync(); + foreach (var attendance in lstAttendances) { if (projectAllocation != null) @@ -241,6 +266,7 @@ namespace MarcoBMS.Services.Controllers FirstName = projectAllocation.Employee?.FirstName, LastName = projectAllocation.Employee?.LastName, JobRoleName = projectAllocation.Employee?.JobRole?.Name, + OrganizationName = projectAllocation.Employee?.Organization?.Name, CheckInTime = attendance.InTime, CheckOutTime = attendance.OutTime, Activity = attendance.Activity @@ -253,118 +279,79 @@ namespace MarcoBMS.Services.Controllers return Ok(ApiResponse.SuccessResponse(result, System.String.Format("{0} Attendance records fetched successfully", result.Count), 200)); } - /// - /// - /// - /// ProjectID - /// YYYY-MM-dd - /// [HttpGet("project/team")] - public async Task EmployeeAttendanceByProject([FromQuery] Guid projectId, [FromQuery] bool IncludeInActive, [FromQuery] string? date = null) + /// + /// Retrieves employee attendance records for a specified project and date. + /// The result is filtered based on the logged-in employee's permissions (Team or Self). + /// + /// The ID of the project. + /// Optional. Filters attendance for employees of a specific organization. + /// Optional. Includes inactive employees in the team list if true. + /// Optional. The date for which to fetch attendance, in "yyyy-MM-dd" format. Defaults to the current UTC date. + /// An IActionResult containing a list of employee attendance records or an error response. + public async Task EmployeeAttendanceByProjectAsync([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] bool includeInactive, [FromQuery] string? date = null) { - Guid TenantId = GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var hasTeamAttendancePermission = await _permission.HasPermission(PermissionsMaster.TeamAttendance, LoggedInEmployee.Id); - var hasSelfAttendancePermission = await _permission.HasPermission(PermissionsMaster.SelfAttendance, LoggedInEmployee.Id); - var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId); + var tenantId = GetTenantId(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - if (!hasProjectPermission) + // --- 1. Initial Validation and Permission Checks --- + _logger.LogInfo("Fetching attendance for ProjectId: {ProjectId}, TenantId: {TenantId}", projectId, tenantId); + + // Validate date format + if (!DateTime.TryParse(date, out var forDate)) { - _logger.LogWarning("Employee {EmployeeId} tries to access attendance of project {ProjectId}, but don't have access", LoggedInEmployee.Id, projectId); - return Unauthorized(ApiResponse.ErrorResponse("Unauthorized access", "Unauthorized access", 404)); + forDate = DateTime.UtcNow.Date; // Default to today's date } - DateTime forDate = new DateTime(); - - if (date != null && DateTime.TryParse(date, out forDate) == false) + // Check if the project exists and if the employee has access + var project = await _context.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId); + if (project == null) { - _logger.LogWarning("User sent Invalid Date while featching attendance logs"); - return BadRequest(ApiResponse.ErrorResponse("Invalid Date", "Invalid Date", 400)); - - } - if (projectId == Guid.Empty) - { - _logger.LogWarning("The project Id sent by user is less than or equal to zero"); - return BadRequest(ApiResponse.ErrorResponse("Project ID is required and must be greater than zero.", "Project ID is required and must be greater than zero.", 400)); + _logger.LogWarning("Project {ProjectId} not found in database", projectId); + return NotFound(ApiResponse.ErrorResponse("Project not found.")); } - var result = new List(); - Attendance? attendance = null; - - if (date == null) forDate = DateTime.UtcNow.Date; - if (hasTeamAttendancePermission) + if (!await _permission.HasProjectPermission(loggedInEmployee, projectId)) { - List lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date == forDate && c.TenantId == TenantId).ToListAsync(); + _logger.LogWarning("Unauthorized access attempt by EmployeeId: {EmployeeId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId); + return Unauthorized(ApiResponse.ErrorResponse("You do not have permission to access this project.")); + } + // --- 2. Delegate to Specific Logic Based on Permissions --- + try + { + var hasTeamAttendancePermission = await _permission.HasPermission(PermissionsMaster.TeamAttendance, loggedInEmployee.Id); + List result; - List projectteam = await _projectServices.GetTeamByProject(TenantId, projectId, IncludeInActive); - var idList = projectteam.Select(p => p.EmployeeId).ToList(); - //var emp = await _context.Employees.Where(e => idList.Contains(e.Id)).Include(e => e.JobRole).ToListAsync(); - var jobRole = await _context.JobRoles.ToListAsync(); - - foreach (ProjectAllocation teamMember in projectteam) + if (hasTeamAttendancePermission) { - if (teamMember.Employee != null && teamMember.Employee.JobRole != null) - { - var result1 = new EmployeeAttendanceVM() - { - EmployeeAvatar = null, - EmployeeId = teamMember.EmployeeId, - FirstName = teamMember.Employee.FirstName, - LastName = teamMember.Employee.LastName, - JobRoleName = teamMember.Employee.JobRole.Name, - }; - - //var member = emp.Where(e => e.Id == teamMember.EmployeeId); - - - attendance = lstAttendance.Find(x => x.EmployeeID == teamMember.EmployeeId) ?? new Attendance(); - if (attendance != null) - { - result1.Id = attendance.Id; - result1.CheckInTime = attendance.InTime; - result1.CheckOutTime = attendance.OutTime; - result1.Activity = attendance.Activity; - } - - result.Add(result1); - } + _logger.LogInfo("EmployeeId: {EmployeeId} has Team Attendance permission. Fetching team attendance.", loggedInEmployee.Id); + result = await GetTeamAttendanceAsync(tenantId, projectId, organizationId, forDate, includeInactive); + } + else if (await _permission.HasPermission(PermissionsMaster.SelfAttendance, loggedInEmployee.Id)) + { + _logger.LogInfo("EmployeeId: {EmployeeId} has Self Attendance permission. Fetching self attendance.", loggedInEmployee.Id); + result = await GetSelfAttendanceAsync(tenantId, projectId, loggedInEmployee.Id, organizationId, forDate); + } + else + { + _logger.LogWarning("Access denied for EmployeeId: {EmployeeId}. No valid attendance permission found.", loggedInEmployee.Id); + return StatusCode(403, ApiResponse.ErrorResponse("You do not have permission to view attendance.", new { }, 403)); } - result.Sort(delegate (EmployeeAttendanceVM x, EmployeeAttendanceVM y) - { - //return x.FirstName.CompareTo(y.FirstName); - return string.Compare(x.FirstName, y.FirstName, StringComparison.Ordinal); - }); + _logger.LogInfo("Successfully fetched {Count} attendance records for ProjectId: {ProjectId}", result.Count, projectId); + return Ok(ApiResponse.SuccessResponse(result, $"{result.Count} attendance records fetched successfully.")); } - else if (hasSelfAttendancePermission) + catch (Exception ex) { - Attendance lstAttendance = await _context.Attendes.FirstOrDefaultAsync(c => c.ProjectID == projectId && c.EmployeeID == LoggedInEmployee.Id && c.AttendanceDate.Date == forDate && c.TenantId == TenantId) ?? new Attendance(); - ProjectAllocation? projectAllocation = await _context.ProjectAllocations.Include(pa => pa.Employee).FirstOrDefaultAsync(pa => pa.ProjectId == projectId && pa.EmployeeId == LoggedInEmployee.Id && pa.TenantId == TenantId && pa.IsActive); - if (projectAllocation != null) - { - EmployeeAttendanceVM result1 = new EmployeeAttendanceVM - { - Id = lstAttendance.Id, - EmployeeAvatar = null, - EmployeeId = projectAllocation.EmployeeId, - FirstName = projectAllocation.Employee?.FirstName, - LastName = projectAllocation.Employee?.LastName, - JobRoleName = projectAllocation.Employee?.JobRole?.Name, - CheckInTime = lstAttendance.InTime, - CheckOutTime = lstAttendance.OutTime, - Activity = lstAttendance.Activity - }; - result.Add(result1); - } + _logger.LogError(ex, "An error occurred while fetching attendance for ProjectId: {ProjectId}", projectId); + return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred.")); } - _logger.LogInfo("{count} Attendance records fetched successfully", result.Count); - - return Ok(ApiResponse.SuccessResponse(result, System.String.Format("{0} Attendance records fetched successfully", result.Count), 200)); } + [HttpGet("regularize")] - public async Task GetRequestRegularizeAttendance([FromQuery] Guid projectId, [FromQuery] bool IncludeInActive) + public async Task GetRequestRegularizeAttendance([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] bool IncludeInActive) { Guid TenantId = GetTenantId(); Employee LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); @@ -379,7 +366,7 @@ namespace MarcoBMS.Services.Controllers List lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE && c.TenantId == TenantId).ToListAsync(); - List projectteam = await _projectServices.GetTeamByProject(TenantId, projectId, true); + List projectteam = await _projectServices.GetTeamByProject(TenantId, projectId, organizationId, true); var idList = projectteam.Select(p => p.EmployeeId).ToList(); var jobRole = await _context.JobRoles.ToListAsync(); @@ -402,6 +389,7 @@ namespace MarcoBMS.Services.Controllers result1.FirstName = teamMember.Employee.FirstName; result1.LastName = teamMember.Employee.LastName; result1.JobRoleName = teamMember.Employee.JobRole.Name; + result1.OrganizationName = teamMember.Employee.Organization?.Name; } result.Add(result1); @@ -415,7 +403,6 @@ namespace MarcoBMS.Services.Controllers return Ok(ApiResponse.SuccessResponse(result, System.String.Format("{0} Attendance records fetched successfully", result.Count), 200)); } - [HttpPost] [Route("record")] public async Task RecordAttendance([FromBody] RecordAttendanceDot recordAttendanceDot) @@ -823,5 +810,110 @@ namespace MarcoBMS.Services.Controllers DateTime finalDateTime = new DateTime(date.Year, date.Month, date.Day, parsedTime.Hour, parsedTime.Minute, 0); return finalDateTime; } + + /// + /// Fetches attendance for an entire project team using a single, optimized database query. + /// + private async Task> GetTeamAttendanceAsync(Guid tenantId, Guid projectId, Guid? organizationId, DateTime forDate, bool includeInactive) + { + // This single query joins ProjectAllocations with Employees and performs a LEFT JOIN with Attendances. + // This is far more efficient than fetching collections and joining them in memory. + var query = _context.ProjectAllocations + .Include(pa => pa.Employee) + .ThenInclude(e => e!.Organization) + .Where(pa => pa.TenantId == tenantId && pa.ProjectId == projectId); + + // Apply filters based on optional parameters + if (!includeInactive) + { + query = query.Where(pa => pa.IsActive); + } + if (organizationId.HasValue) + { + query = query.Where(pa => pa.Employee != null && pa.Employee.OrganizationId == organizationId); + } + + List lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date == forDate && c.TenantId == tenantId).ToListAsync(); + + var teamAttendance = await query + .AsNoTracking() + .ToListAsync(); + + var response = teamAttendance + .Select(teamMember => + { + var result1 = new EmployeeAttendanceVM() + { + EmployeeAvatar = null, + EmployeeId = teamMember.EmployeeId, + FirstName = teamMember.Employee?.FirstName, + LastName = teamMember.Employee?.LastName, + OrganizationName = teamMember.Employee?.Organization?.Name, + JobRoleName = teamMember.Employee?.JobRole?.Name, + }; + + //var member = emp.Where(e => e.Id == teamMember.EmployeeId); + + + var attendance = lstAttendance.Find(x => x.EmployeeID == teamMember.EmployeeId) ?? new Attendance(); + if (attendance != null) + { + result1.Id = attendance.Id; + result1.CheckInTime = attendance.InTime; + result1.CheckOutTime = attendance.OutTime; + result1.Activity = attendance.Activity; + } + return result1; + }) + .OrderBy(vm => vm.FirstName) // Let the database handle sorting. + .ThenBy(vm => vm.LastName).ToList(); + + return response; + } + + /// + /// Fetches a single attendance record for the logged-in employee. + /// + private async Task> GetSelfAttendanceAsync(Guid tenantId, Guid projectId, Guid employeeId, Guid? organizationId, DateTime forDate) + { + List result = new List(); + + // This query fetches the employee's project allocation and their attendance in a single trip. + Attendance lstAttendance = await _context.Attendes + .FirstOrDefaultAsync(c => c.ProjectID == projectId && c.EmployeeID == employeeId && c.AttendanceDate.Date == forDate && c.TenantId == tenantId) ?? new Attendance(); + + var projectAllocationQuery = _context.ProjectAllocations + .Include(pa => pa.Employee) + .ThenInclude(e => e!.Organization) + .Where(pa => pa.ProjectId == projectId && pa.EmployeeId == employeeId && pa.TenantId == tenantId && pa.IsActive); + + if (organizationId.HasValue) + { + projectAllocationQuery = projectAllocationQuery.Where(pa => pa.Employee != null && pa.Employee.OrganizationId == organizationId); + } + + var projectAllocation = await projectAllocationQuery.FirstOrDefaultAsync(); + + if (projectAllocation != null) + { + EmployeeAttendanceVM result1 = new EmployeeAttendanceVM + { + Id = lstAttendance.Id, + EmployeeAvatar = null, + EmployeeId = projectAllocation.EmployeeId, + FirstName = projectAllocation.Employee?.FirstName, + OrganizationName = projectAllocation.Employee?.Organization?.Name, + LastName = projectAllocation.Employee?.LastName, + JobRoleName = projectAllocation.Employee?.JobRole?.Name, + CheckInTime = lstAttendance.InTime, + CheckOutTime = lstAttendance.OutTime, + Activity = lstAttendance.Activity + }; + result.Add(result1); + } + + return result; + } + } } diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs index f88516c..e4ed444 100644 --- a/Marco.Pms.Services/Service/ProjectServices.cs +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -2298,21 +2298,22 @@ namespace Marco.Pms.Services.Service List alloc = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.IsActive == true).Include(c => c.Project).ToListAsync(); return alloc; } - public async Task> GetTeamByProject(Guid TenantId, Guid ProjectId, bool IncludeInactive) + public async Task> GetTeamByProject(Guid TenantId, Guid ProjectId, Guid? OrganizationId, bool IncludeInactive) { - if (IncludeInactive) + var projectAllocationQuery = _context.ProjectAllocations + .Include(pa => pa.Employee) + .ThenInclude(e => e!.Organization) + .Where(pa => pa.TenantId == TenantId && pa.ProjectId == ProjectId); + if (!IncludeInactive) { - - var employees = await _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == ProjectId).Include(e => e.Employee).ToListAsync(); - - return employees; + projectAllocationQuery = projectAllocationQuery.Where(pa => pa.IsActive); } - else + if (OrganizationId.HasValue) { - var employees = await _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == ProjectId && c.IsActive == true).Include(e => e.Employee).ToListAsync(); - - return employees; + projectAllocationQuery = projectAllocationQuery.Where(pa => pa.Employee != null && pa.Employee.OrganizationId == OrganizationId); } + var projectAllocation = await projectAllocationQuery.ToListAsync(); + return projectAllocation; } public async Task> GetMyProjects(Guid tenantId, Employee LoggedInEmployee) { diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs index 7679d95..bab113d 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs @@ -33,7 +33,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> GetAllProjectByTanentID(Guid tanentId); Task> GetProjectByEmployeeID(Guid employeeId); - Task> GetTeamByProject(Guid TenantId, Guid ProjectId, bool IncludeInactive); + Task> GetTeamByProject(Guid TenantId, Guid ProjectId, Guid? OrganizationId, bool IncludeInactive); Task> GetMyProjectIdsAsync(Guid tenantId, Employee LoggedInEmployee);