Merge branch 'master' of https://github.com/samsyntax24/OriginalGozareshgir
This commit is contained in:
@@ -56,6 +56,11 @@ public class AuthHelper : IAuthHelper
|
||||
return Tools.DeserializeFromBsonList<int>(permissions); //Mahan
|
||||
}
|
||||
|
||||
public bool HasPermission(int permission)
|
||||
{
|
||||
return GetPermissions().Any(x => x == permission);
|
||||
}
|
||||
|
||||
public long CurrentAccountId()
|
||||
{
|
||||
return IsAuthenticated()
|
||||
@@ -199,7 +204,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()),
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ public interface IAuthHelper
|
||||
string CurrentAccountRole();
|
||||
AuthViewModel CurrentAccountInfo();
|
||||
List<int> GetPermissions();
|
||||
bool HasPermission(int permission);
|
||||
long CurrentAccountId();
|
||||
string CurrentAccountMobile();
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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<DomainEventNotification<TaskSectionStatusChangedEvent>>
|
||||
{
|
||||
private readonly IBoardNotificationPublisher _boardNotificationPublisher;
|
||||
|
||||
public TaskSectionStatusChangedHandler(IBoardNotificationPublisher boardNotificationPublisher)
|
||||
{
|
||||
_boardNotificationPublisher = boardNotificationPublisher;
|
||||
}
|
||||
|
||||
public Task Handle(DomainEventNotification<TaskSectionStatusChangedEvent> notification, CancellationToken cancellationToken)
|
||||
{
|
||||
var domainEvent = notification.DomainEvent;
|
||||
_boardNotificationPublisher.SendProjectStatusChanged(domainEvent.UserId,domainEvent.OldStatus,domainEvent.NewStatus,domainEvent.SectionId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace GozareshgirProgramManager.Application.Interfaces;
|
||||
|
||||
public interface IBoardNotificationService
|
||||
{
|
||||
Task SendProjectAssignedAsync();
|
||||
}
|
||||
@@ -45,12 +45,12 @@ public class ChangeStatusSectionCommandHandler : IBaseCommandHandler<ChangeStatu
|
||||
if (section.Status == TaskSectionStatus.InProgress)
|
||||
{
|
||||
// Coming FROM InProgress: Stop the active activity
|
||||
section.StopWork(currentUser, request.Status);
|
||||
section.StopWork(request.Status);
|
||||
}
|
||||
else if (request.Status == TaskSectionStatus.InProgress)
|
||||
{
|
||||
// Going TO InProgress: Start work and create activity
|
||||
section.StartWork(currentUser);
|
||||
section.StartWork();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -82,11 +82,11 @@ public class ChangeStatusSectionCommandHandler : IBaseCommandHandler<ChangeStatu
|
||||
// Valid transitions matrix
|
||||
var validTransitions = new Dictionary<TaskSectionStatus, List<TaskSectionStatus>>
|
||||
{
|
||||
{ TaskSectionStatus.ReadyToStart, new List<TaskSectionStatus> { TaskSectionStatus.InProgress } },
|
||||
{ TaskSectionStatus.InProgress, new List<TaskSectionStatus> { TaskSectionStatus.Incomplete, TaskSectionStatus.Completed } },
|
||||
{ TaskSectionStatus.Incomplete, new List<TaskSectionStatus> { TaskSectionStatus.InProgress, TaskSectionStatus.Completed } },
|
||||
{ TaskSectionStatus.Completed, new List<TaskSectionStatus> { TaskSectionStatus.InProgress, TaskSectionStatus.Incomplete } }, // Can return to InProgress or Incomplete
|
||||
{ TaskSectionStatus.NotAssigned, new List<TaskSectionStatus> { 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))
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace GozareshgirProgramManager.Application._Common.Constants;
|
||||
|
||||
public static class ProgramManagerPermissionCode
|
||||
{
|
||||
|
||||
public const int Code = 99;
|
||||
|
||||
/// <summary>
|
||||
///بخش اجرا
|
||||
/// </summary>
|
||||
public static class Board
|
||||
{
|
||||
public const int Code = 991;
|
||||
|
||||
/// <summary>
|
||||
/// تب همه
|
||||
/// </summary>
|
||||
public static class All
|
||||
{
|
||||
public const int Code = 99101;
|
||||
|
||||
/// <summary>
|
||||
/// دیدن همه تسک ها
|
||||
/// </summary>
|
||||
public const int ViewAll = 9910101;
|
||||
}
|
||||
}
|
||||
|
||||
public static List<int> GetAllCodes()
|
||||
{
|
||||
var result = new List<int>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -104,13 +104,8 @@ public class TaskSection : EntityBase<Guid>
|
||||
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<Guid>
|
||||
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<Guid>
|
||||
throw new BadRequestException("هیچ فعالیت فعالی یافت نشد");
|
||||
}
|
||||
|
||||
if (activeActivity.UserId != userId)
|
||||
{
|
||||
throw new BadRequestException("کاربر مجاز به توقف این فعالیت نیست");
|
||||
}
|
||||
|
||||
UpdateStatus(taskSectionStatus);
|
||||
activeActivity.StopWork(endNotes);
|
||||
}
|
||||
@@ -159,7 +149,7 @@ public class TaskSection : EntityBase<Guid>
|
||||
{
|
||||
var oldStatus = Status;
|
||||
Status = status;
|
||||
AddDomainEvent(new TaskSectionStatusChangedEvent(Id, oldStatus, status));
|
||||
AddDomainEvent(new TaskSectionStatusChangedEvent(Id, oldStatus, status,CurrentAssignedUserId));
|
||||
}
|
||||
|
||||
public TimeSpan GetTotalTimeSpent()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -9,4 +9,6 @@ public interface ITaskSectionRepository: IRepository<Guid,TaskSection>
|
||||
|
||||
|
||||
Task<TaskSection?> GetByIdWithFullDataAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<List<TaskSection>> GetAssignedToUserAsync(long userId);
|
||||
}
|
||||
@@ -28,4 +28,11 @@ public class TaskSectionRepository:RepositoryBase<Guid,TaskSection>,ITaskSection
|
||||
.Include(x => x.AdditionalTimes)
|
||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<TaskSection>> GetAssignedToUserAsync(long userId)
|
||||
{
|
||||
return await _context.TaskSections
|
||||
.Where(x => x.CurrentAssignedUserId == userId)
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
72
ServiceHost/Hubs/ProgramManager/ProjectBoardHub.cs
Normal file
72
ServiceHost/Hubs/ProgramManager/ProjectBoardHub.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
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
|
||||
{
|
||||
private readonly IAuthHelper _authHelper;
|
||||
private readonly ITaskSectionRepository _taskSectionRepository;
|
||||
|
||||
public ProjectBoardHub(IAuthHelper authHelper, ITaskSectionRepository taskSectionRepository)
|
||||
{
|
||||
_authHelper = authHelper;
|
||||
_taskSectionRepository = taskSectionRepository;
|
||||
}
|
||||
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
// Rule 4: Determine all group memberships server-side.
|
||||
if (!_authHelper.IsAuthenticated())
|
||||
{
|
||||
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<int>();
|
||||
|
||||
// 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<string>();
|
||||
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<string>(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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using GozareshgirProgramManager.Application._Common.Constants;
|
||||
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<ProjectBoardHub> _hubContext;
|
||||
|
||||
public SignalRBoardNotificationPublisher(IHubContext<ProjectBoardHub> 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
|
||||
};
|
||||
|
||||
// گروه 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);
|
||||
}
|
||||
}
|
||||
@@ -27,10 +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;
|
||||
using ServiceHost.Conventions;
|
||||
|
||||
|
||||
@@ -55,6 +58,7 @@ builder.Services.AddProgramManagerApplication();
|
||||
builder.Services.AddProgramManagerInfrastructure(builder.Configuration);
|
||||
builder.Services.AddValidatorsFromAssemblyContaining<CreateUserCommandValidators>();
|
||||
builder.Services.AddScoped<IDataSeeder, DataSeeder>();
|
||||
builder.Services.AddScoped<IBoardNotificationPublisher, SignalRBoardNotificationPublisher>();
|
||||
#region MongoDb
|
||||
|
||||
var mongoConnectionSection = builder.Configuration.GetSection("MongoDb");
|
||||
@@ -456,6 +460,7 @@ app.MapHub<HolidayApiHub>("/trackingHolidayHub");
|
||||
app.MapHub<CheckoutHub>("/trackingCheckoutHub");
|
||||
// app.MapHub<FaceEmbeddingHub>("/trackingFaceEmbeddingHub");
|
||||
app.MapHub<SendSmsHub>("/trackingSendSmsHub");
|
||||
app.MapHub<ProjectBoardHub>("api/pm/board");
|
||||
app.MapRazorPages();
|
||||
app.MapControllers();
|
||||
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user