using System.Globalization; 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.Mapper; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.AttendanceVM; using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Service; 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 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 EmployeeHelper _employeeHelper; private readonly ProjectsHelper _projectsHelper; private readonly UserHelper _userHelper; private readonly S3UploadService _s3Service; private readonly PermissionServices _permission; private readonly ILoggingService _logger; private readonly IHubContext _signalR; public AttendanceController( ApplicationDbContext context, EmployeeHelper employeeHelper, ProjectsHelper projectsHelper, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permission, IHubContext signalR) { _context = context; _employeeHelper = employeeHelper; _projectsHelper = projectsHelper; _userHelper = userHelper; _s3Service = s3Service; _logger = logger; _permission = permission; _signalR = signalR; } private Guid GetTenantId() { return _userHelper.GetTenantId(); //var tenant = User.FindFirst("TenantId")?.Value; //return (tenant != null ? Convert.ToInt32(tenant) : 1); } [HttpGet("log/attendance/{attendanceid}")] public async Task GetAttendanceLogById(Guid attendanceid) { Guid TenantId = GetTenantId(); List 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 attendanceLogVMs = new List(); foreach (var attendanceLog in lstAttendance) { string objectKey = attendanceLog.Document != null ? attendanceLog.Document.S3Key : string.Empty; string preSignedUrl = string.IsNullOrEmpty(objectKey) ? string.Empty : _s3Service.GeneratePreSignedUrlAsync(objectKey); attendanceLogVMs.Add(attendanceLog.ToAttendanceLogVMFromAttendanceLog(preSignedUrl, preSignedUrl)); } _logger.LogInfo("{count} Attendance records fetched successfully", lstAttendance.Count); return Ok(ApiResponse.SuccessResponse(attendanceLogVMs, System.String.Format("{0} Attendance records fetched successfully", lstAttendance.Count), 200)); } [HttpGet("log/employee/{employeeId}")] public async Task GetAttendanceLogByEmployeeId(Guid employeeId, [FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null) { Guid TenantId = GetTenantId(); DateTime fromDate = new DateTime(); DateTime toDate = new DateTime(); if (dateFrom != null && DateTime.TryParse(dateFrom, out fromDate) == false) { _logger.LogError("User sent Invalid from Date while featching attendance logs"); return BadRequest(ApiResponse.ErrorResponse("Invalid Date", "Invalid Date", 400)); } if (dateTo != null && DateTime.TryParse(dateTo, out toDate) == false) { _logger.LogError("User sent Invalid to Date while featching attendance logs"); return BadRequest(ApiResponse.ErrorResponse("Invalid Date", "Invalid Date", 400)); } if (employeeId == Guid.Empty) { _logger.LogError("The employee Id sent by user is empty"); return BadRequest(ApiResponse.ErrorResponse("Employee ID is required and must not be Empty.", "Employee ID is required and must not be empty.", 400)); } List attendances = await _context.Attendes.Where(c => c.EmployeeID == employeeId && c.TenantId == TenantId && c.AttendanceDate.Date >= fromDate && c.AttendanceDate.Date <= toDate).ToListAsync(); Employee? employee = await _context.Employees.Include(e => e.JobRole).FirstOrDefaultAsync(e => e.Id == employeeId && e.TenantId == TenantId && e.IsActive); List results = new List(); if (employee != null) { foreach (var attendance in attendances) { EmployeeAttendanceVM result = new EmployeeAttendanceVM { Id = attendance.Id, EmployeeId = employee.Id, FirstName = employee.FirstName, LastName = employee.LastName, CheckInTime = attendance.InTime, CheckOutTime = attendance.OutTime, JobRoleName = employee.JobRole != null ? employee.JobRole.Name : "", Activity = attendance.Activity, EmployeeAvatar = null }; results.Add(result); } } _logger.LogInfo("{count} Attendance records fetched successfully", results.Count); return Ok(ApiResponse.SuccessResponse(results, System.String.Format("{0} Attendance records fetched successfully", results.Count), 200)); } /// /// /// /// ProjectID /// YYYY-MM-dd /// [HttpGet("project/log")] public async Task EmployeeAttendanceByDateRange([FromQuery] Guid projectId, [FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null) { Guid TenantId = GetTenantId(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var hasTeamAttendancePermission = await _permission.HasPermission(new Guid("915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"), LoggedInEmployee.Id); var hasSelfAttendancePermission = await _permission.HasPermission(new Guid("ccb0589f-712b-43de-92ed-5b6088e7dc4e"), LoggedInEmployee.Id); var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId.ToString()); if (!hasProjectPermission) { _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)); } DateTime fromDate = new DateTime(); DateTime toDate = new DateTime(); if (dateFrom != null && DateTime.TryParse(dateFrom, out fromDate) == false) { _logger.LogError("User sent Invalid fromDate while featching attendance logs"); return BadRequest(ApiResponse.ErrorResponse("Invalid Date", "Invalid Date", 400)); } if (dateTo != null && DateTime.TryParse(dateTo, out toDate) == false) { _logger.LogError("User sent Invalid toDate while featching attendance logs"); return BadRequest(ApiResponse.ErrorResponse("Invalid Date", "Invalid Date", 400)); } if (projectId == Guid.Empty) { _logger.LogError("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)); } var result = new List(); //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 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 _projectsHelper.GetTeamByProject(TenantId, projectId, 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 }; 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; } else { result1.FirstName = null; result1.LastName = null; result1.JobRoleName = null; } result.Add(result1); } } } 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); 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, CheckInTime = attendance.InTime, CheckOutTime = attendance.OutTime, Activity = attendance.Activity }; result.Add(result1); } } } _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)); } /// /// /// /// ProjectID /// YYYY-MM-dd /// [HttpGet("project/team")] public async Task EmployeeAttendanceByProject([FromQuery] Guid projectId, [FromQuery] bool IncludeInActive, [FromQuery] string? date = null) { Guid TenantId = GetTenantId(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var hasTeamAttendancePermission = await _permission.HasPermission(new Guid("915e6bff-65f6-4e3f-aea8-3fd217d3ea9e"), LoggedInEmployee.Id); var hasSelfAttendancePermission = await _permission.HasPermission(new Guid("ccb0589f-712b-43de-92ed-5b6088e7dc4e"), LoggedInEmployee.Id); var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId.ToString()); if (!hasProjectPermission) { _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)); } DateTime forDate = new DateTime(); if (date != null && DateTime.TryParse(date, out forDate) == false) { _logger.LogError("User sent Invalid Date while featching attendance logs"); return BadRequest(ApiResponse.ErrorResponse("Invalid Date", "Invalid Date", 400)); } if (projectId == Guid.Empty) { _logger.LogError("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)); } var result = new List(); Attendance? attendance = null; if (date == null) forDate = DateTime.UtcNow.Date; if (hasTeamAttendancePermission) { List lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.AttendanceDate.Date == forDate && c.TenantId == TenantId).ToListAsync(); List projectteam = await _projectsHelper.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 (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); } } result.Sort(delegate (EmployeeAttendanceVM x, EmployeeAttendanceVM y) { //return x.FirstName.CompareTo(y.FirstName); return string.Compare(x.FirstName, y.FirstName, StringComparison.Ordinal); }); } else if (hasSelfAttendancePermission) { 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.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) { Guid TenantId = GetTenantId(); Employee LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var result = new List(); var hasProjectPermission = await _permission.HasProjectPermission(LoggedInEmployee, projectId.ToString()); if (!hasProjectPermission) { _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)); } List lstAttendance = await _context.Attendes.Where(c => c.ProjectID == projectId && c.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE && c.TenantId == TenantId).ToListAsync(); List projectteam = await _projectsHelper.GetTeamByProject(TenantId, projectId, true); var idList = projectteam.Select(p => p.EmployeeId).ToList(); var jobRole = await _context.JobRoles.ToListAsync(); foreach (Attendance attende in lstAttendance) { var result1 = new EmployeeAttendanceVM() { Id = attende.Id, CheckInTime = attende.InTime, CheckOutTime = attende.OutTime, Activity = attende.Activity, EmployeeAvatar = null, EmployeeId = attende.EmployeeID, }; var teamMember = projectteam.Find(m => m.EmployeeId == attende.EmployeeID); if (teamMember != null && teamMember.Employee != null && teamMember.Employee.JobRole != null) { result1.FirstName = teamMember.Employee.FirstName; result1.LastName = teamMember.Employee.LastName; result1.JobRoleName = teamMember.Employee.JobRole.Name; } 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.SuccessResponse(result, System.String.Format("{0} Attendance records fetched successfully", result.Count), 200)); } [HttpPost] [Route("record")] public async Task RecordAttendance([FromBody] RecordAttendanceDot recordAttendanceDot) { if (!ModelState.IsValid) { var errors = ModelState.Values .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); _logger.LogError("User sent Invalid Date while marking attendance"); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } Guid TenantId = GetTenantId(); 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.LogError("User sent Invalid Mark Time while marking attendance"); return BadRequest(ApiResponse.ErrorResponse("Invalid Mark Time", "Invalid Mark Time", 400)); } DateTime finalDateTime = GetDateFromTimeStamp(recordAttendanceDot.Date, recordAttendanceDot.MarkTime); if (recordAttendanceDot.Comment == null) { _logger.LogError("User sent Invalid comment while marking attendance"); return BadRequest(ApiResponse.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; //string timeString = "10:30 PM"; // Format: "hh:mm tt" 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; } else { _logger.LogError("Employee {EmployeeId} sent regularization request but it check-out time is earlier than check-out"); return BadRequest(ApiResponse.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.ApprovedBy = currentEmployee.Id; // do nothing } else if (recordAttendanceDot.Action == ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT) { attendance.IsApproved = false; attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT; // 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); return Ok(ApiResponse.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.SuccessResponse(new EmployeeAttendanceVM(), "Attendance marked successfully.", 200)); } catch (Exception ex) { await transaction.RollbackAsync(); // Rollback on failure _logger.LogError("{Error} while marking attendance", ex.Message); var response = new { message = ex.Message, detail = ex.StackTrace, statusCode = StatusCodes.Status500InternalServerError }; return BadRequest(ApiResponse.ErrorResponse(ex.Message, response, 400)); } } [HttpPost] [Route("record-image")] public async Task RecordAttendanceWithImage([FromBody] RecordAttendanceDot recordAttendanceDot) { if (!ModelState.IsValid) { var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); _logger.LogError("Invalid attendance model received."); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } Guid tenantId = GetTenantId(); 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.ErrorResponse("Invalid Mark Time", "Mark time is required", 400)); } if (string.IsNullOrWhiteSpace(recordAttendanceDot.Comment)) { _logger.LogWarning("Empty comment provided."); return BadRequest(ApiResponse.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; } else { _logger.LogWarning("Regularization check-out time is before check-in."); return BadRequest(ApiResponse.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; break; case ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT: attendance.IsApproved = false; attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT; 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.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.GeneratePreSignedUrlAsync(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); _logger.LogInfo("Attendance recorded for employee: {FullName}", $"{employee.FirstName} {employee.LastName}"); return Ok(ApiResponse.SuccessResponse(vm, "Attendance marked successfully.", 200)); } catch (Exception ex) { await transaction.RollbackAsync(); _logger.LogError("Error while recording attendance : {Error}", ex.Message); return BadRequest(ApiResponse.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; } } }