diff --git a/Marco.Pms.Services/Service/FirebaseService.cs b/Marco.Pms.Services/Service/FirebaseService.cs index afacc6a..7a6e763 100644 --- a/Marco.Pms.Services/Service/FirebaseService.cs +++ b/Marco.Pms.Services/Service/FirebaseService.cs @@ -2,6 +2,7 @@ using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Dtos.Attendance; using Marco.Pms.Model.Entitlements; +using Marco.Pms.Model.Projects; using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Service; using Microsoft.EntityFrameworkCore; @@ -669,6 +670,7 @@ namespace Marco.Pms.Services.Service } // Project Controller + //Project Infra public async Task SendModifyTaskMeaasgeAsync(List workItemIds, string name, bool IsExist, Guid tenantId) { @@ -771,7 +773,7 @@ namespace Marco.Pms.Services.Service { notificationFirebase = new Notification { - Title = $"New Task Updated - {project?.ProjectName}", + Title = $"Task Updated - {project?.ProjectName}", Body = $"{name} updated tasks {activityName} at {location}." }; } @@ -1130,6 +1132,316 @@ namespace Marco.Pms.Services.Service } } + public async Task SendDeleteTaskMeaasgeAsync(Guid workItemId, string name, 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.ActivityMaster) + .Include(wi => wi.WorkArea) + .ThenInclude(wa => wa!.Floor) + .ThenInclude(f => f!.Building) + .FirstOrDefaultAsync(wi => workItemId == wi.Id && + wi.TenantId == tenantId && + wi.ActivityMaster != null && + 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(); + }); + var viewProjectInfraRoleTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.RolePermissionMappings + .Where(rp => rp.FeaturePermissionId == PermissionsMaster.ViewProjectInfra) + .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(); + }); + + await Task.WhenAll(projectTask, viewTaskRoleTask, manageProjectsRoleTask, viewProjectInfraRoleTask); + + var viewTaskRoleIds = viewTaskRoleTask.Result; + var manageProjectsRoleIds = manageProjectsRoleTask.Result; + var viewProjectInfraRoleIds = viewProjectInfraRoleTask.Result; + var project = projectTask.Result; + + var activityName = workItem.ActivityMaster!.ActivityName; + + var buildingName = workItem.WorkArea.Floor.Building.Name; + var FloorName = workItem.WorkArea.Floor.FloorName; + var AreaName = workItem.WorkArea.AreaName; + + var location = $"{buildingName} > {FloorName} > {AreaName}"; + + var buildingId = workItem.WorkArea.Floor.Building.Id; + var floorId = workItem.WorkArea.Floor.Id; + var areaId = workItem.WorkArea.Id; + + List projectAssignedEmployeeIds = project?.EmployeeIds ?? new List(); + + var employeeIds = await _context.EmployeeRoleMappings + .Where(er => + (projectAssignedEmployeeIds.Contains(er.EmployeeId) || manageProjectsRoleIds.Contains(er.RoleId)) && + (viewTaskRoleIds.Contains(er.RoleId) || viewProjectInfraRoleIds.Contains(er.RoleId))) + .Select(er => er.EmployeeId) + .ToListAsync(); + + Notification notificationFirebase = new Notification + { + Title = $"Task Deleted - {project?.ProjectName}", + Body = $"{name} deleted tasks {activityName} at {location}." + }; + + + var data = new Dictionary() + { + { "Keyword", "Task_Modified" }, + { "ProjectId", projectId.ToString() }, + { "BuildingId", buildingId.ToString() }, + { "FloorId", floorId.ToString() }, + { "AreaId", areaId.ToString() }, + }; + + + // List of device registration tokens to send the message to + var registrationTokensForNotification = await _context.FCMTokenMappings.Where(ft => employeeIds.Contains(ft.EmployeeId)).Select(ft => ft.FcmToken).ToListAsync(); + + await SendMessageToMultipleDevicesWithDataAsync(registrationTokensForNotification, notificationFirebase, data); + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception occured while get data for sending notification"); + } + } + + //Project Allocation + public async Task SendProjectAllocationMessageAsync(List projectAllocations, string name, Guid tenantId) + { + using var scope = _serviceScopeFactory.CreateScope(); + var _logger = scope.ServiceProvider.GetRequiredService(); + + try + { + await using var _context = await _dbContextFactory.CreateDbContextAsync(); + + foreach (var projectAllocation in projectAllocations) + { + + var projectId = projectAllocation.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 manageTeamRoleTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.RolePermissionMappings + .Where(rp => rp.FeaturePermissionId == PermissionsMaster.ManageTeam) + .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(); + }); + + await Task.WhenAll(projectTask, manageTeamRoleTask, manageProjectsRoleTask); + + var manageTeamRoleIds = manageTeamRoleTask.Result; + var manageProjectsRoleIds = manageProjectsRoleTask.Result; + var project = projectTask.Result; + + List projectAssignedEmployeeIds = project?.EmployeeIds ?? new List(); + + var employeeIds = await _context.EmployeeRoleMappings + .Where(er => + (projectAssignedEmployeeIds.Contains(er.EmployeeId) || manageProjectsRoleIds.Contains(er.RoleId)) && manageTeamRoleIds.Contains(er.RoleId)) + .Select(er => er.EmployeeId) + .ToListAsync(); + + Notification notificationFirebase; + if (projectAllocation.IsActive) + { + notificationFirebase = new Notification + { + Title = $"Assigned to Project - {project?.ProjectName}", + Body = $"You have been assigned to the project \"{project?.ProjectName}\"." + }; + } + else + { + notificationFirebase = new Notification + { + Title = $"Removed from Project - {project?.ProjectName}", + Body = $"{name} has removed you from the project \"{project?.ProjectName}\"." + }; + } + + var data = new Dictionary() + { + { "Keyword", "Team_Modefied" }, + { "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 => projectAllocation.EmployeeId == ft.EmployeeId).Select(ft => ft.FcmToken).ToListAsync(); + + 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"); + } + } + + //Project Management + public async Task SendModifyProjectMessageAsync(Project project, string name, bool IsExist, Guid tenantId) + { + using var scope = _serviceScopeFactory.CreateScope(); + var _logger = scope.ServiceProvider.GetRequiredService(); + + try + { + await using var _context = await _dbContextFactory.CreateDbContextAsync(); + + var projectTask = Task.Run(async () => + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + return await dbContext.ProjectAllocations + .Include(pa => pa.Project) + .Where(pa => pa.ProjectId == project.Id && 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 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(); + }); + + await Task.WhenAll(projectTask, manageProjectsRoleTask); + + var manageProjectsRoleIds = manageProjectsRoleTask.Result; + var projectVM = projectTask.Result; + + List projectAssignedEmployeeIds = projectVM?.EmployeeIds ?? new List(); + + var employeeIds = await _context.EmployeeRoleMappings + .Where(er => + (projectAssignedEmployeeIds.Contains(er.EmployeeId) || manageProjectsRoleIds.Contains(er.RoleId))) + .Select(er => er.EmployeeId) + .ToListAsync(); + + Notification notificationFirebase; + if (IsExist) + { + notificationFirebase = new Notification + { + Title = $"Project Updated - {project.Name}", + Body = $"{name} updated the project \"{project.Name}\"." + }; + } + else + { + notificationFirebase = new Notification + { + Title = $"Project Created - {project.Name}", + Body = $"A new project \"{project.Name}\" has been created by {name}." + }; + } + + var data = new Dictionary() + { + { "Keyword", "Project_Modefied" }, + { "ProjectId", project.Id.ToString() } + }; + + // List of device registration tokens to send the message to + var registrationTokensForNotification = await _context.FCMTokenMappings.Where(ft => employeeIds.Contains(ft.EmployeeId)).Select(ft => ft.FcmToken).ToListAsync(); + + await SendMessageToMultipleDevicesWithDataAsync(registrationTokensForNotification, notificationFirebase, data); + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception occured while get data for sending notification"); + } + } #region =================================================================== Helper Functions =================================================================== @@ -1140,6 +1452,7 @@ namespace Marco.Pms.Services.Service var _logger = scope.ServiceProvider.GetRequiredService(); try { + registrationTokens = registrationTokens.Distinct().ToList(); var message = new MulticastMessage() { Tokens = registrationTokens, @@ -1191,6 +1504,7 @@ namespace Marco.Pms.Services.Service var _logger = scope.ServiceProvider.GetRequiredService(); try { + registrationTokens = registrationTokens.Distinct().ToList(); var message = new MulticastMessage() { Tokens = registrationTokens, @@ -1241,6 +1555,7 @@ namespace Marco.Pms.Services.Service var _logger = scope.ServiceProvider.GetRequiredService(); try { + registrationTokens = registrationTokens.Distinct().ToList(); var message = new MulticastMessage() { Tokens = registrationTokens, diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs index 41e3726..cc98660 100644 --- a/Marco.Pms.Services/Service/ProjectServices.cs +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -349,6 +349,9 @@ namespace Marco.Pms.Services.Service public async Task> CreateProjectAsync(CreateProjectDto projectDto, Guid tenantId, Employee loggedInEmployee) { + using var scope = _serviceScopeFactory.CreateScope(); + var _firebase = scope.ServiceProvider.GetRequiredService(); + // 1. Prepare data without I/O var loggedInUserId = loggedInEmployee.Id; var project = _mapper.Map(projectDto); @@ -385,6 +388,18 @@ namespace Marco.Pms.Services.Service _logger.LogError(ex, "Project {ProjectId} was created, but a post-creation side-effect (caching/notification) failed. ", project.Id); } + _ = 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.SendModifyProjectMessageAsync(project, name, false, tenantId); + + }); + // 4. Return a success response to the user as soon as the critical data is saved. return ApiResponse.SuccessResponse(_mapper.Map(project), "Project created successfully.", 200); } @@ -398,6 +413,9 @@ namespace Marco.Pms.Services.Service /// An ApiResponse confirming the update or an appropriate error. public async Task> UpdateProjectAsync(Guid id, UpdateProjectDto updateProjectDto, Guid tenantId, Employee loggedInEmployee) { + using var scope = _serviceScopeFactory.CreateScope(); + var _firebase = scope.ServiceProvider.GetRequiredService(); + try { // --- Step 1: Fetch the Existing Entity from the Database --- @@ -449,6 +467,18 @@ namespace Marco.Pms.Services.Service // 4a. Update Cache await UpdateCacheInBackground(existingProject); + _ = 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.SendModifyProjectMessageAsync(existingProject, name, true, tenantId); + + }); + // --- Step 5: Return Success Response Immediately --- // The client gets a fast response without waiting for caching or SignalR. return ApiResponse.SuccessResponse(projectDto, "Project updated successfully.", 200); @@ -618,6 +648,9 @@ namespace Marco.Pms.Services.Service /// An ApiResponse containing the list of processed allocations. public async Task>> ManageAllocationAsync(List allocationsDto, Guid tenantId, Employee loggedInEmployee) { + using var scope = _serviceScopeFactory.CreateScope(); + var _firebase = scope.ServiceProvider.GetRequiredService(); + // --- Step 1: Input Validation --- if (allocationsDto == null || !allocationsDto.Any()) { @@ -712,6 +745,17 @@ namespace Marco.Pms.Services.Service // --- Step 5: Map results and return success --- var resultVm = _mapper.Map>(processedAllocations); + _ = 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.SendProjectAllocationMessageAsync(processedAllocations, name, tenantId); + + }); return ApiResponse>.SuccessResponse(resultVm, "Allocations managed successfully.", 200); } @@ -799,6 +843,9 @@ namespace Marco.Pms.Services.Service /// An ApiResponse containing the list of processed allocations. public async Task>> AssigneProjectsToEmployeeAsync(List allocationsDto, Guid employeeId, Guid tenantId, Employee loggedInEmployee) { + using var scope = _serviceScopeFactory.CreateScope(); + var _firebase = scope.ServiceProvider.GetRequiredService(); + // --- Step 1: Input Validation --- if (allocationsDto == null || !allocationsDto.Any() || employeeId == Guid.Empty) { @@ -892,6 +939,17 @@ namespace Marco.Pms.Services.Service // --- Step 6: Map results using AutoMapper and return success --- var resultVm = _mapper.Map>(processedAllocations); + _ = 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.SendProjectAllocationMessageAsync(processedAllocations, name, tenantId); + + }); return ApiResponse>.SuccessResponse(resultVm, "Assignments managed successfully.", 200); } @@ -1417,6 +1475,9 @@ namespace Marco.Pms.Services.Service public async Task DeleteProjectTaskAsync(Guid id, Guid tenantId, Employee loggedInEmployee) { + using var scope = _serviceScopeFactory.CreateScope(); + var _firebase = scope.ServiceProvider.GetRequiredService(); + // 1. Fetch the task and its parent data in a single query. // This is still a major optimization, avoiding a separate query for the floor/building. WorkItem? task = await _context.WorkItems @@ -1489,7 +1550,17 @@ namespace Marco.Pms.Services.Service cacheTasks.Add(_cache.DeleteProjectByIdAsync(building.ProjectId)); } await Task.WhenAll(cacheTasks); + _ = 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.SendDeleteTaskMeaasgeAsync(task.Id, name, tenantId); + + }); // 7. Return the final success response. return new ServiceResponse { diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IFirebaseService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IFirebaseService.cs index 035005c..cedcb10 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IFirebaseService.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IFirebaseService.cs @@ -1,4 +1,5 @@ using Marco.Pms.Model.Dtos.Attendance; +using Marco.Pms.Model.Projects; namespace Marco.Pms.Services.Service.ServiceInterfaces { @@ -14,5 +15,9 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task SendModifyWorkAreaMeaasgeAsync(Guid workAreaId, string name, bool IsExist, Guid tenantId); Task SendModifyFloorMeaasgeAsync(Guid floorId, string name, bool IsExist, Guid tenantId); Task SendModifyBuildingMeaasgeAsync(Guid buildingId, string name, bool IsExist, Guid tenantId); + Task SendDeleteTaskMeaasgeAsync(Guid workItemId, string name, Guid tenantId); + + Task SendProjectAllocationMessageAsync(List projectAllocations, string name, Guid tenantId); + Task SendModifyProjectMessageAsync(Project project, string name, bool IsExist, Guid tenantId); } }