From f1f5fc263f1d18ef71387f2f3d4292748e186848 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Fri, 21 Nov 2025 13:07:55 +0530 Subject: [PATCH] Update firebase helper function for record attendance --- .../Controllers/AttendanceController.cs | 21 +- Marco.Pms.Services/Service/FirebaseService.cs | 284 +++++++++--------- .../ServiceInterfaces/IFirebaseService.cs | 2 +- 3 files changed, 158 insertions(+), 149 deletions(-) diff --git a/Marco.Pms.Services/Controllers/AttendanceController.cs b/Marco.Pms.Services/Controllers/AttendanceController.cs index 275f61f..2c90522 100644 --- a/Marco.Pms.Services/Controllers/AttendanceController.cs +++ b/Marco.Pms.Services/Controllers/AttendanceController.cs @@ -474,7 +474,7 @@ namespace MarcoBMS.Services.Controllers var _employeeHelper = scope.ServiceProvider.GetRequiredService(); var _firebase = scope.ServiceProvider.GetRequiredService(); - var currentEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); using var transaction = await _context.Database.BeginTransactionAsync(); try @@ -517,7 +517,7 @@ namespace MarcoBMS.Services.Controllers { attendance.OutTime = finalDateTime; attendance.Activity = ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE; - attendance.RequestedById = currentEmployee.Id; + attendance.RequestedById = loggedInEmployee.Id; attendance.RequestedAt = DateTime.UtcNow; } else @@ -531,7 +531,7 @@ namespace MarcoBMS.Services.Controllers { attendance.IsApproved = true; attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE; - attendance.ApprovedById = currentEmployee.Id; + attendance.ApprovedById = loggedInEmployee.Id; attendance.ApprovedAt = DateTime.UtcNow; // do nothing } @@ -539,7 +539,7 @@ namespace MarcoBMS.Services.Controllers { attendance.IsApproved = false; attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT; - attendance.ApprovedById = currentEmployee.Id; + attendance.ApprovedById = loggedInEmployee.Id; attendance.ApprovedAt = DateTime.UtcNow; // do nothing } @@ -584,7 +584,7 @@ namespace MarcoBMS.Services.Controllers Longitude = recordAttendanceDot.Longitude, TenantId = tenantId, - UpdatedBy = currentEmployee.Id, + UpdatedBy = loggedInEmployee.Id, UpdatedOn = recordAttendanceDot.Date }; //if (recordAttendanceDot.Image != null && recordAttendanceDot.Image.Count > 0) @@ -619,7 +619,7 @@ namespace MarcoBMS.Services.Controllers { sendActivity = 1; } - var notification = new { LoggedInUserId = currentEmployee.Id, Keyword = "Attendance", Activity = sendActivity, ProjectId = attendance.ProjectID, Response = vm }; + var notification = new { LoggedInUserId = loggedInEmployee.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); @@ -628,10 +628,11 @@ namespace MarcoBMS.Services.Controllers // --- 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 context = HttpContext; + string origin = context.Request.Headers["Origin"].FirstOrDefault() ?? ""; var name = $"{vm.FirstName} {vm.LastName}"; - await _firebase.SendAttendanceMessageAsync(attendance.ProjectID, name, recordAttendanceDot.Action, attendance.EmployeeId, tenantId); + await _firebase.SendAttendanceMessageAsync(attendance.ProjectID, name, recordAttendanceDot.Action, attendance.EmployeeId, origin, loggedInEmployee.Id, tenantId); }); @@ -848,10 +849,12 @@ namespace MarcoBMS.Services.Controllers // --- 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 context = HttpContext; + string origin = context.Request.Headers["Origin"].FirstOrDefault() ?? ""; var name = $"{vm.FirstName} {vm.LastName}"; - await _firebase.SendAttendanceMessageAsync(attendance.ProjectID, name, recordAttendanceDot.Action, attendance.EmployeeId, tenantId); + await _firebase.SendAttendanceMessageAsync(attendance.ProjectID, name, recordAttendanceDot.Action, attendance.EmployeeId, origin, loggedInEmployee.Id, tenantId); }); diff --git a/Marco.Pms.Services/Service/FirebaseService.cs b/Marco.Pms.Services/Service/FirebaseService.cs index fa69504..ceb2e12 100644 --- a/Marco.Pms.Services/Service/FirebaseService.cs +++ b/Marco.Pms.Services/Service/FirebaseService.cs @@ -16,6 +16,7 @@ namespace Marco.Pms.Services.Service { private readonly IDbContextFactory _dbContextFactory; private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly ILoggingService _logger; private static readonly Guid Review = Guid.Parse("6537018f-f4e9-4cb3-a210-6c3b2da999d7"); private static readonly Guid RejectedByReviewer = Guid.Parse("965eda62-7907-4963-b4a1-657fb0b2724b"); @@ -25,10 +26,12 @@ namespace Marco.Pms.Services.Service private static readonly Guid Processed = Guid.Parse("61578360-3a49-4c34-8604-7b35a3787b95"); public FirebaseService(IDbContextFactory dbContextFactory, - IServiceScopeFactory serviceScopeFactory) + IServiceScopeFactory serviceScopeFactory, + ILoggingService logger) { _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } // Auth Controller @@ -124,171 +127,174 @@ namespace Marco.Pms.Services.Service await SendMessageToMultipleDevicesOnlyDataAsync(registrationTokensForData, data); } - // Attendance Controller - public async Task SendAttendanceMessageAsync(Guid projectId, string name, ATTENDANCE_MARK_TYPE markType, Guid employeeId, Guid tenantId) + #region =================================================================== Attendance Functions =================================================================== + + /// + /// Sends attendance-related notifications for the specified project and employee attendance action. + /// + /// The Id of the project where attendance is marked. + /// Name of the employee marking attendance. + /// Type of attendance mark action. + /// Employee for whom attendance is marked. + /// Origin of the request (optional), used to filter notifications. + /// Employee Id of the caller (to exclude self-notifications). + /// Tenant identifier for multi-tenant setup. + /// Task representing the asynchronous operation. + public async Task SendAttendanceMessageAsync(Guid projectId, string name, ATTENDANCE_MARK_TYPE markType, Guid employeeId, string origin, Guid loggedInEmployeeId, Guid tenantId) { - await using var _context = await _dbContextFactory.CreateDbContextAsync(); - var projectTask = Task.Run(async () => + try { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); - return await dbContext.ProjectAllocations - .Include(pa => pa.Project) - .Where(pa => pa.ProjectId == projectId && pa.IsActive && pa.Project != null) - .GroupBy(pa => pa.ProjectId) - .Select(g => new + + // Fetch project details and assigned employees grouped by ProjectId + var project = await dbContext.ProjectAllocations + .Include(pa => pa.Project) + .Where(pa => pa.ProjectId == projectId && pa.IsActive && pa.Project != null) + .GroupBy(pa => pa.ProjectId) + .Select(g => new + { + ProjectName = g.Select(pa => pa.Project!.Name).FirstOrDefault(), + EmployeeIds = g.Select(pa => pa.EmployeeId).Distinct().ToList() + }) + .FirstOrDefaultAsync(); + + if (project == null) { - ProjectName = g.Select(pa => pa.Project!.Name).FirstOrDefault(), - EmployeeIds = g.Select(pa => pa.EmployeeId).Distinct().ToList() - }).FirstOrDefaultAsync(); - }); + _logger.LogWarning("No active project allocations found for ProjectId: {ProjectId}", projectId); + return; // or throw if critical + } - var teamAttendanceRoleTask = Task.Run(async () => - { - await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); - return await dbContext.RolePermissionMappings - .Where(rp => rp.FeaturePermissionId == PermissionsMaster.TeamAttendance) - .Select(rp => rp.ApplicationRoleId).ToListAsync(); - }); - var regularizeAttendanceRoleTask = Task.Run(async () => - { - await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); - return await dbContext.RolePermissionMappings - .Where(rp => rp.FeaturePermissionId == PermissionsMaster.RegularizeAttendance) - .Select(rp => rp.ApplicationRoleId).ToListAsync(); - }); - var manageProjectsRoleTask = Task.Run(async () => - { - await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); - return await dbContext.RolePermissionMappings - .Where(rp => rp.FeaturePermissionId == PermissionsMaster.ManageProject) - .Select(rp => rp.ApplicationRoleId).ToListAsync(); - }); + var projectAssignedEmployeeIds = project.EmployeeIds; - await Task.WhenAll(projectTask, teamAttendanceRoleTask, manageProjectsRoleTask, regularizeAttendanceRoleTask); + // Load required role IDs in parallel for efficiency + var teamAttendanceRoleIdsTask = dbContext.RolePermissionMappings.Where(rp => rp.FeaturePermissionId == PermissionsMaster.TeamAttendance).Select(rp => rp.ApplicationRoleId).ToListAsync(); + var regularizeAttendanceRoleIdsTask = dbContext.RolePermissionMappings.Where(rp => rp.FeaturePermissionId == PermissionsMaster.RegularizeAttendance).Select(rp => rp.ApplicationRoleId).ToListAsync(); + var manageProjectsRoleIdsTask = dbContext.RolePermissionMappings.Where(rp => rp.FeaturePermissionId == PermissionsMaster.ManageProject).Select(rp => rp.ApplicationRoleId).ToListAsync(); - var teamAttendanceRoleIds = teamAttendanceRoleTask.Result; - var regularizeAttendanceRoleIds = regularizeAttendanceRoleTask.Result; - var manageProjectsRoleIds = manageProjectsRoleTask.Result; - var project = projectTask.Result; + await Task.WhenAll(teamAttendanceRoleIdsTask, regularizeAttendanceRoleIdsTask, manageProjectsRoleIdsTask); - List projectAssignedEmployeeIds = project?.EmployeeIds ?? new List(); + var teamAttendanceRoleIds = teamAttendanceRoleIdsTask.Result; + var regularizeAttendanceRoleIds = regularizeAttendanceRoleIdsTask.Result; + var manageProjectsRoleIds = manageProjectsRoleIdsTask.Result; - var employeeIdsTask = Task.Run(async () => - { - await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); - return await dbContext.EmployeeRoleMappings - .Where(er => (projectAssignedEmployeeIds.Contains(er.EmployeeId) || manageProjectsRoleIds.Contains(er.RoleId)) && teamAttendanceRoleIds.Contains(er.RoleId)) - .Select(er => er.EmployeeId) - .ToListAsync(); - }); - var teamEmployeeIdsTask = Task.Run(async () => - { - await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); - return await dbContext.EmployeeRoleMappings - .Where(er => projectAssignedEmployeeIds.Contains(er.EmployeeId) || manageProjectsRoleIds.Contains(er.RoleId)) - .Select(er => er.EmployeeId) - .ToListAsync(); - }); + // Fetch employees eligible for attendance notifications + var employeeIds = await dbContext.EmployeeRoleMappings + .Where(er => (projectAssignedEmployeeIds.Contains(er.EmployeeId) || manageProjectsRoleIds.Contains(er.RoleId)) + && teamAttendanceRoleIds.Contains(er.RoleId)) + .Select(er => er.EmployeeId) + .Distinct() + .ToListAsync(); - await Task.WhenAll(employeeIdsTask, teamEmployeeIdsTask); + // Fetch team employees (for data-only notifications) + var teamEmployeeIds = await dbContext.EmployeeRoleMappings + .Where(er => projectAssignedEmployeeIds.Contains(er.EmployeeId) || manageProjectsRoleIds.Contains(er.RoleId)) + .Select(er => er.EmployeeId) + .Distinct() + .ToListAsync(); - var employeeIds = employeeIdsTask.Result; - var teamEmployeeIds = teamEmployeeIdsTask.Result; + List messageNotificationIds = new(); - var mesaageNotificationIds = new List(); - - Notification notificationFirebase; - switch (markType) - { - case ATTENDANCE_MARK_TYPE.CHECK_IN: - notificationFirebase = new Notification + // Construct notification content based on attendance mark type + Notification notificationFirebase = markType switch + { + ATTENDANCE_MARK_TYPE.CHECK_IN => new Notification { Title = "Attendance Update", - Body = $" {name} has checked in for project {project?.ProjectName ?? ""}." - }; - mesaageNotificationIds.AddRange(employeeIds); - break; - case ATTENDANCE_MARK_TYPE.CHECK_OUT: - notificationFirebase = new Notification + Body = $"{name} has checked in for project {project.ProjectName}." + }, + ATTENDANCE_MARK_TYPE.CHECK_OUT => new Notification { Title = "Attendance Update", - Body = $" {name} has checked out for project {project?.ProjectName ?? ""}." - }; - mesaageNotificationIds.AddRange(employeeIds); - break; - case ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE: - notificationFirebase = new Notification + Body = $"{name} has checked out for project {project.ProjectName}." + }, + ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE => new Notification { Title = "Regularization Request", - Body = $" {name} has submitted a regularization request for project {project?.ProjectName ?? ""}." - }; - mesaageNotificationIds = await _context.EmployeeRoleMappings - .Where(er => (projectAssignedEmployeeIds.Contains(er.EmployeeId) || manageProjectsRoleIds.Contains(er.RoleId)) && regularizeAttendanceRoleIds.Contains(er.RoleId)) - .Select(er => er.EmployeeId) - .ToListAsync(); - break; - case ATTENDANCE_MARK_TYPE.REGULARIZE: - notificationFirebase = new Notification + Body = $"{name} has submitted a regularization request for project {project.ProjectName}." + }, + ATTENDANCE_MARK_TYPE.REGULARIZE => new Notification { - Title = " Regularization Approved", - Body = $" {name}'s regularization request for project {project?.ProjectName ?? ""} has been accepted." - }; - mesaageNotificationIds.Add(employeeId); - break; - case ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT: - notificationFirebase = new Notification + Title = "Regularization Approved", + Body = $"{name}'s regularization request for project {project.ProjectName} has been accepted." + }, + ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT => new Notification { Title = "Regularization Denied", - Body = $" {name}'s regularization request for project {project?.ProjectName ?? ""} has been rejected." - }; - mesaageNotificationIds.Add(employeeId); - break; - default: - notificationFirebase = new Notification + Body = $"{name}'s regularization request for project {project.ProjectName} has been rejected." + }, + _ => new Notification { Title = "Attendance Update", - Body = $" {name} has update his/her attendance for project {project?.ProjectName ?? ""}." - }; - break; + Body = $"{name} has updated attendance for project {project.ProjectName}." + } + }; + + // Set notification recipients based on type + if (markType == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE) + { + messageNotificationIds = await dbContext.EmployeeRoleMappings + .Where(er => (projectAssignedEmployeeIds.Contains(er.EmployeeId) || manageProjectsRoleIds.Contains(er.RoleId)) + && regularizeAttendanceRoleIds.Contains(er.RoleId)) + .Select(er => er.EmployeeId) + .Distinct() + .ToListAsync(); + } + else if (markType == ATTENDANCE_MARK_TYPE.REGULARIZE || markType == ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT) + { + messageNotificationIds.Add(employeeId); + } + else + { + messageNotificationIds.AddRange(employeeIds); + } + + var dataPayload = new Dictionary + { + { "Keyword", "Attendance" }, + { "ProjectId", projectId.ToString() }, + { "Action", markType.ToString() } + }; + + // Filter out current (logged-in) employee from notifications if origin is not provided (optional) + if (string.IsNullOrWhiteSpace(origin)) + { + messageNotificationIds = messageNotificationIds.Where(e => e != Guid.Empty && e != loggedInEmployeeId).Distinct().ToList(); + teamEmployeeIds = teamEmployeeIds.Where(e => e != Guid.Empty && e != loggedInEmployeeId).Distinct().ToList(); + } + + // Fetch FCM tokens for notification recipients + var registrationTokensForNotification = messageNotificationIds.Count > 0 + ? await dbContext.FCMTokenMappings.Where(ft => messageNotificationIds.Contains(ft.EmployeeId) && ft.ExpiredAt >= DateTime.UtcNow && ft.TenantId == tenantId).Select(ft => ft.FcmToken).ToListAsync() + : new List(); + + // Fetch FCM tokens for data-only notification recipients + var registrationTokensForData = teamEmployeeIds.Count > 0 + ? await dbContext.FCMTokenMappings.Where(ft => teamEmployeeIds.Contains(ft.EmployeeId) && ft.ExpiredAt >= DateTime.UtcNow && ft.TenantId == tenantId).Select(ft => ft.FcmToken).ToListAsync() + : new List(); + + // Send notifications concurrently + var sendNotificationTask = registrationTokensForNotification.Count > 0 + ? SendMessageToMultipleDevicesWithDataAsync(registrationTokensForNotification, notificationFirebase, dataPayload) + : Task.CompletedTask; + + var sendDataOnlyTask = registrationTokensForData.Count > 0 + ? SendMessageToMultipleDevicesOnlyDataAsync(registrationTokensForData, dataPayload) + : Task.CompletedTask; + + await Task.WhenAll(sendNotificationTask, sendDataOnlyTask); + + _logger.LogInfo("Attendance message sent successfully for ProjectId {ProjectId}, MarkType {MarkType}, EmployeeId {EmployeeId}, Origin {Origin}", + projectId, markType, employeeId, origin); } - - // List of device registration tokens to send the message to - - var data = new Dictionary() - { - { "Keyword", "Attendance" }, - { "ProjectId", projectId.ToString() }, - { "Action", markType.ToString() } - }; - - var registrationTokensForNotificationTask = Task.Run(async () => + catch (Exception ex) { - if (mesaageNotificationIds.Any()) - { - await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); - var registrationTokensForNotification = await dbContext.FCMTokenMappings - .Where(ft => mesaageNotificationIds.Contains(ft.EmployeeId) && ft.ExpiredAt >= DateTime.UtcNow && ft.TenantId == tenantId) - .Select(ft => ft.FcmToken).ToListAsync(); - - await SendMessageToMultipleDevicesWithDataAsync(registrationTokensForNotification, notificationFirebase, data); - } - }); - var registrationTokensForDataTask = Task.Run(async () => - { - if (teamEmployeeIds.Any()) - { - await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); - var registrationTokensForData = await dbContext.FCMTokenMappings - .Where(ft => teamEmployeeIds.Contains(ft.EmployeeId) && ft.ExpiredAt >= DateTime.UtcNow && ft.TenantId == tenantId) - .Select(ft => ft.FcmToken).ToListAsync(); - - await SendMessageToMultipleDevicesOnlyDataAsync(registrationTokensForData, data); - } - }); - - await Task.WhenAll(registrationTokensForNotificationTask, registrationTokensForDataTask); + _logger.LogError(ex, "Error sending attendance message for ProjectId {ProjectId}, MarkType {MarkType}, EmployeeId {EmployeeId}, Origin {Origin}", + projectId, markType, employeeId, origin); + } } + + #endregion // Task Controller public async Task SendAssignTaskMessageAsync(Guid workItemId, string name, List teamMembers, Guid tenantId) { diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IFirebaseService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IFirebaseService.cs index 53fc50d..866d8ea 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IFirebaseService.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IFirebaseService.cs @@ -12,7 +12,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task SendEmployeeSuspendMessageAsync(Guid employeeId, Guid tenantId); - Task SendAttendanceMessageAsync(Guid projectId, string Name, ATTENDANCE_MARK_TYPE markType, Guid employeeId, Guid tenantId); + Task SendAttendanceMessageAsync(Guid projectId, string Name, ATTENDANCE_MARK_TYPE markType, Guid employeeId, string origin, Guid loggedInEmployeeId, Guid tenantId); Task SendAssignTaskMessageAsync(Guid workItemId, string name, List teamMembers, Guid tenantId); Task SendReportTaskMessageAsync(Guid taskAllocationId, string name, Guid tenantId); Task SendTaskCommentMessageAsync(Guid taskAllocationId, string name, Guid tenantId);