From da46d45601d29c2f878eec83461fc3fe5cccf651 Mon Sep 17 00:00:00 2001 From: mahan Date: Sun, 14 Dec 2025 12:13:23 +0330 Subject: [PATCH 1/7] feat: implement SignalR notifications for project status changes --- .../TaskSectionStatusChangedHandler.cs | 23 ++++++++++++++ .../Interfaces/IBoardNotificationPublisher.cs | 9 ++++++ .../Interfaces/IBoardNotificationService.cs | 6 ---- .../ProjectAgg/Entities/TaskSection.cs | 2 +- .../ProjectAgg/Events/ProjectEvents.cs | 3 +- .../Hubs/ProgramManager/ProjectBoardHub.cs | 13 ++++++++ .../SignalRBoardNotificationPublisher.cs | 30 +++++++++++++++++++ ServiceHost/Program.cs | 2 ++ 8 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 ProgramManager/src/Application/GozareshgirProgramManager.Application/DomainEventHandlers/ProjectSection/TaskSectionStatusChangedHandler.cs create mode 100644 ProgramManager/src/Application/GozareshgirProgramManager.Application/Interfaces/IBoardNotificationPublisher.cs delete mode 100644 ProgramManager/src/Application/GozareshgirProgramManager.Application/Interfaces/IBoardNotificationService.cs create mode 100644 ServiceHost/Hubs/ProgramManager/ProjectBoardHub.cs create mode 100644 ServiceHost/Notifications/ProgramManager/SignalRBoardNotificationPublisher.cs diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/DomainEventHandlers/ProjectSection/TaskSectionStatusChangedHandler.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/DomainEventHandlers/ProjectSection/TaskSectionStatusChangedHandler.cs new file mode 100644 index 00000000..cca24761 --- /dev/null +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/DomainEventHandlers/ProjectSection/TaskSectionStatusChangedHandler.cs @@ -0,0 +1,23 @@ +using GozareshgirProgramManager.Application._Common.Models; +using GozareshgirProgramManager.Application.Interfaces; +using GozareshgirProgramManager.Domain.ProjectAgg.Events; +using MediatR; + +namespace GozareshgirProgramManager.Application.DomainEventHandlers.ProjectSection; + +public class TaskSectionStatusChangedHandler:INotificationHandler> +{ + private readonly IBoardNotificationPublisher _boardNotificationPublisher; + + public TaskSectionStatusChangedHandler(IBoardNotificationPublisher boardNotificationPublisher) + { + _boardNotificationPublisher = boardNotificationPublisher; + } + + public Task Handle(DomainEventNotification notification, CancellationToken cancellationToken) + { + var domainEvent = notification.DomainEvent; + _boardNotificationPublisher.SendProjectStatusChanged(domainEvent.UserId,domainEvent.OldStatus,domainEvent.NewStatus,domainEvent.SectionId); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Interfaces/IBoardNotificationPublisher.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Interfaces/IBoardNotificationPublisher.cs new file mode 100644 index 00000000..1f4d5f1b --- /dev/null +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Interfaces/IBoardNotificationPublisher.cs @@ -0,0 +1,9 @@ +using GozareshgirProgramManager.Domain.ProjectAgg.Enums; + +namespace GozareshgirProgramManager.Application.Interfaces; + +public interface IBoardNotificationPublisher +{ + Task SendProjectStatusChanged(long userId, TaskSectionStatus oldStatus, + TaskSectionStatus newStatus, Guid sectionId); +} \ No newline at end of file diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Interfaces/IBoardNotificationService.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Interfaces/IBoardNotificationService.cs deleted file mode 100644 index 959e7fbf..00000000 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Interfaces/IBoardNotificationService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace GozareshgirProgramManager.Application.Interfaces; - -public interface IBoardNotificationService -{ - Task SendProjectAssignedAsync(); -} \ No newline at end of file diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/TaskSection.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/TaskSection.cs index e478478d..b2a6fd91 100644 --- a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/TaskSection.cs +++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/TaskSection.cs @@ -159,7 +159,7 @@ public class TaskSection : EntityBase { var oldStatus = Status; Status = status; - AddDomainEvent(new TaskSectionStatusChangedEvent(Id, oldStatus, status)); + AddDomainEvent(new TaskSectionStatusChangedEvent(Id, oldStatus, status,CurrentAssignedUserId)); } public TimeSpan GetTotalTimeSpent() diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Events/ProjectEvents.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Events/ProjectEvents.cs index e823967e..fb6ca424 100644 --- a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Events/ProjectEvents.cs +++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Events/ProjectEvents.cs @@ -104,7 +104,8 @@ public record TaskSectionRemovedEvent(Guid TaskId, Guid SectionId) : IDomainEven } // TaskSection Events -public record TaskSectionStatusChangedEvent(Guid SectionId, TaskSectionStatus OldStatus, TaskSectionStatus NewStatus) : IDomainEvent +public record TaskSectionStatusChangedEvent(Guid SectionId, TaskSectionStatus OldStatus, + TaskSectionStatus NewStatus,long UserId) : IDomainEvent { public DateTime OccurredOn { get; init; } = DateTime.UtcNow; } diff --git a/ServiceHost/Hubs/ProgramManager/ProjectBoardHub.cs b/ServiceHost/Hubs/ProgramManager/ProjectBoardHub.cs new file mode 100644 index 00000000..7acec847 --- /dev/null +++ b/ServiceHost/Hubs/ProgramManager/ProjectBoardHub.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.SignalR; + +namespace ServiceHost.Hubs.ProgramManager; + +public class ProjectBoardHub:Hub +{ + public override async Task OnConnectedAsync() + { + Console.WriteLine($"{Context.ConnectionId} connected"); + await Clients.All.SendAsync("ReceiveGreeting","A new user has connected to the Project Board Hub!"); + await base.OnConnectedAsync(); + } +} \ No newline at end of file diff --git a/ServiceHost/Notifications/ProgramManager/SignalRBoardNotificationPublisher.cs b/ServiceHost/Notifications/ProgramManager/SignalRBoardNotificationPublisher.cs new file mode 100644 index 00000000..5b280f1d --- /dev/null +++ b/ServiceHost/Notifications/ProgramManager/SignalRBoardNotificationPublisher.cs @@ -0,0 +1,30 @@ +using GozareshgirProgramManager.Application.Interfaces; +using GozareshgirProgramManager.Domain.ProjectAgg.Enums; +using Microsoft.AspNetCore.SignalR; +using ServiceHost.Hubs.ProgramManager; + +namespace ServiceHost.Notifications.ProgramManager; + +public class SignalRBoardNotificationPublisher:IBoardNotificationPublisher +{ + private readonly IHubContext _hubContext; + + public SignalRBoardNotificationPublisher(IHubContext hubContext) + { + _hubContext = hubContext; + } + + public Task SendProjectStatusChanged(long userId, TaskSectionStatus oldStatus, + TaskSectionStatus newStatus, Guid sectionId) + { + var payload = new + { + UserId = userId, + OldStatus = oldStatus, + NewStatus = newStatus, + SectionId = sectionId + }; + _hubContext.Clients.User(userId.ToString()).SendAsync("ReceiveProjectStatusChanged",payload); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/ServiceHost/Program.cs b/ServiceHost/Program.cs index 40ca19b2..4c4b58d8 100644 --- a/ServiceHost/Program.cs +++ b/ServiceHost/Program.cs @@ -31,6 +31,7 @@ using GozareshgirProgramManager.Application.Modules.Users.Commands.CreateUser; using GozareshgirProgramManager.Infrastructure; using GozareshgirProgramManager.Infrastructure.Persistence.Seed; using Microsoft.OpenApi; +using ServiceHost.Hubs.ProgramManager; var builder = WebApplication.CreateBuilder(args); @@ -451,6 +452,7 @@ app.MapHub("/trackingHolidayHub"); app.MapHub("/trackingCheckoutHub"); // app.MapHub("/trackingFaceEmbeddingHub"); app.MapHub("/trackingSendSmsHub"); +app.MapHub("/pm/board"); app.MapRazorPages(); app.MapControllers(); From 83a7bbf5f39c527d32279014ad4335bb6fa297a0 Mon Sep 17 00:00:00 2001 From: mahan Date: Sun, 14 Dec 2025 13:50:24 +0330 Subject: [PATCH 2/7] feat: register SignalR notification publisher for board updates --- ServiceHost/Program.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ServiceHost/Program.cs b/ServiceHost/Program.cs index 4c4b58d8..70db4c48 100644 --- a/ServiceHost/Program.cs +++ b/ServiceHost/Program.cs @@ -27,11 +27,13 @@ using Swashbuckle.AspNetCore.SwaggerUI; using AccountManagement.Domain.InternalApiCaller; using FluentValidation; using GozareshgirProgramManager.Application._Bootstrapper; +using GozareshgirProgramManager.Application.Interfaces; using GozareshgirProgramManager.Application.Modules.Users.Commands.CreateUser; using GozareshgirProgramManager.Infrastructure; using GozareshgirProgramManager.Infrastructure.Persistence.Seed; using Microsoft.OpenApi; using ServiceHost.Hubs.ProgramManager; +using ServiceHost.Notifications.ProgramManager; var builder = WebApplication.CreateBuilder(args); @@ -55,6 +57,7 @@ builder.Services.AddProgramManagerApplication(); builder.Services.AddProgramManagerInfrastructure(builder.Configuration); builder.Services.AddValidatorsFromAssemblyContaining(); builder.Services.AddScoped(); +builder.Services.AddScoped(); #region MongoDb var mongoConnectionSection = builder.Configuration.GetSection("MongoDb"); From bca1e66f0f94eee591c0de2e7df959bc654b5e1c Mon Sep 17 00:00:00 2001 From: mahan Date: Sun, 14 Dec 2025 14:21:45 +0330 Subject: [PATCH 3/7] feat: update ProjectBoardHub endpoint to include API prefix --- ServiceHost/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ServiceHost/Program.cs b/ServiceHost/Program.cs index 70db4c48..9c583c1c 100644 --- a/ServiceHost/Program.cs +++ b/ServiceHost/Program.cs @@ -455,7 +455,7 @@ app.MapHub("/trackingHolidayHub"); app.MapHub("/trackingCheckoutHub"); // app.MapHub("/trackingFaceEmbeddingHub"); app.MapHub("/trackingSendSmsHub"); -app.MapHub("/pm/board"); +app.MapHub("api/pm/board"); app.MapRazorPages(); app.MapControllers(); From a7d3b1e96f0455b5dc5ffd761c5d1758ed958459 Mon Sep 17 00:00:00 2001 From: mahan Date: Mon, 15 Dec 2025 10:14:32 +0330 Subject: [PATCH 4/7] feat: update SignalR group handling for project status notifications --- 0_Framework/Application/AuthHelper.cs | 2 +- ServiceHost/Hubs/ProgramManager/ProjectBoardHub.cs | 12 ++++++++++-- .../SignalRBoardNotificationPublisher.cs | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/0_Framework/Application/AuthHelper.cs b/0_Framework/Application/AuthHelper.cs index 12f820f8..41e6efa2 100644 --- a/0_Framework/Application/AuthHelper.cs +++ b/0_Framework/Application/AuthHelper.cs @@ -199,7 +199,7 @@ public class AuthHelper : IAuthHelper new("WorkshopSlug",slug), new("WorkshopId", account.WorkshopId.ToString()), new("WorkshopName",account.WorkshopName??""), - new("pm.userId", account.PmUserId?.ToString() ?? "0"), + new("pm.userId", account.PmUserId.ToString()), }; diff --git a/ServiceHost/Hubs/ProgramManager/ProjectBoardHub.cs b/ServiceHost/Hubs/ProgramManager/ProjectBoardHub.cs index 7acec847..5c262095 100644 --- a/ServiceHost/Hubs/ProgramManager/ProjectBoardHub.cs +++ b/ServiceHost/Hubs/ProgramManager/ProjectBoardHub.cs @@ -6,8 +6,16 @@ public class ProjectBoardHub:Hub { public override async Task OnConnectedAsync() { - Console.WriteLine($"{Context.ConnectionId} connected"); - await Clients.All.SendAsync("ReceiveGreeting","A new user has connected to the Project Board Hub!"); + var user = Context.User?.FindFirst("pm.userId")?.Value; + + if (user != null && user !="0") + { + await Groups.AddToGroupAsync( + Context.ConnectionId, + $"pm.user-{user}" + ); + } + await base.OnConnectedAsync(); } } \ No newline at end of file diff --git a/ServiceHost/Notifications/ProgramManager/SignalRBoardNotificationPublisher.cs b/ServiceHost/Notifications/ProgramManager/SignalRBoardNotificationPublisher.cs index 5b280f1d..130d8f09 100644 --- a/ServiceHost/Notifications/ProgramManager/SignalRBoardNotificationPublisher.cs +++ b/ServiceHost/Notifications/ProgramManager/SignalRBoardNotificationPublisher.cs @@ -24,7 +24,7 @@ public class SignalRBoardNotificationPublisher:IBoardNotificationPublisher NewStatus = newStatus, SectionId = sectionId }; - _hubContext.Clients.User(userId.ToString()).SendAsync("ReceiveProjectStatusChanged",payload); + _hubContext.Clients.Group($"pm.user-{userId}").SendAsync("ReceiveProjectStatusChanged",payload); return Task.CompletedTask; } } \ No newline at end of file From 2a31b27f9b6877c691dff83f66ae86efbe3a2b71 Mon Sep 17 00:00:00 2001 From: mahan Date: Mon, 15 Dec 2025 10:43:17 +0330 Subject: [PATCH 5/7] feat: simplify StartWork and StopWork methods by removing userId parameter --- .../ChangeStatusSectionCommandHandler.cs | 14 +++++++------- .../ProjectAgg/Entities/TaskSection.cs | 16 +++------------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ChangeStatusSection/ChangeStatusSectionCommandHandler.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ChangeStatusSection/ChangeStatusSectionCommandHandler.cs index 9e37cdc8..4214ec2d 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ChangeStatusSection/ChangeStatusSectionCommandHandler.cs +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/Modules/Projects/Commands/ChangeStatusSection/ChangeStatusSectionCommandHandler.cs @@ -45,12 +45,12 @@ public class ChangeStatusSectionCommandHandler : IBaseCommandHandler> { - { TaskSectionStatus.ReadyToStart, new List { TaskSectionStatus.InProgress } }, - { TaskSectionStatus.InProgress, new List { TaskSectionStatus.Incomplete, TaskSectionStatus.Completed } }, - { TaskSectionStatus.Incomplete, new List { TaskSectionStatus.InProgress, TaskSectionStatus.Completed } }, - { TaskSectionStatus.Completed, new List { TaskSectionStatus.InProgress, TaskSectionStatus.Incomplete } }, // Can return to InProgress or Incomplete - { TaskSectionStatus.NotAssigned, new List { TaskSectionStatus.InProgress, TaskSectionStatus.ReadyToStart } } + { TaskSectionStatus.ReadyToStart, [TaskSectionStatus.InProgress] }, + { TaskSectionStatus.InProgress, [TaskSectionStatus.Incomplete, TaskSectionStatus.Completed] }, + { TaskSectionStatus.Incomplete, [TaskSectionStatus.InProgress, TaskSectionStatus.Completed] }, + { TaskSectionStatus.Completed, [TaskSectionStatus.InProgress, TaskSectionStatus.Incomplete] }, // Can return to InProgress or Incomplete + { TaskSectionStatus.NotAssigned, [TaskSectionStatus.InProgress, TaskSectionStatus.ReadyToStart] } }; if (!validTransitions.TryGetValue(currentStatus, out var allowedTargets)) diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/TaskSection.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/TaskSection.cs index b2a6fd91..c663ed4b 100644 --- a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/TaskSection.cs +++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Entities/TaskSection.cs @@ -104,13 +104,8 @@ public class TaskSection : EntityBase UpdateStatus(TaskSectionStatus.NotAssigned); } - public void StartWork(long userId, string? notes = null) + public void StartWork(string? notes = null) { - if (CurrentAssignedUserId != userId) - { - throw new BadRequestException("کاربر مجاز به شروع این بخش نیست"); - } - // if (Status == TaskSectionStatus.Completed) // { // throw new BadRequestException("این بخش قبلاً تکمیل شده است"); @@ -121,14 +116,14 @@ public class TaskSection : EntityBase throw new BadRequestException("یک فعالیت در حال انجام وجود دارد"); } - var activity = new TaskSectionActivity(Id, userId, notes); + var activity = new TaskSectionActivity(Id, CurrentAssignedUserId, notes); _activities.Add(activity); UpdateStatus(TaskSectionStatus.InProgress); } - public void StopWork(long userId, TaskSectionStatus taskSectionStatus, string? endNotes = null) + public void StopWork(TaskSectionStatus taskSectionStatus, string? endNotes = null) { var activeActivity = _activities.FirstOrDefault(a => a.IsActive); if (activeActivity == null) @@ -136,11 +131,6 @@ public class TaskSection : EntityBase throw new BadRequestException("هیچ فعالیت فعالی یافت نشد"); } - if (activeActivity.UserId != userId) - { - throw new BadRequestException("کاربر مجاز به توقف این فعالیت نیست"); - } - UpdateStatus(taskSectionStatus); activeActivity.StopWork(endNotes); } From 000af89fd7a9a3a48bfba6f6e77c829ff3f0f990 Mon Sep 17 00:00:00 2001 From: mahan Date: Mon, 15 Dec 2025 13:49:41 +0330 Subject: [PATCH 6/7] feat: add permission checking and task assignment handling in ProjectBoardHub --- 0_Framework/Application/AuthHelper.cs | 5 ++ 0_Framework/Application/IAuthHelper.cs | 1 + DadmehrGostar.sln | 3 + .../Constants/ProgramManagerPermissionCode.cs | 26 +++++++ .../Repositories/ITaskSectionRepository.cs | 2 + .../Hubs/ProgramManager/ProjectBoardHub.cs | 67 ++++++++++++++++--- .../SignalRBoardNotificationPublisher.cs | 15 ++++- ServiceHost/Properties/launchSettings.json | 2 +- 8 files changed, 109 insertions(+), 12 deletions(-) create mode 100644 ProgramManager/src/Application/GozareshgirProgramManager.Application/_Common/Constants/ProgramManagerPermissionCode.cs diff --git a/0_Framework/Application/AuthHelper.cs b/0_Framework/Application/AuthHelper.cs index 41e6efa2..2af2188f 100644 --- a/0_Framework/Application/AuthHelper.cs +++ b/0_Framework/Application/AuthHelper.cs @@ -56,6 +56,11 @@ public class AuthHelper : IAuthHelper return Tools.DeserializeFromBsonList(permissions); //Mahan } + public bool HasPermission(int permission) + { + return GetPermissions().Any(x => x == permission); + } + public long CurrentAccountId() { return IsAuthenticated() diff --git a/0_Framework/Application/IAuthHelper.cs b/0_Framework/Application/IAuthHelper.cs index ab9cf572..d43ac069 100644 --- a/0_Framework/Application/IAuthHelper.cs +++ b/0_Framework/Application/IAuthHelper.cs @@ -12,6 +12,7 @@ public interface IAuthHelper string CurrentAccountRole(); AuthViewModel CurrentAccountInfo(); List GetPermissions(); + bool HasPermission(int permission); long CurrentAccountId(); string CurrentAccountMobile(); diff --git a/DadmehrGostar.sln b/DadmehrGostar.sln index f31d5bcb..342cf0ef 100644 --- a/DadmehrGostar.sln +++ b/DadmehrGostar.sln @@ -106,6 +106,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GozareshgirProgramManager.I EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared.Contracts", "Shared.Contracts\Shared.Contracts.csproj", "{08B234B6-783B-44E9-9961-4F97EAD16308}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{F61F77F5-9B14-49CC-870B-1C61D2636586}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -275,6 +277,7 @@ Global {B57EB542-C028-4A77-9386-9DFF1E60FDCB} = {9D85672B-D48E-40B5-9804-0CE220E0E64C} {D2B4F1D7-6336-4B30-910C-219F4119303F} = {D74D1E3B-3BE3-47EE-9914-785A8AD536E5} {408281FE-615F-4CBE-BD95-2E86F5ACC6C3} = {C0AE9368-D4E7-450B-9713-929D319DE690} + {08B234B6-783B-44E9-9961-4F97EAD16308} = {F61F77F5-9B14-49CC-870B-1C61D2636586} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E6CFB3A7-A7C8-4E82-8F06-F750408F0BA9} diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/_Common/Constants/ProgramManagerPermissionCode.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/_Common/Constants/ProgramManagerPermissionCode.cs new file mode 100644 index 00000000..ea171972 --- /dev/null +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/_Common/Constants/ProgramManagerPermissionCode.cs @@ -0,0 +1,26 @@ +namespace GozareshgirProgramManager.Application._Common.Constants; + +public static class ProgramManagerPermissionCode +{ + public const int Code = 99; + + /// + ///بخش اجرا + /// + public static class Board + { + public const int Code = 991; +/// +/// تب همه +/// + public static class All + { + public const int Code = 99101; + /// + /// دیدن همه تسک ها + /// + public const int ViewAll = 9910101; + } + + } +} diff --git a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Repositories/ITaskSectionRepository.cs b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Repositories/ITaskSectionRepository.cs index bedf59bf..d9c11d75 100644 --- a/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Repositories/ITaskSectionRepository.cs +++ b/ProgramManager/src/Domain/GozareshgirProgramManager.Domain/ProjectAgg/Repositories/ITaskSectionRepository.cs @@ -9,4 +9,6 @@ public interface ITaskSectionRepository: IRepository Task GetByIdWithFullDataAsync(Guid id, CancellationToken cancellationToken = default); + + Task> GetAssignedToUserAsync(long userId); } \ No newline at end of file diff --git a/ServiceHost/Hubs/ProgramManager/ProjectBoardHub.cs b/ServiceHost/Hubs/ProgramManager/ProjectBoardHub.cs index 5c262095..c7870a62 100644 --- a/ServiceHost/Hubs/ProgramManager/ProjectBoardHub.cs +++ b/ServiceHost/Hubs/ProgramManager/ProjectBoardHub.cs @@ -1,19 +1,70 @@ +using _0_Framework.Application; +using GozareshgirProgramManager.Application._Common.Constants; using Microsoft.AspNetCore.SignalR; +using GozareshgirProgramManager.Domain.ProjectAgg.Repositories; +using System.Collections.Generic; +using System.Linq; +using System; namespace ServiceHost.Hubs.ProgramManager; -public class ProjectBoardHub:Hub +public class ProjectBoardHub : Hub { + private readonly IAuthHelper _authHelper; + private readonly ITaskSectionRepository _taskSectionRepository; + + public ProjectBoardHub(IAuthHelper authHelper, ITaskSectionRepository taskSectionRepository) + { + _authHelper = authHelper; + _taskSectionRepository = taskSectionRepository; + } + public override async Task OnConnectedAsync() { - var user = Context.User?.FindFirst("pm.userId")?.Value; - - if (user != null && user !="0") + // Rule 4: Determine all group memberships server-side. + if (!_authHelper.IsAuthenticated()) { - await Groups.AddToGroupAsync( - Context.ConnectionId, - $"pm.user-{user}" - ); + await base.OnConnectedAsync(); + return; + } + + var connectionId = Context.ConnectionId; + + // Rule 2: Add to all permission-based groups based on user's claims. + var permissionGroups = _authHelper.GetPermissions() ?? new List(); + + // Rule 3: Add to task-specific groups for all tasks assigned to the user (by accountId claim). + var userId =Convert.ToInt32(Context.User?.FindFirst("pm.userId")?.Value); + + var taskGroups = new List(); + if (userId > 0) + { + var assignedTasks = await _taskSectionRepository.GetAssignedToUserAsync(userId); + if (assignedTasks is { Count: > 0 }) + { + taskGroups = assignedTasks + .Select(t => $"pm.task:{t.Id}") + .ToList(); + } + } + + // Build the full, de-duplicated set of groups to join. + var groupsToJoin = new HashSet(StringComparer.OrdinalIgnoreCase); + + // Permission-based groups + foreach (var perm in permissionGroups) + groupsToJoin.Add($"pm.perm:{perm}"); + + // Task-based groups + foreach (var tg in taskGroups) + groupsToJoin.Add(tg); + + // Rule 5: Avoid duplicate joins; join all needed groups concurrently. + if (groupsToJoin.Count > 0) + { + var joinTasks = groupsToJoin + .Select(group => Groups.AddToGroupAsync(connectionId, group)); + await Task.WhenAll(joinTasks); } await base.OnConnectedAsync(); diff --git a/ServiceHost/Notifications/ProgramManager/SignalRBoardNotificationPublisher.cs b/ServiceHost/Notifications/ProgramManager/SignalRBoardNotificationPublisher.cs index 130d8f09..87a0c539 100644 --- a/ServiceHost/Notifications/ProgramManager/SignalRBoardNotificationPublisher.cs +++ b/ServiceHost/Notifications/ProgramManager/SignalRBoardNotificationPublisher.cs @@ -1,3 +1,4 @@ +using GozareshgirProgramManager.Application._Common.Constants; using GozareshgirProgramManager.Application.Interfaces; using GozareshgirProgramManager.Domain.ProjectAgg.Enums; using Microsoft.AspNetCore.SignalR; @@ -19,12 +20,20 @@ public class SignalRBoardNotificationPublisher:IBoardNotificationPublisher { var payload = new { - UserId = userId, + UserId= userId, OldStatus = oldStatus, NewStatus = newStatus, SectionId = sectionId }; - _hubContext.Clients.Group($"pm.user-{userId}").SendAsync("ReceiveProjectStatusChanged",payload); - return Task.CompletedTask; + + // گروه task-specific (برای همه assigneeهای واقعی) + var taskGroup = $"pm.task:{sectionId}"; + + // گروه permission-based (مثلاً برای Admin ها) + var permissionGroup = $"pm.perm:{ProgramManagerPermissionCode.Board.All.ViewAll}"; + + // ارسال به هر دو گروه؛ SignalR خودش duplicate connection رو هندل می‌کنه + return _hubContext.Clients.Groups(taskGroup, permissionGroup) + .SendAsync("ReceiveProjectStatusChanged", payload); } } \ No newline at end of file diff --git a/ServiceHost/Properties/launchSettings.json b/ServiceHost/Properties/launchSettings.json index fd650415..d7381591 100644 --- a/ServiceHost/Properties/launchSettings.json +++ b/ServiceHost/Properties/launchSettings.json @@ -19,7 +19,7 @@ "sqlDebugging": true, "dotnetRunMessages": "true", "nativeDebugging": true, - "applicationUrl": "https://localhost:5004;http://localhost:5003", + "applicationUrl": "https://localhost:5004;http://localhost:5003;https://192.168.0.117:5006", "jsWebView2Debugging": false, "hotReloadEnabled": true }, From d9da2e97ab300415fb8039e22210fed7db6bb0dc Mon Sep 17 00:00:00 2001 From: mahan Date: Mon, 15 Dec 2025 14:06:07 +0330 Subject: [PATCH 7/7] feat: add method to retrieve task sections assigned to a user and enhance permission code structure --- .../Constants/ProgramManagerPermissionCode.cs | 46 +++++++++++++++++-- .../Repositories/TaskSectionRepository.cs | 7 +++ 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/ProgramManager/src/Application/GozareshgirProgramManager.Application/_Common/Constants/ProgramManagerPermissionCode.cs b/ProgramManager/src/Application/GozareshgirProgramManager.Application/_Common/Constants/ProgramManagerPermissionCode.cs index ea171972..f5578ea2 100644 --- a/ProgramManager/src/Application/GozareshgirProgramManager.Application/_Common/Constants/ProgramManagerPermissionCode.cs +++ b/ProgramManager/src/Application/GozareshgirProgramManager.Application/_Common/Constants/ProgramManagerPermissionCode.cs @@ -1,7 +1,10 @@ +using System.Reflection; + namespace GozareshgirProgramManager.Application._Common.Constants; public static class ProgramManagerPermissionCode { + public const int Code = 99; /// @@ -10,17 +13,50 @@ public static class ProgramManagerPermissionCode public static class Board { public const int Code = 991; -/// -/// تب همه -/// + + /// + /// تب همه + /// public static class All { public const int Code = 99101; + /// /// دیدن همه تسک ها /// public const int ViewAll = 9910101; } - } -} + + public static List GetAllCodes() + { + var result = new List(); + + void Collect(Type type) + { + // Collect const int fields directly declared on this type + var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly); + foreach (var f in fields) + { + if (f.FieldType == typeof(int) && f.IsLiteral && !f.IsInitOnly) + { + var raw = f.GetRawConstantValue(); + if (raw is int value) + { + result.Add(value); + } + } + } + + // Recurse into nested types + var nestedTypes = type.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic); + foreach (var nt in nestedTypes) + { + Collect(nt); + } + } + + Collect(typeof(ProgramManagerPermissionCode)); + return result; + } +} \ No newline at end of file diff --git a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/TaskSectionRepository.cs b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/TaskSectionRepository.cs index 80564915..41551be5 100644 --- a/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/TaskSectionRepository.cs +++ b/ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/Persistence/Repositories/TaskSectionRepository.cs @@ -28,4 +28,11 @@ public class TaskSectionRepository:RepositoryBase,ITaskSection .Include(x => x.AdditionalTimes) .FirstOrDefaultAsync(x => x.Id == id, cancellationToken); } + + public async Task> GetAssignedToUserAsync(long userId) + { + return await _context.TaskSections + .Where(x => x.CurrentAssignedUserId == userId) + .ToListAsync(); + } } \ No newline at end of file