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.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 EmployeeHelper _employeeHelper; private readonly IProjectServices _projectServices; private readonly UserHelper _userHelper; private readonly S3UploadService _s3Service; private readonly PermissionServices _permission; private readonly ILoggingService _logger; private readonly IHubContext _signalR; private readonly IFirebaseService _firebase; public AttendanceController( ApplicationDbContext context, EmployeeHelper employeeHelper, IProjectServices projectServices, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permission, IHubContext signalR, IFirebaseService firebase) { _context = context; _employeeHelper = employeeHelper; _projectServices = projectServices; _userHelper = userHelper; _s3Service = s3Service; _logger = logger; _permission = permission; _signalR = signalR; _firebase = firebase; } 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.GeneratePreSignedUrl(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.LogWarning("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.LogWarning("User sent Invalid to Date while featching attendance logs"); return BadRequest(ApiResponse.ErrorResponse("Invalid Date", "Invalid Date", 400)); } if (employeeId == Guid.Empty) { _logger.LogWarning("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] Guid? organizationId, [FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null) { 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); 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.LogWarning("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.LogWarning("User sent Invalid toDate 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)); } 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 _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 }; 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; } else { result1.FirstName = null; result1.LastName = null; result1.JobRoleName = null; result1.OrganizationName = 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(); 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) { 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, 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)); } [HttpGet("project/team")] /// /// 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) { var tenantId = GetTenantId(); 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.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.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; 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.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.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.ErrorResponse("An internal server error occurred.")); } } [HttpGet("regularize")] public async Task GetRequestRegularizeAttendance([FromQuery] Guid projectId, [FromQuery] Guid? organizationId, [FromQuery] bool IncludeInActive) { Guid TenantId = GetTenantId(); Employee LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var result = new List(); 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.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 _projectServices.GetTeamByProject(TenantId, projectId, organizationId, 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; result1.OrganizationName = teamMember.Employee.Organization?.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.LogWarning("User sent Invalid Date while marking attendance \n {Error}", string.Join(",", errors)); 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.LogWarning("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.LogWarning("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.LogWarning("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); _ = 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.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(ex, "An Error occured while marking attendance"); 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.LogWarning("Invalid attendance model received. \n {Error}", string.Join(",", errors)); 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.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.SuccessResponse(vm, "Attendance marked successfully.", 200)); } catch (Exception ex) { await transaction.RollbackAsync(); _logger.LogError(ex, "Error while recording attendance"); 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; } /// /// 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; } } }