22 KiB
Network Tree - Activation Week Feature
نمای کلی (Overview)
این سند تغییرات مربوط به افزودن قابلیت فیلتر و نمایش هفته فعالسازی در درخت شبکه را توضیح میدهد.
تاریخ پیادهسازی: دسامبر 2025
تغییرات کلیدی:
- اضافه شدن فیلد
IsActivatedInTargetWeekبرای flagging (به جای filtering) - حذف فیلتر سمت Backend و انتقال به UI
- نمایش بصری وضعیت فعالسازی در درخت
منطق کسبوکار (Business Logic)
رویکرد قبلی (❌ Removed)
- فیلتر میکرد و فقط نودهایی که در هفته هدف فعال شدهاند نمایش داده میشدند
- مشکل: کاربران نمیتوانستند کل ساختار شبکه را ببینند
رویکرد جدید (✅ Current)
- همه نودها نمایش داده میشوند (بدون فیلتر در دیتابیس)
- هر نود یک flag دارد:
IsActivatedInTargetWeek - UI از این flag برای نمایش بصری استفاده میکند
محاسبه هفته فعالسازی
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
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
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
قبل (❌):
private IEnumerable<User> GetFilteredChildren(
IEnumerable<User> 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();
}
بعد (✅):
private IEnumerable<User> GetFilteredChildren(
IEnumerable<User> 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
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
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.csBackOffice.BFF.WebApi/Common/Mappings/NetworkMembershipProfile.csProtobufs/networkmembership.proto
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
تغییر در ستون "وضعیت"
قبل (❌):
<PropertyColumn Property="x => x.IsActive" Title="وضعیت">
<CellTemplate>
@if (context.Item.IsActive!=null) {
<MudChip Color="@((bool)context.Item.IsActive ? Color.Success : Color.Error)">
@((bool)context.Item.IsActive ? "فعال" : "غیرفعال")
</MudChip>
}
</CellTemplate>
</PropertyColumn>
بعد (✅):
<PropertyColumn Property="x => x.IsClubActive" Title="وضعیت">
<CellTemplate>
<MudChip T="string"
Color="@(context.Item.IsClubActive ? Color.Success : Color.Error)"
Size="Size.Small">
@(context.Item.IsClubActive ? "فعال" : "غیرفعال")
</MudChip>
</CellTemplate>
</PropertyColumn>
ارسال داده به JavaScript
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
منطق رنگ نود (دایره)
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');
منطق رنگ تایتل (نام کاربر)
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
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 (راهنمای رنگها)
// 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
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
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: nullActivationWeekNumber: null
Expected:
- همه نودها نمایش داده شوند
- همه دایرهها سبز
- تایتلها بر اساس IsClubActive
Test 2: فیلتر باشگاه فعال
Input:
IsClubActive: trueActivationWeekNumber: null
Expected:
- فقط نودهای با باشگاه فعال
- همه دایرهها سبز
- همه تایتلها مشکی
Test 3: فیلتر هفته
Input:
IsClubActive: nullActivationWeekNumber: "140352"
Expected:
- همه نودها نمایش داده شوند
- دایره سبز: فعال شده در هفته 52
- دایره قرمز: فعال شده در هفتههای دیگر
- تایتلها بر اساس IsClubActive
Test 4: ترکیب فیلترها
Input:
IsClubActive: trueActivationWeekNumber: "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
- ❌
IsActivedeprecated است → استفاده ازIsClubActive - ✅ فیلد جدید
IsActivatedInTargetWeekاضافه شد
Backward Compatibility
- Proto field numbers حفظ شدهاند
- Response structure تغییر نکرده (فقط فیلد جدید اضافه شده)
Deployment Steps
- Deploy Backend (CMS) با Proto جدید
- Deploy BFF با Proto جدید
- Deploy Frontend با visualization جدید
- تست تمام 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)
تاریخ ایجاد: 14 دسامبر 2025 آخرین بهروزرسانی: 14 دسامبر 2025 نویسنده: Development Team