diff --git a/src/FrontOffice.BFF.Application/NetworkMembershipCQ/Queries/GetSubordinateTree/GetSubordinateTreeQuery.cs b/src/FrontOffice.BFF.Application/NetworkMembershipCQ/Queries/GetSubordinateTree/GetSubordinateTreeQuery.cs
new file mode 100644
index 0000000..c24d3e3
--- /dev/null
+++ b/src/FrontOffice.BFF.Application/NetworkMembershipCQ/Queries/GetSubordinateTree/GetSubordinateTreeQuery.cs
@@ -0,0 +1,18 @@
+namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetSubordinateTree;
+
+///
+/// Query برای دریافت درخت شبکه یک زیرمجموعه
+/// با چک امنیتی که فقط زیرمجموعههای خود کاربر قابل مشاهده باشند
+///
+public record GetSubordinateTreeQuery : IRequest
+{
+ ///
+ /// شناسه کاربر زیرمجموعه که میخواهیم درختش را ببینیم
+ ///
+ public long TargetUserId { get; init; }
+
+ ///
+ /// حداکثر عمق درخت (پیشفرض: 3)
+ ///
+ public int MaxDepth { get; init; } = 3;
+}
diff --git a/src/FrontOffice.BFF.Application/NetworkMembershipCQ/Queries/GetSubordinateTree/GetSubordinateTreeQueryHandler.cs b/src/FrontOffice.BFF.Application/NetworkMembershipCQ/Queries/GetSubordinateTree/GetSubordinateTreeQueryHandler.cs
new file mode 100644
index 0000000..2bfa377
--- /dev/null
+++ b/src/FrontOffice.BFF.Application/NetworkMembershipCQ/Queries/GetSubordinateTree/GetSubordinateTreeQueryHandler.cs
@@ -0,0 +1,106 @@
+using CMSMicroservice.Protobuf.Protos.NetworkMembership;
+using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
+
+namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetSubordinateTree;
+
+///
+/// Handler برای دریافت درخت زیرمجموعه
+/// امنیت: کاربر باید احراز هویت شده باشد (توکن معتبر)
+/// CMS خودش چک میکند که آیا targetUserId در زیرمجموعههای کاربر توکن هست یا نه
+///
+public class GetSubordinateTreeQueryHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+ private readonly ICurrentUserService _currentUserService;
+
+ public GetSubordinateTreeQueryHandler(
+ IApplicationContractContext context,
+ ICurrentUserService currentUserService)
+ {
+ _context = context;
+ _currentUserService = currentUserService;
+ }
+
+ public async Task Handle(GetSubordinateTreeQuery request, CancellationToken cancellationToken)
+ {
+ var currentUserId = _currentUserService.UserId ?? throw new UnauthorizedAccessException("User not authenticated");
+ var targetUserId = request.TargetUserId;
+
+ // اگر کاربر درخت خودش را میخواهد
+ if (targetUserId == currentUserId || targetUserId == 0)
+ {
+ targetUserId = currentUserId;
+ }
+
+ var cmsRequest = new GetNetworkTreeRequest
+ {
+ UserId = targetUserId,
+ MaxDepth = Math.Clamp(request.MaxDepth, 1, 15)
+ };
+
+ var response = await _context.NetworkMemberships.GetNetworkTreeAsync(cmsRequest, cancellationToken: cancellationToken);
+ var rootNode = BuildTreeFromFlatList(response.Nodes.ToList(), targetUserId);
+
+ return new GetSubordinateTreeResponseDto
+ {
+ RootNode = rootNode,
+ TotalMembers = response.Nodes.Count,
+ CurrentDepth = CalculateDepth(rootNode),
+ CanGoBack = targetUserId != currentUserId,
+ ParentUserId = targetUserId != currentUserId ? currentUserId : null
+ };
+ }
+
+ ///
+ /// Build hierarchical tree from flat list
+ ///
+ private NetworkNodeDto? BuildTreeFromFlatList(List flatNodes, long rootUserId)
+ {
+ if (flatNodes == null || flatNodes.Count == 0)
+ return null;
+
+ var nodeDict = flatNodes.ToDictionary(n => n.UserId);
+ var rootCmsNode = flatNodes.FirstOrDefault(n => n.UserId == rootUserId);
+
+ if (rootCmsNode == null)
+ return null;
+
+ return BuildNodeRecursive(rootCmsNode, flatNodes, nodeDict, 0);
+ }
+
+ private NetworkNodeDto BuildNodeRecursive(
+ NetworkTreeNodeModel cmsNode,
+ List allNodes,
+ Dictionary nodeDict,
+ int level)
+ {
+ var leftChild = allNodes.FirstOrDefault(n =>
+ n.ParentId.HasValue && n.ParentId.Value == cmsNode.UserId && n.NetworkLeg == 0);
+ var rightChild = allNodes.FirstOrDefault(n =>
+ n.ParentId.HasValue && n.ParentId.Value == cmsNode.UserId && n.NetworkLeg == 1);
+
+ var position = level == 0 ? "Root" : (cmsNode.NetworkLeg == 0 ? "Left" : "Right");
+
+ return new NetworkNodeDto
+ {
+ UserId = cmsNode.UserId,
+ FullName = cmsNode.UserName ?? string.Empty,
+ Mobile = string.Empty,
+ Avatar = null,
+ Position = position,
+ Level = level,
+ IsActive = cmsNode.IsActive,
+ JoinedAt = cmsNode.JoinedAt?.ToDateTime(),
+ IsClubActive = cmsNode.IsClubActive,
+ ActivationWeekNumber = cmsNode.ActivationWeekNumber,
+ LeftChild = leftChild != null ? BuildNodeRecursive(leftChild, allNodes, nodeDict, level + 1) : null,
+ RightChild = rightChild != null ? BuildNodeRecursive(rightChild, allNodes, nodeDict, level + 1) : null
+ };
+ }
+
+ private int CalculateDepth(NetworkNodeDto? node)
+ {
+ if (node == null) return 0;
+ return 1 + Math.Max(CalculateDepth(node.LeftChild), CalculateDepth(node.RightChild));
+ }
+}
diff --git a/src/FrontOffice.BFF.Application/NetworkMembershipCQ/Queries/GetSubordinateTree/GetSubordinateTreeResponseDto.cs b/src/FrontOffice.BFF.Application/NetworkMembershipCQ/Queries/GetSubordinateTree/GetSubordinateTreeResponseDto.cs
new file mode 100644
index 0000000..ae9cfa8
--- /dev/null
+++ b/src/FrontOffice.BFF.Application/NetworkMembershipCQ/Queries/GetSubordinateTree/GetSubordinateTreeResponseDto.cs
@@ -0,0 +1,34 @@
+using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
+
+namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetSubordinateTree;
+
+///
+/// Response برای درخت زیرمجموعه - ساختار مشابه GetMyNetworkTreeResponseDto
+///
+public class GetSubordinateTreeResponseDto
+{
+ ///
+ /// نود ریشه (کاربر زیرمجموعه انتخاب شده)
+ ///
+ public NetworkNodeDto? RootNode { get; set; }
+
+ ///
+ /// تعداد کل اعضا در این زیردرخت
+ ///
+ public int TotalMembers { get; set; }
+
+ ///
+ /// عمق فعلی درخت
+ ///
+ public int CurrentDepth { get; set; }
+
+ ///
+ /// آیا کاربر میتواند به عقب برگردد (root خودش نیست)
+ ///
+ public bool CanGoBack { get; set; }
+
+ ///
+ /// شناسه parent برای بازگشت به عقب
+ ///
+ public long? ParentUserId { get; set; }
+}
diff --git a/src/FrontOffice.BFF.WebApi/Common/Mappings/NetworkMembershipProfile.cs b/src/FrontOffice.BFF.WebApi/Common/Mappings/NetworkMembershipProfile.cs
index 5a62b29..9ba00de 100644
--- a/src/FrontOffice.BFF.WebApi/Common/Mappings/NetworkMembershipProfile.cs
+++ b/src/FrontOffice.BFF.WebApi/Common/Mappings/NetworkMembershipProfile.cs
@@ -1,6 +1,7 @@
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkStatistics;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkPosition;
+using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetSubordinateTree;
using Google.Protobuf.WellKnownTypes;
using ProtoDto = FrontOffice.BFF.NetworkMembership.Protobuf.Protos.NetworkMembership;
@@ -14,12 +15,22 @@ public class NetworkMembershipProfile : IRegister
config.NewConfig()
.Map(dest => dest.MaxDepth, src => src.MaxDepth > 0 ? src.MaxDepth : 3);
+ config.NewConfig()
+ .Map(dest => dest.TargetUserId, src => src.TargetUserId)
+ .Map(dest => dest.MaxDepth, src => src.MaxDepth > 0 ? src.MaxDepth : 3);
+
// Response mappings - Tree
config.NewConfig()
.Map(dest => dest.RootNode, src => src.RootNode)
.Map(dest => dest.TotalMembers, src => src.TotalMembers)
.Map(dest => dest.CurrentDepth, src => src.CurrentDepth);
+ // Subordinate tree uses same response type
+ config.NewConfig()
+ .Map(dest => dest.RootNode, src => src.RootNode)
+ .Map(dest => dest.TotalMembers, src => src.TotalMembers)
+ .Map(dest => dest.CurrentDepth, src => src.CurrentDepth);
+
config.NewConfig()
.Map(dest => dest.UserId, src => src.UserId)
.Map(dest => dest.FullName, src => src.FullName)
diff --git a/src/FrontOffice.BFF.WebApi/Services/NetworkMembershipService.cs b/src/FrontOffice.BFF.WebApi/Services/NetworkMembershipService.cs
index 4d025a8..33092e8 100644
--- a/src/FrontOffice.BFF.WebApi/Services/NetworkMembershipService.cs
+++ b/src/FrontOffice.BFF.WebApi/Services/NetworkMembershipService.cs
@@ -2,6 +2,7 @@ using FrontOffice.BFF.WebApi.Common.Services;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkStatistics;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkPosition;
+using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetSubordinateTree;
using FrontOffice.BFF.NetworkMembership.Protobuf.Protos.NetworkMembership;
namespace FrontOffice.BFF.WebApi.Services;
@@ -26,6 +27,14 @@ public class NetworkMembershipService : NetworkMembershipContract.NetworkMembers
return await _dispatchRequestToCQRS.Handle(request, context);
}
+ ///
+ /// دریافت درخت شبکه یک زیرمجموعه (با چک امنیتی)
+ ///
+ public override async Task GetSubordinateTree(GetSubordinateTreeRequest request, ServerCallContext context)
+ {
+ return await _dispatchRequestToCQRS.Handle(request, context);
+ }
+
///
/// دریافت آمار شبکه کاربر جاری
///
diff --git a/src/Protobufs/FrontOffice.BFF.NetworkMembership.Protobuf/Protos/networkmembership.proto b/src/Protobufs/FrontOffice.BFF.NetworkMembership.Protobuf/Protos/networkmembership.proto
index 7a386a3..41be677 100644
--- a/src/Protobufs/FrontOffice.BFF.NetworkMembership.Protobuf/Protos/networkmembership.proto
+++ b/src/Protobufs/FrontOffice.BFF.NetworkMembership.Protobuf/Protos/networkmembership.proto
@@ -19,6 +19,13 @@ service NetworkMembershipContract
};
};
+ // دریافت درخت شبکه یک زیرمجموعه (با چک امنیتی)
+ rpc GetSubordinateTree(GetSubordinateTreeRequest) returns (GetMyNetworkTreeResponse){
+ option (google.api.http) = {
+ get: "/NetworkMembership/SubordinateTree/{target_user_id}"
+ };
+ };
+
// دریافت آمار شبکه کاربر جاری
rpc GetMyNetworkStatistics(google.protobuf.Empty) returns (GetMyNetworkStatisticsResponse){
option (google.api.http) = {
@@ -40,6 +47,13 @@ message GetMyNetworkTreeRequest
int32 max_depth = 1; // حداکثر عمق درخت (پیشفرض: 3)
}
+// ============ GetSubordinateTree ============
+message GetSubordinateTreeRequest
+{
+ int64 target_user_id = 1; // شناسه زیرمجموعه مورد نظر
+ int32 max_depth = 2; // حداکثر عمق درخت (پیشفرض: 3)
+}
+
message GetMyNetworkTreeResponse
{
NetworkNodeModel root_node = 1;