From 5d8a5e0cc8f750f6bd99a3db421483a8be9157c5 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 3 Dec 2025 11:13:41 +0530 Subject: [PATCH] Added an API to get todays attendance record for current logged-in-employee --- ...VM.cs => DashBoardEmployeeAttendanceVM.cs} | 2 +- .../DashBoard/ProjectAttendanceVM.cs | 2 +- .../Controllers/AppMenuController.cs | 7 + .../Controllers/DashboardController.cs | 138 +++++++++++++++++- 4 files changed, 143 insertions(+), 6 deletions(-) rename Marco.Pms.Model/ViewModels/DashBoard/{EmployeeAttendanceVM.cs => DashBoardEmployeeAttendanceVM.cs} (88%) diff --git a/Marco.Pms.Model/ViewModels/DashBoard/EmployeeAttendanceVM.cs b/Marco.Pms.Model/ViewModels/DashBoard/DashBoardEmployeeAttendanceVM.cs similarity index 88% rename from Marco.Pms.Model/ViewModels/DashBoard/EmployeeAttendanceVM.cs rename to Marco.Pms.Model/ViewModels/DashBoard/DashBoardEmployeeAttendanceVM.cs index 7d0629e..7bca043 100644 --- a/Marco.Pms.Model/ViewModels/DashBoard/EmployeeAttendanceVM.cs +++ b/Marco.Pms.Model/ViewModels/DashBoard/DashBoardEmployeeAttendanceVM.cs @@ -1,6 +1,6 @@ namespace Marco.Pms.Model.ViewModels.DashBoard { - public class EmployeeAttendanceVM + public class DashBoardEmployeeAttendanceVM { public string? FirstName { get; set; } public string? LastName { get; set; } diff --git a/Marco.Pms.Model/ViewModels/DashBoard/ProjectAttendanceVM.cs b/Marco.Pms.Model/ViewModels/DashBoard/ProjectAttendanceVM.cs index 12d61ec..bf7fd4e 100644 --- a/Marco.Pms.Model/ViewModels/DashBoard/ProjectAttendanceVM.cs +++ b/Marco.Pms.Model/ViewModels/DashBoard/ProjectAttendanceVM.cs @@ -2,7 +2,7 @@ { public class ProjectAttendanceVM { - public List? AttendanceTable { get; set; } + public List? AttendanceTable { get; set; } public int CheckedInEmployee { get; set; } public int AssignedEmployee { get; set; } } diff --git a/Marco.Pms.Services/Controllers/AppMenuController.cs b/Marco.Pms.Services/Controllers/AppMenuController.cs index 81b749c..82b122f 100644 --- a/Marco.Pms.Services/Controllers/AppMenuController.cs +++ b/Marco.Pms.Services/Controllers/AppMenuController.cs @@ -832,6 +832,13 @@ namespace Marco.Pms.Services.Controllers Available = true, MobileLink = "/dashboard/service-projects" }); + response.Add(new MenuSectionApplicationVM + { + Id = Guid.Parse("5fab4b88-c9a0-417b-aca2-130980fdb0cf"), + Name = "Infra Projects", + Available = true, + MobileLink = "/dashboard/infra-projects" + }); // Step 3: Log success response = response.Where(ms => !string.IsNullOrWhiteSpace(ms.MobileLink)).ToList(); diff --git a/Marco.Pms.Services/Controllers/DashboardController.cs b/Marco.Pms.Services/Controllers/DashboardController.cs index eee93bd..30f8345 100644 --- a/Marco.Pms.Services/Controllers/DashboardController.cs +++ b/Marco.Pms.Services/Controllers/DashboardController.cs @@ -1,8 +1,11 @@ -using Marco.Pms.DataAccess.Data; +using AutoMapper; +using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Dtos.Attendance; using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Expenses; using Marco.Pms.Model.Utilities; +using Marco.Pms.Model.ViewModels.Activities; +using Marco.Pms.Model.ViewModels.AttendanceVM; using Marco.Pms.Model.ViewModels.DashBoard; using Marco.Pms.Services.Service; using Marco.Pms.Services.Service.ServiceInterfaces; @@ -26,6 +29,9 @@ namespace Marco.Pms.Services.Controllers private readonly ILoggingService _logger; private readonly PermissionServices _permissionServices; private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly IMapper _mapper; + + 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"); @@ -40,7 +46,8 @@ namespace Marco.Pms.Services.Controllers IProjectServices projectServices, IServiceScopeFactory serviceScopeFactory, ILoggingService logger, - PermissionServices permissionServices) + PermissionServices permissionServices, + IMapper mapper) { _context = context; _userHelper = userHelper; @@ -48,8 +55,10 @@ namespace Marco.Pms.Services.Controllers _logger = logger; _serviceScopeFactory = serviceScopeFactory; _permissionServices = permissionServices; + _mapper = mapper; 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. /// @@ -499,7 +508,7 @@ namespace Marco.Pms.Services.Controllers return Ok(ApiResponse.SuccessResponse( new ProjectAttendanceVM { - AttendanceTable = new List(), + AttendanceTable = new List(), CheckedInEmployee = 0, AssignedEmployee = 0 }, @@ -523,7 +532,7 @@ namespace Marco.Pms.Services.Controllers .Join(employees, attendance => attendance.EmployeeId, employee => employee.Id, - (attendance, employee) => new EmployeeAttendanceVM + (attendance, employee) => new DashBoardEmployeeAttendanceVM { FirstName = employee.FirstName, LastName = employee.LastName, @@ -1074,5 +1083,126 @@ namespace Marco.Pms.Services.Controllers ApiResponse.ErrorResponse("An error occurred while fetching pending expenses.", "An error occurred while fetching pending expenses.", 500)); // [Error Response] } } + + /// + /// Retrieves today's attendance details for a specific employee on a given project, + /// defaulting to the currently logged-in employee when no employeeId is provided. + /// Includes related project and employee information for UI display. + /// + /// The project identifier whose attendance is requested. + /// + /// Optional employee identifier. When null, the currently logged-in employee is used. + /// + /// + /// 200 OK with an payload on success, or a standardized + /// error envelope on validation or processing failure. + /// + [HttpGet("get/attendance/employee/{projectId}")] + public async Task GetAttendanceByEmployeeAsync(Guid projectId, [FromQuery] Guid? employeeId) + { + // TenantId is assumed to come from a base controller, HttpContext, or similar. + if (tenantId == Guid.Empty) + { + _logger.LogWarning("GetAttendanceByEmployeeAsync called with empty TenantId. ProjectId={ProjectId}", projectId); + + return BadRequest( + ApiResponse.ErrorResponse("Invalid tenant information.", "TenantId is empty in GetAttendanceByEmployeeAsync.", 400)); + } + + if (projectId == Guid.Empty) + { + _logger.LogWarning("GetAttendanceByEmployeeAsync called with empty ProjectId. TenantId={TenantId}", tenantId); + + return BadRequest( + ApiResponse.ErrorResponse("Project reference is required.", "ProjectId is empty in GetAttendanceByEmployeeAsync.", 400)); + } + + // Resolve the currently logged-in employee (e.g., from token or session). + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + var attendanceEmployeeId = employeeId ?? loggedInEmployee.Id; + + try + { + + // Step 1: Ensure employee is allocated to the project for this tenant. + var projectAllocation = await _context.ProjectAllocations + .Include(pa => pa.Employee)!.ThenInclude(e => e.JobRole) + .Include(pa => pa.Employee)!.ThenInclude(e => e.Organization) + .Include(pa => pa.Project) + .FirstOrDefaultAsync(pa => + pa.ProjectId == projectId && + pa.EmployeeId == attendanceEmployeeId && + pa.IsActive && + pa.TenantId == tenantId); + + if (projectAllocation == null) + { + _logger.LogWarning( + "GetAttendanceByEmployeeAsync failed: Employee not allocated to project. TenantId={TenantId}, ProjectId={ProjectId}, EmployeeId={EmployeeId}, RequestedById={RequestedById}", + tenantId, projectId, attendanceEmployeeId, loggedInEmployee.Id); + + return BadRequest(ApiResponse.ErrorResponse("The employee is not allocated to the selected project.", "Project allocation not found for given ProjectId, EmployeeId, and TenantId.", + 400)); + } + + // Step 2: Fetch today's attendance (if any) for the selected employee and project. + var today = DateTime.UtcNow.Date; // Prefer UTC for server-side comparisons. + + var attendance = await _context.Attendes + .Include(a => a.Approver) + .Include(a => a.RequestedBy) + .FirstOrDefaultAsync(a => + a.TenantId == tenantId && + a.EmployeeId == attendanceEmployeeId && + a.ProjectID == projectId && + a.AttendanceDate.Date == today); + + // Step 3: Map to view model with defensive null handling. + var attendanceVm = new EmployeeAttendanceVM + { + Id = attendance?.Id ?? Guid.Empty, + EmployeeAvatar = null, // Can be filled from a file service or CDN later. + EmployeeId = projectAllocation.EmployeeId, + FirstName = projectAllocation.Employee?.FirstName, + OrganizationName = projectAllocation.Employee?.Organization?.Name, + LastName = projectAllocation.Employee?.LastName, + JobRoleName = projectAllocation.Employee?.JobRole?.Name, + ProjectId = projectId, + ProjectName = projectAllocation.Project?.Name, + CheckInTime = attendance?.InTime, + CheckOutTime = attendance?.OutTime, + Activity = attendance?.Activity ?? ATTENDANCE_MARK_TYPE.CHECK_IN, + ApprovedAt = attendance?.ApprovedAt, + Approver = attendance == null + ? null + : _mapper.Map(attendance.Approver), + RequestedAt = attendance?.RequestedAt, + RequestedBy = attendance == null + ? null + : _mapper.Map(attendance.RequestedBy) + }; + + _logger.LogInfo("GetAttendanceByEmployeeAsync completed successfully. TenantId={TenantId}, ProjectId={ProjectId}, EmployeeId={EmployeeId}, HasAttendance={HasAttendance}", + tenantId, projectId, attendanceEmployeeId, attendance != null); + + return Ok(ApiResponse.SuccessResponse(attendanceVm, "Attendance fetched successfully.", 200)); + } + catch (OperationCanceledException) + { + _logger.LogWarning("GetAttendanceByEmployeeAsync was canceled. TenantId={TenantId}, ProjectId={ProjectId}, EmployeeId={EmployeeId}", + tenantId, projectId, attendanceEmployeeId); + + return StatusCode(499, ApiResponse.ErrorResponse("The request was canceled.", "GetAttendanceByEmployeeAsync was canceled by the client.", 499)); + } + catch (Exception ex) + { + _logger.LogError(ex, "GetAttendanceByEmployeeAsync failed with an unexpected error. TenantId={TenantId}, ProjectId={ProjectId}, EmployeeId={EmployeeId}", + tenantId, projectId, attendanceEmployeeId); + + return StatusCode(500, ApiResponse.ErrorResponse("An error occurred while fetching attendance.", "Unhandled exception in GetAttendanceByEmployeeAsync.", 500)); + } + } + } }