Files
docs/03-BACKEND/CMS/network-tree-activation-week.md
T
masoodafar-web 002e99f6bf Implement Persian Date Conversion and Enhance User Network Information Service
- Added PersianDateTimeService for converting Gregorian dates to Persian format in the BackOffice frontend.
- Updated multiple frontend pages (Dashboard, UserPayouts, WorkerControl, UserNetworkInfo) to utilize the new Persian date service.
- Enhanced GetUserNetworkPositionDto with 28+ new fields for comprehensive user network data.
- Updated GetUserNetworkPositionQueryHandler to include new methods for calculating network statistics.
- Modified Protobuf messages to accommodate the new fields, increasing from 14 to 42.
- Refined week number calculation algorithm to ensure consistency across C# and SQL implementations.
- Created new CSV and Excel files for binary plan calculations.
- Ensured all changes are tested and validated for accuracy and performance.
2025-12-20 06:15:59 +03:30

643 lines
22 KiB
Markdown

# 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<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();
}
```
**بعد (✅):**
```csharp
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`
```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
<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>
```
**بعد (✅):**
```razor
<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
```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