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

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.cs
  • BackOffice.BFF.WebApi/Common/Mappings/NetworkMembershipProfile.cs
  • Protobufs/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: 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 - تغییر IsActiveIsClubActive
  • NetworkTreeViewer.razor - اضافه isActivatedInTargetWeek به jsNodes
  • network-tree.js - منطق رنگ نود بر اساس flag
  • network-tree.js - منطق رنگ تایتل بر اساس isClubActive
  • network-tree.js - Legend جدید

مراجع (References)


تاریخ ایجاد: 14 دسامبر 2025 آخرین به‌روزرسانی: 14 دسامبر 2025 نویسنده: Development Team