diff --git a/Marco.Pms.Model/Projects/Building.cs b/Marco.Pms.Model/Projects/Building.cs index 0b60feb..ee2c6cd 100644 --- a/Marco.Pms.Model/Projects/Building.cs +++ b/Marco.Pms.Model/Projects/Building.cs @@ -1,6 +1,6 @@ -using System.ComponentModel; +using Marco.Pms.Model.Utilities; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using Marco.Pms.Model.Utilities; namespace Marco.Pms.Model.Projects { diff --git a/Marco.Pms.Services/Controllers/TaskController.cs b/Marco.Pms.Services/Controllers/TaskController.cs index 9f648ac..e8dc7a2 100644 --- a/Marco.Pms.Services/Controllers/TaskController.cs +++ b/Marco.Pms.Services/Controllers/TaskController.cs @@ -9,6 +9,7 @@ using Marco.Pms.Model.ViewModels.Activities; using Marco.Pms.Services.Helpers; 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; @@ -33,9 +34,10 @@ namespace MarcoBMS.Services.Controllers private readonly IHubContext _signalR; private readonly CacheUpdateHelper _cache; private readonly PermissionServices _permissionServices; + private readonly IFirebaseService _firebase; public TaskController(ApplicationDbContext context, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permissionServices, - IHubContext signalR, CacheUpdateHelper cache) + IHubContext signalR, CacheUpdateHelper cache, IFirebaseService firebase) { _context = context; _userHelper = userHelper; @@ -44,6 +46,7 @@ namespace MarcoBMS.Services.Controllers _signalR = signalR; _cache = cache; _permissionServices = permissionServices; + _firebase = firebase; } private Guid GetTenantId() @@ -66,28 +69,28 @@ namespace MarcoBMS.Services.Controllers return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } - // Retrieve tenant and employee context + // Retrieve tenant and loggedInEmployee context var tenantId = GetTenantId(); - var employee = await _userHelper.GetCurrentEmployeeAsync(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); // Check for permission to approve tasks - var hasPermission = await _permissionServices.HasPermission(PermissionsMaster.AssignAndReportProgress, employee.Id); + var hasPermission = await _permissionServices.HasPermission(PermissionsMaster.AssignAndReportProgress, loggedInEmployee.Id); if (!hasPermission) { - _logger.LogWarning("Employee {EmployeeId} attempted to assign Task without permission", employee.Id); + _logger.LogWarning("Employee {EmployeeId} attempted to assign Task without permission", loggedInEmployee.Id); return StatusCode(403, ApiResponse.ErrorResponse("You don't have access", "User not authorized to approve tasks", 403)); } - _logger.LogInfo("Employee {EmployeeId} is assigning a new task", employee.Id); + _logger.LogInfo("Employee {EmployeeId} is assigning a new task", loggedInEmployee.Id); // Convert DTO to entity and save TaskAllocation - var taskAllocation = assignTask.ToTaskAllocationFromAssignTaskDto(employee.Id, tenantId); + var taskAllocation = assignTask.ToTaskAllocationFromAssignTaskDto(loggedInEmployee.Id, tenantId); _context.TaskAllocations.Add(taskAllocation); await _context.SaveChangesAsync(); await _cache.UpdatePlannedAndCompleteWorksInWorkItem(taskAllocation.WorkItemId, todaysAssigned: taskAllocation.PlannedTask); - _logger.LogInfo("Task {TaskId} assigned by Employee {EmployeeId}", taskAllocation.Id, employee.Id); + _logger.LogInfo("Task {TaskId} assigned by Employee {EmployeeId}", taskAllocation.Id, loggedInEmployee.Id); var response = taskAllocation.ToAssignTaskVMFromTaskAllocation(); @@ -117,6 +120,18 @@ namespace MarcoBMS.Services.Controllers var team = employees.Select(e => e.ToBasicEmployeeVMFromEmployee()).ToList(); response.teamMembers = team; + _ = 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 = $"{loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; + + await _firebase.SendAssignTaskMessageAsync(taskAllocation.WorkItemId, name, employeeIds, tenantId); + + }); + return Ok(ApiResponse.SuccessResponse(response, "Task assigned successfully", 200)); } diff --git a/Marco.Pms.Services/Service/FirebaseService.cs b/Marco.Pms.Services/Service/FirebaseService.cs index e3b0cba..7bd063a 100644 --- a/Marco.Pms.Services/Service/FirebaseService.cs +++ b/Marco.Pms.Services/Service/FirebaseService.cs @@ -34,7 +34,7 @@ namespace Marco.Pms.Services.Service await SendMessageToMultipleDevicesAsync(registrationTokens, notificationFirebase); } - public async Task SendAttendanceMessageAsync(Guid projectId, string Name, ATTENDANCE_MARK_TYPE markType, Guid tenantId) + public async Task SendAttendanceMessageAsync(Guid projectId, string name, ATTENDANCE_MARK_TYPE markType, Guid tenantId) { await using var _context = await _dbContextFactory.CreateDbContextAsync(); var projectTask = Task.Run(async () => @@ -86,42 +86,42 @@ namespace Marco.Pms.Services.Service notificationFirebase = new Notification { Title = "Attendance Update", - Body = $" {Name} has checked in for project {project?.ProjectName ?? ""}." + Body = $" {name} has checked in for project {project?.ProjectName ?? ""}." }; break; case ATTENDANCE_MARK_TYPE.CHECK_OUT: notificationFirebase = new Notification { Title = "Attendance Update", - Body = $" {Name} has checked out for project {project?.ProjectName ?? ""}." + Body = $" {name} has checked out for project {project?.ProjectName ?? ""}." }; break; case ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE: notificationFirebase = new Notification { Title = "Regularization Request", - Body = $" {Name} has submitted a regularization request for project {project?.ProjectName ?? ""}." + Body = $" {name} has submitted a regularization request for project {project?.ProjectName ?? ""}." }; break; case ATTENDANCE_MARK_TYPE.REGULARIZE: notificationFirebase = new Notification { Title = " Regularization Approved", - Body = $" {Name}'s regularization request for project {project?.ProjectName ?? ""} has been accepted." + Body = $" {name}'s regularization request for project {project?.ProjectName ?? ""} has been accepted." }; break; case ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT: notificationFirebase = new Notification { Title = "Regularization Denied", - Body = $" {Name}'s regularization request for project {project?.ProjectName ?? ""} has been rejected." + Body = $" {name}'s regularization request for project {project?.ProjectName ?? ""} has been rejected." }; break; default: notificationFirebase = new Notification { Title = "Attendance Update", - Body = $" {Name} has update his/her attendance for project {project?.ProjectName ?? ""}." + Body = $" {name} has update his/her attendance for project {project?.ProjectName ?? ""}." }; break; } @@ -140,6 +140,106 @@ namespace Marco.Pms.Services.Service await SendMessageToMultipleDevicesWithDataAsync(registrationTokens, notificationFirebase, data); } + + public async Task SendAssignTaskMessageAsync(Guid workItemId, string name, List teamMembers, Guid tenantId) + { + using var scope = _serviceScopeFactory.CreateScope(); + var _logger = scope.ServiceProvider.GetRequiredService(); + + try + { + await using var _context = await _dbContextFactory.CreateDbContextAsync(); + + var workItem = await _context.WorkItems + .Include(wi => wi.WorkArea) + .ThenInclude(wa => wa!.Floor) + .ThenInclude(f => f!.Building) + .FirstOrDefaultAsync(wi => wi.Id == workItemId && wi.WorkArea != null && wi.WorkArea.Floor != null && wi.WorkArea.Floor.Building != null); + + if (workItem == null) + { + return; + } + + var projectId = workItem.WorkArea!.Floor!.Building!.ProjectId; + + var projectTask = Task.Run(async () => + { + 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 + { + ProjectName = g.Select(pa => pa.Project!.Name).FirstOrDefault(), + EmployeeIds = g.Select(pa => pa.EmployeeId).Distinct().ToList() + }).FirstOrDefaultAsync(); + }); + + var viewTaskRoleTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.RolePermissionMappings + .Where(rp => rp.FeaturePermissionId == PermissionsMaster.ViewTask) + .Select(rp => rp.ApplicationRoleId).ToListAsync(); + }); + + await Task.WhenAll(projectTask, viewTaskRoleTask); + + var viewTaskRoleIds = viewTaskRoleTask.Result; + var project = projectTask.Result; + + var buildingName = workItem.WorkArea.Floor.Building.Name; + var FloorName = workItem.WorkArea.Floor.FloorName; + var AreaName = workItem.WorkArea.AreaName; + + var location = $"{buildingName} > {FloorName} > {AreaName}"; + + List projectAssignedEmployeeIds = project?.EmployeeIds ?? new List(); + + var employeeIds = await _context.EmployeeRoleMappings + .Where(er => projectAssignedEmployeeIds.Contains(er.EmployeeId) && viewTaskRoleIds.Contains(er.RoleId)) + .Select(er => er.EmployeeId) + .ToListAsync(); + + var data = new Dictionary() + { + { "Keyword", "Assign_Task" }, + { "ProjectId", projectId.ToString() } + }; + + + // List of device registration tokens to send the message to + + var registrationTokensForNotificationTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + var registrationTokensForNotification = await dbContext.FCMTokenMappings.Where(ft => teamMembers.Contains(ft.EmployeeId)).Select(ft => ft.FcmToken).ToListAsync(); + var notificationFirebase = new Notification + { + Title = $"Task Assigned for {project?.ProjectName}", + Body = $"A task has been assigned to you by {name} at {location}" + }; + + await SendMessageToMultipleDevicesWithDataAsync(registrationTokensForNotification, notificationFirebase, data); + }); + var registrationTokensForDataTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + var registrationTokensForData = await dbContext.FCMTokenMappings.Where(ft => employeeIds.Contains(ft.EmployeeId)).Select(ft => ft.FcmToken).ToListAsync(); + + await SendMessageToMultipleDevicesOnlyDataAsync(registrationTokensForData, data); + }); + + await Task.WhenAll(registrationTokensForNotificationTask, registrationTokensForDataTask); + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception occured while get data for sending notification"); + } + } + public async Task SendMessageToMultipleDevicesWithDataAsync(List registrationTokens, Notification notificationFirebase, Dictionary data) { using var scope = _serviceScopeFactory.CreateScope(); @@ -190,6 +290,56 @@ namespace Marco.Pms.Services.Service _logger.LogError(ex, "Exception Occured while sending notification to firebase"); } + } + public async Task SendMessageToMultipleDevicesOnlyDataAsync(List registrationTokens, Dictionary data) + { + using var scope = _serviceScopeFactory.CreateScope(); + var _logger = scope.ServiceProvider.GetRequiredService(); + try + { + var message = new MulticastMessage() + { + Tokens = registrationTokens, + Data = data + }; + // Send the multicast message + var response = await FirebaseMessaging.DefaultInstance.SendEachForMulticastAsync(message); + + _logger.LogInfo("{SuccessCount} messages were sent successfully.", response.SuccessCount); + + if (response.FailureCount > 0) + { + var failedTokens = new List(); + for (int i = 0; i < response.Responses.Count; i++) + { + if (!response.Responses[i].IsSuccess) + { + failedTokens.Add(registrationTokens[i]); + } + } + _logger.LogInfo("List of tokens that caused failures: " + string.Join(", ", failedTokens)); + } + } + catch (FirebaseMessagingException ex) + { + // Log the specific Firebase error. + _logger.LogError(ex, "Error sending push notification"); + + // Check for specific error codes that indicate an invalid or unregistered token. + if (ex.MessagingErrorCode == MessagingErrorCode.Unregistered || + ex.MessagingErrorCode == MessagingErrorCode.InvalidArgument) + { + _logger.LogWarning("FCM token is invalid and should be deleted from the database"); + + // TODO: Implement the logic here to remove the invalid token from your database. + // Example: await YourTokenService.DeleteTokenAsync(loginDto.DeviceToken); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception Occured while sending notification to firebase"); + } + } public async Task SendMessageToMultipleDevicesAsync(List registrationTokens, Notification notificationFirebase) { diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IFirebaseService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IFirebaseService.cs index 6b7d151..bc5eb8f 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IFirebaseService.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IFirebaseService.cs @@ -6,5 +6,6 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces { Task SendLoginMessageAsync(string name); Task SendAttendanceMessageAsync(Guid projectId, string Name, ATTENDANCE_MARK_TYPE markType, Guid tenantId); + Task SendAssignTaskMessageAsync(Guid workItemId, string name, List teamMembers, Guid tenantId); } }