marco.pms.api/Marco.Pms.Services/Controllers/AttendanceController.cs

1015 lines
51 KiB
C#

using AutoMapper;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.AttendanceModule;
using Marco.Pms.Model.Dtos.Attendance;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Mapper;
using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.AttendanceVM;
using Marco.Pms.Services.Hubs;
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.AspNetCore.SignalR;
using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore;
using System.Globalization;
using Document = Marco.Pms.Model.DocumentManager.Document;
namespace MarcoBMS.Services.Controllers
{
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class AttendanceController : ControllerBase
{
private readonly ApplicationDbContext _context;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly UserHelper _userHelper;
private readonly PermissionServices _permission;
private readonly ILoggingService _logger;
private readonly Guid tenantId;
private readonly IMapper _mapper;
public AttendanceController(
ApplicationDbContext context,
UserHelper userHelper,
IServiceScopeFactory serviceScopeFactory,
ILoggingService logger,
PermissionServices permission,
IMapper mapper)
{
_context = context;
_serviceScopeFactory = serviceScopeFactory;
_userHelper = userHelper;
_logger = logger;
_permission = permission;
_mapper = mapper;
tenantId = userHelper.GetTenantId();
}
[HttpGet("log/attendance/{attendanceid}")]
public async Task<IActionResult> GetAttendanceLogById(Guid attendanceid)
{
using var scope = _serviceScopeFactory.CreateScope();
var _s3Service = scope.ServiceProvider.GetRequiredService<S3UploadService>();
List<AttendanceLog> lstAttendance = await _context.AttendanceLogs
.Include(a => a.Document)
.Include(a => a.Employee)
.Include(a => a.UpdatedByEmployee)
.Where(c => c.AttendanceId == attendanceid && c.TenantId == tenantId)
.ToListAsync();
List<AttendanceLogVM> attendanceLogVMs = new List<AttendanceLogVM>();
foreach (var attendanceLog in lstAttendance)
{
string objectKey = attendanceLog.Document != null ? attendanceLog.Document.S3Key : string.Empty;
string preSignedUrl = string.IsNullOrEmpty(objectKey) ? string.Empty : _s3Service.GeneratePreSignedUrl(objectKey);
attendanceLogVMs.Add(attendanceLog.ToAttendanceLogVMFromAttendanceLog(preSignedUrl, preSignedUrl));
}
_logger.LogInfo("{count} Attendance records fetched successfully", lstAttendance.Count);
return Ok(ApiResponse<object>.SuccessResponse(attendanceLogVMs, System.String.Format("{0} Attendance records fetched successfully", lstAttendance.Count), 200));
}
[HttpGet("log/employee/{employeeId}")]
public async Task<IActionResult> GetAttendanceLogByEmployeeId(Guid employeeId, [FromQuery] DateTime? dateFrom = null, [FromQuery] DateTime? dateTo = null)
{
if (employeeId == Guid.Empty)
{
_logger.LogWarning("The employee Id sent by user is empty");
return BadRequest(ApiResponse<object>.ErrorResponse("Employee ID is required and must not be Empty.", "Employee ID is required and must not be empty.", 400));
}
Employee? employee = await _context.Employees.Include(e => e.JobRole).FirstOrDefaultAsync(e => e.Id == employeeId && e.TenantId == tenantId);
if (employee == null)
{
_logger.LogWarning("Employee {EmployeeId} not found", employeeId);
return NotFound(ApiResponse<object>.ErrorResponse("Employee not found", "Employee not found in database", 404));
}
if (!dateFrom.HasValue)
{
dateFrom = DateTime.UtcNow;
}
if (!dateTo.HasValue)
{
var days = 0 - 7;
dateTo = dateFrom.Value.AddDays(days);
}
List<Attendance> attendances = await _context.Attendes
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Where(c => c.EmployeeId == employeeId && c.TenantId == tenantId && c.AttendanceDate.Date >= dateFrom && c.AttendanceDate.Date <= dateTo).ToListAsync();
var projectIds = attendances.Select(a => a.ProjectID).Distinct().ToList();
var projects = await _context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync();
List<EmployeeAttendanceVM> results = new List<EmployeeAttendanceVM>();
if (employee != null)
{
foreach (var attendance in attendances)
{
EmployeeAttendanceVM result = new EmployeeAttendanceVM
{
Id = attendance.Id,
EmployeeId = employee.Id,
FirstName = employee.FirstName,
LastName = employee.LastName,
ProjectId = attendance.ProjectID,
ProjectName = projects.Where(p => p.Id == attendance.ProjectID).Select(p => p.Name).FirstOrDefault(),
CheckInTime = attendance.InTime,
CheckOutTime = attendance.OutTime,
JobRoleName = employee.JobRole != null ? employee.JobRole.Name : "",
Activity = attendance.Activity,
EmployeeAvatar = null,
RequestedAt = attendance.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy),
ApprovedAt = attendance.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver)
};
results.Add(result);
}
}
_logger.LogInfo("{count} Attendance records fetched successfully", results.Count);
return Ok(ApiResponse<object>.SuccessResponse(results, System.String.Format("{0} Attendance records fetched successfully", results.Count), 200));
}
/// <summary>
///
/// </summary>
/// <param name="projectId">ProjectID</param>
/// <param name="date">YYYY-MM-dd</param>
/// <returns></returns>
[HttpGet("project/log")]
public async Task<IActionResult> EmployeeAttendanceByDateRange([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null)
{
using var scope = _serviceScopeFactory.CreateScope();
var _projectServices = scope.ServiceProvider.GetRequiredService<IProjectServices>();
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<object>.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);
if (!hasProjectPermission)
{
_logger.LogWarning("Employee {EmployeeId} tries to access attendance of project {ProjectId}, but don't have access", loggedInEmployee.Id, projectId);
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized access", "Unauthorized access", 404));
}
DateTime fromDate = new DateTime();
DateTime toDate = new DateTime();
if (dateFrom != null && DateTime.TryParse(dateFrom, out fromDate) == false)
{
_logger.LogWarning("User sent Invalid fromDate while featching attendance logs");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Date", "Invalid Date", 400));
}
if (dateTo != null && DateTime.TryParse(dateTo, out toDate) == false)
{
_logger.LogWarning("User sent Invalid toDate while featching attendance logs");
return BadRequest(ApiResponse<object>.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<object>.ErrorResponse("Project ID is required and must be greater than zero.", "Project ID is required and must be greater than zero.", 400));
}
var result = new List<EmployeeAttendanceVM>();
//Attendance? attendance = null;
ProjectAllocation? teamMember = null;
if (dateFrom == null) fromDate = DateTime.UtcNow.Date;
if (dateTo == null && dateFrom != null) toDate = fromDate.AddDays(-1);
if (hasTeamAttendancePermission)
{
List<Attendance> lstAttendance = await _context.Attendes
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Include(a => a.Approver)
.ThenInclude(e => e!.JobRole)
.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date >= fromDate.Date && c.AttendanceDate.Date <= toDate.Date && c.TenantId == tenantId)
.ToListAsync();
List<ProjectAllocation> projectteam = await _projectServices.GetTeamByProject(tenantId, projectId, organizationId, true);
var jobRole = await _context.JobRoles.ToListAsync();
foreach (Attendance? attendance in lstAttendance)
{
var result1 = new EmployeeAttendanceVM()
{
Id = attendance.Id,
CheckInTime = attendance.InTime,
CheckOutTime = attendance.OutTime,
Activity = attendance.Activity,
ApprovedAt = attendance.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver),
RequestedAt = attendance.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy)
};
teamMember = projectteam.Find(x => x.EmployeeId == attendance.EmployeeId);
if (teamMember != null)
{
result1.EmployeeAvatar = null;
result1.EmployeeId = teamMember.EmployeeId;
if (teamMember.Employee != null)
{
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;
result1.ProjectId = projectId;
result1.ProjectName = teamMember.Project?.Name;
}
else
{
result1.FirstName = null;
result1.LastName = null;
result1.JobRoleName = null;
result1.OrganizationName = null;
}
result.Add(result1);
}
}
}
else if (hasSelfAttendancePermission)
{
List<Attendance> lstAttendances = await _context.Attendes
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Include(a => a.Approver)
.ThenInclude(e => e!.JobRole)
.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.Project)
.Include(pa => pa.Employee)
.ThenInclude(e => e!.Organization)
.Include(pa => pa.Employee)
.ThenInclude(e => e!.JobRole)
.Where(pa => pa.EmployeeId == loggedInEmployee.Id && pa.TenantId == tenantId && pa.IsActive &&
pa.ProjectId == projectId && pa.Project != null &&
pa.Employee != null && pa.Employee.Organization != null && pa.Employee.JobRole != null);
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)
{
EmployeeAttendanceVM result1 = new EmployeeAttendanceVM
{
Id = attendance.Id,
EmployeeAvatar = null,
EmployeeId = projectAllocation.EmployeeId,
FirstName = projectAllocation.Employee?.FirstName,
LastName = projectAllocation.Employee?.LastName,
JobRoleName = projectAllocation.Employee?.JobRole?.Name,
OrganizationName = projectAllocation.Employee?.Organization?.Name,
ProjectId = attendance.ProjectID,
ProjectName = projectAllocation.Project?.Name,
CheckInTime = attendance.InTime,
CheckOutTime = attendance.OutTime,
Activity = attendance.Activity,
ApprovedAt = attendance.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver),
RequestedAt = attendance.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy)
};
result.Add(result1);
}
}
}
_logger.LogInfo("{count} Attendance records fetched successfully", result.Count);
return Ok(ApiResponse<object>.SuccessResponse(result, System.String.Format("{0} Attendance records fetched successfully", result.Count), 200));
}
[HttpGet("project/team")]
/// <summary>
/// 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).
/// </summary>
/// <param name="projectId">The ID of the project.</param>
/// <param name="organizationId">Optional. Filters attendance for employees of a specific organization.</param>
/// <param name="includeInactive">Optional. Includes inactive employees in the team list if true.</param>
/// <param name="date">Optional. The date for which to fetch attendance, in "yyyy-MM-dd" format. Defaults to the current UTC date.</param>
/// <returns>An IActionResult containing a list of employee attendance records or an error response.</returns>
public async Task<IActionResult> EmployeeAttendanceByProjectAsync([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] bool includeInactive, [FromQuery] string? date = null)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// --- 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))
{
forDate = DateTime.UtcNow.Date; // Default to today's date
}
// 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("Project {ProjectId} not found in database", projectId);
return NotFound(ApiResponse<object>.ErrorResponse("Project not found."));
}
if (!await _permission.HasProjectPermission(loggedInEmployee, projectId))
{
_logger.LogWarning("Unauthorized access attempt by EmployeeId: {EmployeeId} for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId);
return Unauthorized(ApiResponse<object>.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<EmployeeAttendanceVM> result;
if (hasTeamAttendancePermission)
{
_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<object>.ErrorResponse("You do not have permission to view attendance.", new { }, 403));
}
_logger.LogInfo("Successfully fetched {Count} attendance records for ProjectId: {ProjectId}", result.Count, projectId);
return Ok(ApiResponse<object>.SuccessResponse(result, $"{result.Count} attendance records fetched successfully."));
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while fetching attendance for ProjectId: {ProjectId}", projectId);
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred."));
}
}
[HttpGet("regularize")]
public async Task<IActionResult> GetRequestRegularizeAttendance([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] bool IncludeInActive)
{
using var scope = _serviceScopeFactory.CreateScope();
var _projectServices = scope.ServiceProvider.GetRequiredService<IProjectServices>();
Employee LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var result = new List<EmployeeAttendanceVM>();
var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId);
if (!hasProjectPermission)
{
_logger.LogWarning("Employee {EmployeeId} tries to access attendance of project {ProjectId}, but don't have access", LoggedInEmployee.Id, projectId);
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized access", "Unauthorized access", 404));
}
List<Attendance> lstAttendance = await _context.Attendes
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Include(a => a.Approver)
.ThenInclude(e => e!.JobRole)
.Where(c => c.ProjectID == projectId && c.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE && c.TenantId == tenantId)
.ToListAsync();
List<ProjectAllocation> projectteam = await _projectServices.GetTeamByProject(tenantId, projectId, organizationId, true);
var idList = projectteam.Select(p => p.EmployeeId).ToList();
foreach (Attendance attende in lstAttendance)
{
var teamMember = projectteam.Find(m => m.EmployeeId == attende.EmployeeId);
if (teamMember != null && teamMember.Employee != null && teamMember.Employee.JobRole != null)
{
var result1 = new EmployeeAttendanceVM()
{
Id = attende.Id,
CheckInTime = attende.InTime,
CheckOutTime = attende.OutTime,
Activity = attende.Activity,
EmployeeAvatar = null,
EmployeeId = attende.EmployeeId,
FirstName = teamMember.Employee.FirstName,
LastName = teamMember.Employee.LastName,
JobRoleName = teamMember.Employee.JobRole.Name,
OrganizationName = teamMember.Employee.Organization?.Name,
ProjectId = projectId,
ProjectName = teamMember.Project?.Name,
ApprovedAt = attende.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(attende.Approver),
RequestedAt = attende.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(attende.RequestedBy)
};
result.Add(result1);
}
}
result.Sort(delegate (EmployeeAttendanceVM x, EmployeeAttendanceVM y)
{
return string.Compare(x.FirstName, y.FirstName, StringComparison.Ordinal);
});
_logger.LogInfo("{count} Attendance records fetched successfully", result.Count);
return Ok(ApiResponse<object>.SuccessResponse(result, System.String.Format("{0} Attendance records fetched successfully", result.Count), 200));
}
[HttpPost]
[Route("record")]
public async Task<IActionResult> RecordAttendance([FromBody] RecordAttendanceDot recordAttendanceDot)
{
if (!ModelState.IsValid)
{
var errors = ModelState.Values
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
_logger.LogWarning("User sent Invalid Date while marking attendance \n {Error}", string.Join(",", errors));
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
using var scope = _serviceScopeFactory.CreateScope();
var _signalR = scope.ServiceProvider.GetRequiredService<IHubContext<MarcoHub>>();
var _employeeHelper = scope.ServiceProvider.GetRequiredService<EmployeeHelper>();
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
var currentEmployee = await _userHelper.GetCurrentEmployeeAsync();
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
Attendance? attendance = await _context.Attendes.FirstOrDefaultAsync(a => a.Id == recordAttendanceDot.Id && a.TenantId == tenantId); ;
if (recordAttendanceDot.MarkTime == null)
{
_logger.LogWarning("User sent Invalid Mark Time while marking attendance");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Mark Time", "Invalid Mark Time", 400));
}
DateTime finalDateTime = GetDateFromTimeStamp(recordAttendanceDot.Date, recordAttendanceDot.MarkTime);
if (recordAttendanceDot.Comment == null)
{
_logger.LogWarning("User sent Invalid comment while marking attendance");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Comment", "Invalid Comment", 400));
}
if (attendance != null)
{
attendance.Comment = recordAttendanceDot.Comment;
if (recordAttendanceDot.Action == ATTENDANCE_MARK_TYPE.CHECK_IN)
{
attendance.InTime = finalDateTime;
attendance.OutTime = null;
attendance.Activity = ATTENDANCE_MARK_TYPE.CHECK_OUT;
}
else if (recordAttendanceDot.Action == ATTENDANCE_MARK_TYPE.CHECK_OUT)
{
attendance.IsApproved = true;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE;
attendance.OutTime = finalDateTime;
}
else if (recordAttendanceDot.Action == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE)
{
DateTime date = attendance.AttendanceDate;
finalDateTime = GetDateFromTimeStamp(date.Date, recordAttendanceDot.MarkTime);
if (attendance.InTime < finalDateTime)
{
attendance.OutTime = finalDateTime;
attendance.Activity = ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE;
attendance.RequestedById = currentEmployee.Id;
attendance.RequestedAt = DateTime.UtcNow;
}
else
{
_logger.LogWarning("Employee {EmployeeId} sent regularization request but it check-out time is earlier than check-out");
return BadRequest(ApiResponse<object>.ErrorResponse("Check-out time must be later than check-in time", "Check-out time must be later than check-in time", 400));
}
// do nothing
}
else if (recordAttendanceDot.Action == ATTENDANCE_MARK_TYPE.REGULARIZE)
{
attendance.IsApproved = true;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE;
attendance.ApprovedById = currentEmployee.Id;
attendance.ApprovedAt = DateTime.UtcNow;
// do nothing
}
else if (recordAttendanceDot.Action == ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT)
{
attendance.IsApproved = false;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT;
attendance.ApprovedById = currentEmployee.Id;
attendance.ApprovedAt = DateTime.UtcNow;
// do nothing
}
attendance.Date = DateTime.UtcNow;
// update code
_context.Attendes.Update(attendance);
}
else
{
attendance = new Attendance();
attendance.TenantId = tenantId;
attendance.AttendanceDate = recordAttendanceDot.Date;
// attendance.Activity = recordAttendanceDot.Action;
attendance.Comment = recordAttendanceDot.Comment;
attendance.EmployeeId = recordAttendanceDot.EmployeeID;
attendance.ProjectID = recordAttendanceDot.ProjectID;
attendance.Date = DateTime.UtcNow;
attendance.InTime = finalDateTime;
attendance.OutTime = null;
attendance.Activity = ATTENDANCE_MARK_TYPE.CHECK_OUT;
_context.Attendes.Add(attendance);
}
await _context.SaveChangesAsync();
// Step 3: Always insert a new log entry
var attendanceLog = new AttendanceLog
{
AttendanceId = attendance.Id, // Use existing or new AttendanceId
Activity = attendance.Activity,
ActivityTime = finalDateTime,
Comment = recordAttendanceDot.Comment,
EmployeeID = recordAttendanceDot.EmployeeID,
Latitude = recordAttendanceDot.Latitude,
Longitude = recordAttendanceDot.Longitude,
TenantId = tenantId,
UpdatedBy = currentEmployee.Id,
UpdatedOn = recordAttendanceDot.Date
};
//if (recordAttendanceDot.Image != null && recordAttendanceDot.Image.Count > 0)
//{
// attendanceLog.Photo = recordAttendanceDot.Image[0].Base64Data;
//}
_context.AttendanceLogs.Add(attendanceLog);
await _context.SaveChangesAsync();
await transaction.CommitAsync(); // Commit transaction
Employee employee = await _employeeHelper.GetEmployeeByID(recordAttendanceDot.EmployeeID);
if (employee.JobRole != null)
{
EmployeeAttendanceVM vm = new EmployeeAttendanceVM()
{
CheckInTime = attendance.InTime,
CheckOutTime = attendance.OutTime,
EmployeeAvatar = null,
EmployeeId = recordAttendanceDot.EmployeeID,
FirstName = employee.FirstName,
LastName = employee.LastName,
Id = attendance.Id,
Activity = attendance.Activity,
JobRoleName = employee.JobRole.Name
};
var sendActivity = 0;
if (recordAttendanceDot.Id == Guid.Empty)
{
sendActivity = 1;
}
var notification = new { LoggedInUserId = currentEmployee.Id, Keyword = "Attendance", Activity = sendActivity, ProjectId = attendance.ProjectID, Response = vm };
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
_logger.LogInfo("Attendance for employee {FirstName} {LastName} has been marked", employee.FirstName ?? string.Empty, employee.LastName ?? string.Empty);
_ = Task.Run(async () =>
{
// --- Push Notification Section ---
// This section attempts to send a test push notification to the user's device.
// It's designed to fail gracefully and handle invalid Firebase Cloud Messaging (FCM) tokens.
var name = $"{vm.FirstName} {vm.LastName}";
await _firebase.SendAttendanceMessageAsync(attendance.ProjectID, name, recordAttendanceDot.Action, attendance.EmployeeId, tenantId);
});
return Ok(ApiResponse<object>.SuccessResponse(vm, "Attendance marked successfully.", 200));
}
_logger.LogInfo("Attendance for employee {FirstName} {LastName} has been marked", employee.FirstName ?? string.Empty, employee.LastName ?? string.Empty);
return Ok(ApiResponse<object>.SuccessResponse(new EmployeeAttendanceVM(), "Attendance marked successfully.", 200));
}
catch (Exception ex)
{
await transaction.RollbackAsync(); // Rollback on failure
_logger.LogError(ex, "An Error occured while marking attendance");
var response = new
{
message = ex.Message,
detail = ex.StackTrace,
statusCode = StatusCodes.Status500InternalServerError
};
return BadRequest(ApiResponse<object>.ErrorResponse(ex.Message, response, 400));
}
}
[HttpPost]
[Route("record-image")]
public async Task<IActionResult> RecordAttendanceWithImage([FromBody] RecordAttendanceDot recordAttendanceDot)
{
if (!ModelState.IsValid)
{
var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList();
_logger.LogWarning("Invalid attendance model received. \n {Error}", string.Join(",", errors));
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
using var scope = _serviceScopeFactory.CreateScope();
var _s3Service = scope.ServiceProvider.GetRequiredService<S3UploadService>();
var _signalR = scope.ServiceProvider.GetRequiredService<IHubContext<MarcoHub>>();
var _employeeHelper = scope.ServiceProvider.GetRequiredService<EmployeeHelper>();
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var batchId = Guid.NewGuid();
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// Validate mark time
if (recordAttendanceDot.MarkTime == null)
{
_logger.LogWarning("Null mark time provided.");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Mark Time", "Mark time is required", 400));
}
if (string.IsNullOrWhiteSpace(recordAttendanceDot.Comment))
{
_logger.LogWarning("Empty comment provided.");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Comment", "Comment is required", 400));
}
var finalDateTime = GetDateFromTimeStamp(recordAttendanceDot.Date, recordAttendanceDot.MarkTime);
var attendance = await _context.Attendes
.FirstOrDefaultAsync(a => a.Id == recordAttendanceDot.Id && a.TenantId == tenantId);
// Create or update attendance
if (attendance == null)
{
attendance = new Attendance
{
TenantId = tenantId,
AttendanceDate = recordAttendanceDot.Date,
Comment = recordAttendanceDot.Comment,
EmployeeId = recordAttendanceDot.EmployeeID,
ProjectID = recordAttendanceDot.ProjectID,
Date = DateTime.UtcNow,
InTime = finalDateTime,
Activity = ATTENDANCE_MARK_TYPE.CHECK_OUT
};
_context.Attendes.Add(attendance);
}
else
{
attendance.Comment = recordAttendanceDot.Comment;
attendance.Date = DateTime.UtcNow;
switch (recordAttendanceDot.Action)
{
case ATTENDANCE_MARK_TYPE.CHECK_IN:
attendance.InTime = finalDateTime;
attendance.OutTime = null;
attendance.Activity = ATTENDANCE_MARK_TYPE.CHECK_OUT;
break;
case ATTENDANCE_MARK_TYPE.CHECK_OUT:
attendance.OutTime = finalDateTime;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE;
attendance.IsApproved = true;
break;
case ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE:
DateTime date = attendance.InTime ?? recordAttendanceDot.Date;
finalDateTime = GetDateFromTimeStamp(date.Date, recordAttendanceDot.MarkTime);
if (attendance.InTime < finalDateTime)
{
attendance.OutTime = finalDateTime;
attendance.Activity = ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE;
attendance.RequestedById = loggedInEmployee.Id;
attendance.RequestedAt = DateTime.UtcNow;
}
else
{
_logger.LogWarning("Regularization check-out time is before check-in.");
return BadRequest(ApiResponse<object>.ErrorResponse("Check-out time must be later than check-in time", "Invalid regularization", 400));
}
break;
case ATTENDANCE_MARK_TYPE.REGULARIZE:
attendance.IsApproved = true;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE;
attendance.ApprovedById = loggedInEmployee.Id;
attendance.ApprovedAt = DateTime.UtcNow;
break;
case ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT:
attendance.IsApproved = false;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT;
attendance.ApprovedById = loggedInEmployee.Id;
attendance.ApprovedAt = DateTime.UtcNow;
break;
}
_context.Attendes.Update(attendance);
}
// Upload image if present
Document? document = null;
string? preSignedUrl = null;
if (recordAttendanceDot.Image != null && recordAttendanceDot.Image.ContentType != null)
{
string base64 = recordAttendanceDot.Image.Base64Data?.Split(',').LastOrDefault() ?? "";
if (string.IsNullOrWhiteSpace(base64))
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Image data missing", 400));
var fileType = _s3Service.GetContentTypeFromBase64(base64);
var fileName = _s3Service.GenerateFileName(fileType, tenantId, "attendance");
var objectKey = $"tenant-{tenantId}/Employee/{recordAttendanceDot.EmployeeID}/Attendance/{fileName}";
await _s3Service.UploadFileAsync(base64, fileType, objectKey);
preSignedUrl = _s3Service.GeneratePreSignedUrl(objectKey);
document = new Document
{
BatchId = batchId,
UploadedById = loggedInEmployee.Id,
FileName = recordAttendanceDot.Image.FileName ?? "",
ContentType = recordAttendanceDot.Image.ContentType,
S3Key = objectKey,
//Base64Data = recordAttendanceDot.Image.Base64Data,
FileSize = recordAttendanceDot.Image.FileSize,
UploadedAt = recordAttendanceDot.Date,
TenantId = tenantId
};
_context.Documents.Add(document);
}
// Log attendance
var attendanceLog = new AttendanceLog
{
AttendanceId = attendance.Id,
Activity = attendance.Activity,
ActivityTime = finalDateTime,
Comment = recordAttendanceDot.Comment,
EmployeeID = recordAttendanceDot.EmployeeID,
Latitude = recordAttendanceDot.Latitude,
Longitude = recordAttendanceDot.Longitude,
DocumentId = document?.Id,
TenantId = tenantId,
UpdatedBy = loggedInEmployee.Id,
UpdatedOn = recordAttendanceDot.Date
};
_context.AttendanceLogs.Add(attendanceLog);
await _context.SaveChangesAsync();
await transaction.CommitAsync();
// Construct view model
var employee = await _employeeHelper.GetEmployeeByID(recordAttendanceDot.EmployeeID);
var vm = new EmployeeAttendanceVM
{
Id = attendance.Id,
EmployeeId = employee.Id,
FirstName = employee.FirstName,
LastName = employee.LastName,
CheckInTime = attendance.InTime,
CheckOutTime = attendance.OutTime,
Activity = attendance.Activity,
JobRoleName = employee.JobRole?.Name,
DocumentId = document?.Id ?? Guid.Empty,
ThumbPreSignedUrl = preSignedUrl ?? "",
PreSignedUrl = preSignedUrl ?? ""
};
var notification = new
{
LoggedInUserId = loggedInEmployee.Id,
Keyword = "Attendance",
Activity = recordAttendanceDot.Id == Guid.Empty ? 1 : 0,
ProjectId = attendance.ProjectID,
Response = vm
};
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
_ = Task.Run(async () =>
{
// --- Push Notification Section ---
// This section attempts to send a test push notification to the user's device.
// It's designed to fail gracefully and handle invalid Firebase Cloud Messaging (FCM) tokens.
var name = $"{vm.FirstName} {vm.LastName}";
await _firebase.SendAttendanceMessageAsync(attendance.ProjectID, name, recordAttendanceDot.Action, attendance.EmployeeId, tenantId);
});
_logger.LogInfo("Attendance recorded for employee: {FullName}", $"{employee.FirstName} {employee.LastName}");
return Ok(ApiResponse<object>.SuccessResponse(vm, "Attendance marked successfully.", 200));
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "Error while recording attendance");
return BadRequest(ApiResponse<object>.ErrorResponse("Something went wrong", ex.Message, 500));
}
}
private static DateTime GetDateFromTimeStamp(DateTime date, string timeString)
{
//DateTime date = recordAttendanceDot.Date;
// Parse time string to TimeSpan
DateTime parsedTime = DateTime.ParseExact(timeString, "hh:mm tt", CultureInfo.InvariantCulture);
// Combine date with time
DateTime finalDateTime = new DateTime(date.Year, date.Month, date.Day, parsedTime.Hour, parsedTime.Minute, 0);
return finalDateTime;
}
/// <summary>
/// Fetches attendance for an entire project team using a single, optimized database query.
/// </summary>
private async Task<List<EmployeeAttendanceVM>> 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.Project)
.Include(pa => pa.Employee)
.ThenInclude(e => e!.Organization)
.Include(pa => pa.Employee)
.ThenInclude(e => e!.JobRole)
.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<Attendance> lstAttendance = await _context.Attendes
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Include(a => a.Approver)
.ThenInclude(e => e!.JobRole)
.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,
ProjectId = projectId,
ProjectName = teamMember.Project?.Name
};
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;
result1.ApprovedAt = attendance.ApprovedAt;
result1.Approver = _mapper.Map<BasicEmployeeVM>(attendance.Approver);
result1.RequestedAt = attendance.RequestedAt;
result1.RequestedBy = _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy);
}
return result1;
})
.OrderBy(vm => vm.FirstName) // Let the database handle sorting.
.ThenBy(vm => vm.LastName).ToList();
return response;
}
/// <summary>
/// Fetches a single attendance record for the logged-in employee.
/// </summary>
private async Task<List<EmployeeAttendanceVM>> GetSelfAttendanceAsync(Guid tenantId, Guid projectId, Guid employeeId, Guid? organizationId, DateTime forDate)
{
List<EmployeeAttendanceVM> result = new List<EmployeeAttendanceVM>();
// This query fetches the employee's project allocation and their attendance in a single trip.
Attendance lstAttendance = await _context.Attendes
.Include(a => a.RequestedBy)
.ThenInclude(e => e!.JobRole)
.Include(a => a.Approver)
.ThenInclude(e => e!.JobRole)
.FirstOrDefaultAsync(c => c.ProjectID == projectId && c.EmployeeId == employeeId && c.AttendanceDate.Date == forDate && c.TenantId == tenantId) ?? new Attendance();
var projectAllocationQuery = _context.ProjectAllocations
.Include(pa => pa.Project)
.Include(pa => pa.Employee)
.ThenInclude(e => e!.Organization)
.Include(pa => pa.Employee)
.ThenInclude(e => e!.JobRole)
.Where(pa => pa.EmployeeId == employeeId && pa.TenantId == tenantId && pa.IsActive &&
pa.ProjectId == projectId && pa.Project != null &&
pa.Employee != null && pa.Employee.Organization != null && pa.Employee.JobRole != null);
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,
ProjectId = projectId,
ProjectName = projectAllocation.Project?.Name,
CheckInTime = lstAttendance.InTime,
CheckOutTime = lstAttendance.OutTime,
Activity = lstAttendance.Activity,
ApprovedAt = lstAttendance.ApprovedAt,
Approver = _mapper.Map<BasicEmployeeVM>(lstAttendance.Approver),
RequestedAt = lstAttendance.RequestedAt,
RequestedBy = _mapper.Map<BasicEmployeeVM>(lstAttendance.RequestedBy)
};
result.Add(result1);
}
return result;
}
}
}