# Network Tree - Activation Week Feature ## نمای کلی (Overview) این سند تغییرات مربوط به افزودن قابلیت فیلتر و نمایش هفته فعال‌سازی در درخت شبکه را توضیح می‌دهد. **تاریخ پیاده‌سازی:** دسامبر 2025 **تغییرات کلیدی:** - اضافه شدن فیلد `IsActivatedInTargetWeek` برای flagging (به جای filtering) - حذف فیلتر سمت Backend و انتقال به UI - نمایش بصری وضعیت فعال‌سازی در درخت --- ## منطق کسب‌وکار (Business Logic) ### رویکرد قبلی (❌ Removed) - فیلتر می‌کرد و فقط نودهایی که در هفته هدف فعال شده‌اند نمایش داده می‌شدند - مشکل: کاربران نمی‌توانستند کل ساختار شبکه را ببینند ### رویکرد جدید (✅ Current) - **همه نودها نمایش داده می‌شوند** (بدون فیلتر در دیتابیس) - هر نود یک flag دارد: `IsActivatedInTargetWeek` - UI از این flag برای نمایش بصری استفاده می‌کند ### محاسبه هفته فعال‌سازی ```csharp private static int CalculateWeekNumber(DateTimeOffset date) { var persianCalendar = new PersianCalendar(); int year = persianCalendar.GetYear(date.DateTime); int dayOfYear = persianCalendar.GetDayOfYear(date.DateTime); int weekNumber = (dayOfYear - 1) / 7 + 1; return int.Parse($"{year}{weekNumber:D2}"); // مثال: 140352 = سال 1403، هفته 52 } ``` --- ## تغییرات Backend ### 1. DTO Changes **فایل:** `CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/NetworkTreeDto.cs` ```csharp public class NetworkTreeDto { // ... existing fields public string? ActivationWeekNumber { get; set; } public bool IsActivatedInTargetWeek { get; set; } // ✅ NEW public DateTimeOffset UserCreated { get; set; } public NetworkTreeDto? LeftChild { get; set; } public NetworkTreeDto? RightChild { get; set; } } ``` ### 2. Query Handler Changes **فایل:** `CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQueryHandler.cs` #### تغییر در BuildTree Method ```csharp private NetworkTreeDto BuildTree( User user, int currentDepth, int maxDepth, string? requestActivationWeekNumber) // ✅ پارامتر اضافه شد { // محاسبه هفته فعال‌سازی string? activationWeekNumber = null; bool isActivatedInTargetWeek = false; if (user.ClubMembership?.ActivatedAt != null) { activationWeekNumber = CalculateWeekNumber(user.ClubMembership.ActivatedAt.Value) .ToString(); // چک کردن اینکه آیا در هفته هدف فعال شده if (!string.IsNullOrEmpty(requestActivationWeekNumber)) { isActivatedInTargetWeek = activationWeekNumber == requestActivationWeekNumber; } } var node = new NetworkTreeDto { // ... existing fields ActivationWeekNumber = activationWeekNumber, IsActivatedInTargetWeek = isActivatedInTargetWeek, // ✅ تنظیم flag }; // ... recursive calls } ``` #### حذف فیلتر از GetFilteredChildren **قبل (❌):** ```csharp private IEnumerable GetFilteredChildren( IEnumerable children, bool? isClubActive, string? activationWeekNumber) { var query = children.AsQueryable(); if (isClubActive.HasValue) { query = query.Where(u => u.ClubMembership != null && u.ClubMembership.IsActive == isClubActive.Value); } if (!string.IsNullOrEmpty(activationWeekNumber)) { // ❌ فیلتر می‌کرد query = query.Where(u => /* filter logic */); } return query.ToList(); } ``` **بعد (✅):** ```csharp private IEnumerable GetFilteredChildren( IEnumerable children, bool? isClubActive) { var query = children.AsQueryable(); // فقط فیلتر IsClubActive باقی ماند if (isClubActive.HasValue) { query = query.Where(u => u.ClubMembership != null && u.ClubMembership.IsActive == isClubActive.Value); } return query.ToList(); } ``` ### 3. Proto Definition **فایل:** `CMSMicroservice.Protobuf/Protos/networkmembership.proto` ```protobuf message NetworkTreeNodeModel { int64 user_id = 1; string user_name = 2; optional int64 parent_id = 3; optional int32 network_leg = 4; optional int32 network_level = 5; optional bool is_active = 6; optional google.protobuf.Timestamp joined_at = 7; optional google.protobuf.Timestamp club_activated_at = 8; bool is_club_active = 9; string activation_week_number = 10; bool is_activated_in_target_week = 11; // ✅ NEW google.protobuf.Timestamp user_created = 12; } ``` ### 4. Mapping **فایل:** `CMSMicroservice.WebApi/Common/Mappings/NetworkMembershipProfile.cs` ```csharp var protoNode = new NetworkTreeNodeModel { UserId = node.UserId, UserName = node.UserName, ParentId = node.ParentId, NetworkLeg = node.NetworkLeg, NetworkLevel = node.NetworkLevel, IsActive = node.IsActive, JoinedAt = node.JoinedAt.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(node.JoinedAt.Value, DateTimeKind.Utc)) : null, ClubActivatedAt = node.ClubActivatedAt.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(node.ClubActivatedAt.Value, DateTimeKind.Utc)) : null, IsClubActive = node.IsClubActive, ActivationWeekNumber = node.ActivationWeekNumber ?? string.Empty, IsActivatedInTargetWeek = node.IsActivatedInTargetWeek, // ✅ NEW UserCreated = Timestamp.FromDateTime(DateTime.SpecifyKind(node.UserCreated, DateTimeKind.Utc)) }; ``` --- ## تغییرات BFF ### Proto & Mapping همان تغییرات در CMS در BFF هم اعمال شد: **فایل‌ها:** - `BackOffice.BFF.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeResponseDto.cs` - `BackOffice.BFF.WebApi/Common/Mappings/NetworkMembershipProfile.cs` - `Protobufs/networkmembership.proto` ```csharp public class NetworkTreeNodeDto { // ... existing properties public bool IsActivatedInTargetWeek { get; set; } // ✅ NEW public string ActivationWeekNumber { get; set; } = string.Empty; } ``` --- ## تغییرات Frontend ### 1. Razor Component **فایل:** `BackOffice/Pages/Network/NetworkTreeViewer.razor` #### تغییر در ستون "وضعیت" **قبل (❌):** ```razor @if (context.Item.IsActive!=null) { @((bool)context.Item.IsActive ? "فعال" : "غیرفعال") } ``` **بعد (✅):** ```razor @(context.Item.IsClubActive ? "فعال" : "غیرفعال") ``` #### ارسال داده به JavaScript ```csharp private async Task RenderTree() { if (_treeData == null || !_treeData.Nodes.Any()) return; var jsNodes = _treeData.Nodes.Select(n => new { userId = n.UserId, userName = n.UserName, parentId = n.ParentId, networkLevel = n.NetworkLevel, networkLeg = n.NetworkLeg, isActive = n.IsClubActive, // ✅ تغییر به IsClubActive isClubActive = n.IsClubActive, isActivatedInTargetWeek = n.IsActivatedInTargetWeek, // ✅ NEW activationWeekNumber = _activationWeekFilter ?? "", // ✅ فیلتر UI clubActivatedAt = n.ClubActivatedAt?.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd") ?? "", userCreated = n.UserCreated?.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd") ?? "" }).ToArray(); await JS.InvokeVoidAsync("NetworkTreeViewer.initialize", "network-tree-container", jsNodes); } ``` **نکته مهم:** `activationWeekNumber` از فیلتر UI گرفته می‌شود (`_activationWeekFilter`) نه از Backend. ### 2. JavaScript Visualization **فایل:** `BackOffice/wwwroot/js/network-tree.js` #### منطق رنگ نود (دایره) ```javascript node.append('circle') .attr('r', 8) .style('fill', d => { // اگر هفته‌ای انتخاب نشده، همه سبز if (!d.data.activationWeekNumber || d.data.activationWeekNumber === '') { return '#4caf50'; } // اگر در هفته هدف فعال شده، سبز، وگرنه قرمز return d.data.isActivatedInTargetWeek ? '#4caf50' : '#f44336'; }) .style('stroke', '#fff') .style('stroke-width', 2) .style('cursor', 'pointer'); ``` #### منطق رنگ تایتل (نام کاربر) ```javascript node.append('text') .attr('dy', -15) .attr('text-anchor', 'middle') .style('font-size', '12px') .style('font-weight', 'bold') .style('fill', d => d.data.isClubActive ? '#424242' : '#9e9e9e') .text(d => d.data.userName || `User ${d.data.userId}`); ``` #### اضافه کردن فیلدها به buildHierarchy ```javascript buildHierarchy: function(nodes) { // ... const nodeMap = new Map(); nodes.forEach(node => { nodeMap.set(node.userId, { userId: node.userId, userName: node.userName, parentId: node.parentId, level: node.networkLevel, networkLeg: node.networkLeg, isActive: node.isActive, isClubActive: node.isClubActive, // ✅ NEW isActivatedInTargetWeek: node.isActivatedInTargetWeek, // ✅ NEW activationWeekNumber: node.activationWeekNumber, // ✅ NEW clubActivatedAt: node.clubActivatedAt, userCreated: node.userCreated, children: [] }); }); // ... } ``` #### Legend (راهنمای رنگ‌ها) ```javascript // Legend for title colors (club status) legend.append('text') .attr('x', 0) .attr('y', 0) .style('font-size', '12px') .style('font-weight', 'bold') .style('fill', '#424242') .text('باشگاه فعال'); legend.append('text') .attr('x', 0) .attr('y', 20) .style('font-size', '12px') .style('font-weight', 'bold') .style('fill', '#9e9e9e') .text('باشگاه غیرفعال'); // Legend for circles (week status) legend.append('circle') .attr('cx', 0) .attr('cy', 50) .attr('r', 6) .style('fill', '#4caf50'); legend.append('text') .attr('x', 12) .attr('y', 54) .style('font-size', '12px') .text('فعال در هفته هدف'); legend.append('circle') .attr('cx', 0) .attr('cy', 75) .attr('r', 6) .style('fill', '#f44336'); legend.append('text') .attr('x', 12) .attr('y', 79) .style('font-size', '12px') .text('خارج از هفته هدف'); ``` --- ## رفتار UI ### حالت 1: بدون فیلتر هفته **وضعیت:** `_activationWeekFilter` خالی است **رفتار:** - **دایره‌ها:** همه سبز (#4caf50) - **تایتل:** مشکی (#424242) برای باشگاه فعال، خاکستری (#9e9e9e) برای باشگاه غیرفعال ### حالت 2: با فیلتر هفته **وضعیت:** مثلاً `_activationWeekFilter = "140352"` **رفتار:** - **دایره‌ها:** - سبز (#4caf50) → کاربران فعال شده در هفته 52 سال 1403 - قرمز (#f44336) → کاربران فعال شده در هفته‌های دیگر - **تایتل:** همچنان بر اساس `isClubActive` ### حالت 3: فیلتر IsClubActive این فیلتر در سمت Backend اعمال می‌شود و نودهای غیرفعال را حذف می‌کند. --- ## Flow Diagram ``` ┌─────────────────────────────────────────────────────────────┐ │ User Interface │ │ ┌────────────────┐ ┌──────────────────┐ │ │ │ IsClubActive │ │ActivationWeek │ │ │ │ Filter │ │ Filter │ │ │ └────────┬───────┘ └────────┬─────────┘ │ └───────────┼──────────────────┼────────────────────────────┘ │ │ ▼ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Backend (CMS) │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ GetNetworkTreeQueryHandler │ │ │ │ │ │ │ │ 1. GetFilteredChildren (IsClubActive filter only) │ │ │ │ 2. BuildTree (calculate IsActivatedInTargetWeek) │ │ │ │ 3. Return ALL nodes with flags │ │ │ └──────────────────────────────────────────────────────┘ │ └───────────────────────────┬─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ BFF Layer │ │ - Proto mapping │ │ - Pass-through to Frontend │ └───────────────────────────┬─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Frontend (Blazor) │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ NetworkTreeViewer.razor │ │ │ │ │ │ │ │ - Prepare data with UI filter (_activationWeekFilter)│ │ │ │ - Send to JavaScript │ │ │ └──────────────────────────────────────────────────────┘ │ └───────────────────────────┬─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ JavaScript (D3.js) │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ network-tree.js │ │ │ │ │ │ │ │ - Apply visual logic: │ │ │ │ * Circle color by activationWeekNumber + flag │ │ │ │ * Title color by isClubActive │ │ │ └──────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Data Model ### Request ```csharp public class GetNetworkTreeRequest { public long UserId { get; set; } public int? MaxDepth { get; set; } public bool? IsClubActive { get; set; } // Backend filter public string? ActivationWeekNumber { get; set; } // For flag calculation only } ``` ### Response ```csharp public class NetworkTreeDto { public long UserId { get; set; } public string UserName { get; set; } public long? ParentId { get; set; } public int? NetworkLeg { get; set; } public int? NetworkLevel { get; set; } public bool? IsActive { get; set; } // Deprecated public DateTime? JoinedAt { get; set; } public DateTime? ClubActivatedAt { get; set; } public bool IsClubActive { get; set; } // ✅ Use this public string? ActivationWeekNumber { get; set; } public bool IsActivatedInTargetWeek { get; set; } // ✅ NEW public DateTimeOffset UserCreated { get; set; } public NetworkTreeDto? LeftChild { get; set; } public NetworkTreeDto? RightChild { get; set; } } ``` --- ## Testing Scenarios ### Test 1: بدون فیلتر **Input:** - `IsClubActive`: null - `ActivationWeekNumber`: null **Expected:** - همه نودها نمایش داده شوند - همه دایره‌ها سبز - تایتل‌ها بر اساس IsClubActive ### Test 2: فیلتر باشگاه فعال **Input:** - `IsClubActive`: true - `ActivationWeekNumber`: null **Expected:** - فقط نودهای با باشگاه فعال - همه دایره‌ها سبز - همه تایتل‌ها مشکی ### Test 3: فیلتر هفته **Input:** - `IsClubActive`: null - `ActivationWeekNumber`: "140352" **Expected:** - همه نودها نمایش داده شوند - دایره سبز: فعال شده در هفته 52 - دایره قرمز: فعال شده در هفته‌های دیگر - تایتل‌ها بر اساس IsClubActive ### Test 4: ترکیب فیلترها **Input:** - `IsClubActive`: true - `ActivationWeekNumber`: "140352" **Expected:** - فقط نودهای با باشگاه فعال - دایره سبز: فعال شده در هفته 52 - دایره قرمز: فعال شده در هفته‌های دیگر - همه تایتل‌ها مشکی (چون همه باشگاه فعال دارند) --- ## Performance Considerations ### Database Query - ✅ فیلتر `ActivationWeekNumber` از Query حذف شد - ✅ فقط فیلتر `IsClubActive` در سمت دیتابیس - ⚠️ ممکن است تعداد نودهای بیشتری بازگردانده شود ### Memory - Backend همه نودها را می‌فرستد - Frontend/JavaScript فیلتر بصری اعمال می‌کند - برای درخت‌های بسیار بزرگ (>1000 نود) ممکن است نیاز به pagination باشد ### UI Rendering - D3.js برای درخت‌های متوسط (<500 نود) عملکرد خوبی دارد - برای بهبود عملکرد می‌توان از virtualization استفاده کرد --- ## Migration Notes ### Breaking Changes - ❌ `IsActive` deprecated است → استفاده از `IsClubActive` - ✅ فیلد جدید `IsActivatedInTargetWeek` اضافه شد ### Backward Compatibility - Proto field numbers حفظ شده‌اند - Response structure تغییر نکرده (فقط فیلد جدید اضافه شده) ### Deployment Steps 1. Deploy Backend (CMS) با Proto جدید 2. Deploy BFF با Proto جدید 3. Deploy Frontend با visualization جدید 4. تست تمام scenarios --- ## نکات مهم (Key Points) ### ✅ Do's - از `IsClubActive` برای وضعیت باشگاه استفاده کنید - `IsActivatedInTargetWeek` فقط برای نمایش بصری است - فیلتر UI را از Razor به JS بفرستید (`_activationWeekFilter`) ### ❌ Don'ts - از `IsActive` استفاده نکنید (deprecated) - `ActivationWeekNumber` را از Backend برای UI filtering استفاده نکنید - فیلتر `ActivationWeekNumber` را در Query اعمال نکنید ### 💡 Best Practices - همیشه فیلتر UI و Backend flag را sync نگه دارید - برای درخت‌های بزرگ از lazy loading استفاده کنید - Legend را همیشه با منطق UI sync کنید --- ## فایل‌های تغییر یافته ### Backend (CMS) - ✅ `NetworkTreeDto.cs` - اضافه `IsActivatedInTargetWeek` - ✅ `GetNetworkTreeQueryHandler.cs` - محاسبه flag + حذف فیلتر - ✅ `networkmembership.proto` - اضافه field 11 - ✅ `NetworkMembershipProfile.cs` - mapping فیلد جدید ### BFF - ✅ `GetNetworkTreeResponseDto.cs` - اضافه property - ✅ `NetworkMembershipProfile.cs` - mapping - ✅ `networkmembership.proto` - sync با CMS ### Frontend - ✅ `NetworkTreeViewer.razor` - تغییر `IsActive` → `IsClubActive` - ✅ `NetworkTreeViewer.razor` - اضافه `isActivatedInTargetWeek` به jsNodes - ✅ `network-tree.js` - منطق رنگ نود بر اساس flag - ✅ `network-tree.js` - منطق رنگ تایتل بر اساس `isClubActive` - ✅ `network-tree.js` - Legend جدید --- ## مراجع (References) - [Binary Tree Guide](../../01-BUSINESS/binary-tree-guide.md) - [Network Commission System](../../01-BUSINESS/network-commission-system.md) - [CMS API Coverage](./api-coverage.md) --- **تاریخ ایجاد:** 14 دسامبر 2025 **آخرین به‌روزرسانی:** 14 دسامبر 2025 **نویسنده:** Development Team