Added an API to get todays attendance record for current logged-in-employee

This commit is contained in:
ashutosh.nehete 2025-12-03 11:13:41 +05:30
parent a4714d5440
commit 5d8a5e0cc8
4 changed files with 143 additions and 6 deletions

View File

@ -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; }

View File

@ -2,7 +2,7 @@
{
public class ProjectAttendanceVM
{
public List<EmployeeAttendanceVM>? AttendanceTable { get; set; }
public List<DashBoardEmployeeAttendanceVM>? AttendanceTable { get; set; }
public int CheckedInEmployee { get; set; }
public int AssignedEmployee { get; set; }
}

View File

@ -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();

View File

@ -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();
}
/// <summary>
/// Fetches project progression data (planned and completed tasks) in graph form for a tenant and specified (or all) projects over a date range.
/// </summary>
@ -499,7 +508,7 @@ namespace Marco.Pms.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(
new ProjectAttendanceVM
{
AttendanceTable = new List<EmployeeAttendanceVM>(),
AttendanceTable = new List<DashBoardEmployeeAttendanceVM>(),
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<object>.ErrorResponse("An error occurred while fetching pending expenses.", "An error occurred while fetching pending expenses.", 500)); // [Error Response]
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="projectId">The project identifier whose attendance is requested.</param>
/// <param name="employeeId">
/// Optional employee identifier. When null, the currently logged-in employee is used.
/// </param>
/// <returns>
/// 200 OK with an <see cref="EmployeeAttendanceVM"/> payload on success, or a standardized
/// error envelope on validation or processing failure.
/// </returns>
[HttpGet("get/attendance/employee/{projectId}")]
public async Task<IActionResult> 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<object>.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<object>.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<object>.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<BasicEmployeeVM>(attendance.Approver),
RequestedAt = attendance?.RequestedAt,
RequestedBy = attendance == null
? null
: _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy)
};
_logger.LogInfo("GetAttendanceByEmployeeAsync completed successfully. TenantId={TenantId}, ProjectId={ProjectId}, EmployeeId={EmployeeId}, HasAttendance={HasAttendance}",
tenantId, projectId, attendanceEmployeeId, attendance != null);
return Ok(ApiResponse<object>.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<object>.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<object>.ErrorResponse("An error occurred while fetching attendance.", "Unhandled exception in GetAttendanceByEmployeeAsync.", 500));
}
}
}
}