# 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
