Files
BackOffice/src/BackOffice/Pages/Network/NetworkTreeViewer.razor
T
masoodafar-web 49017027d0
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m20s
feat: full BackOffice updates - discount shop, blog, content, UI improvements
- Blog management pages (posts, categories)
- Content management pages (site pages, sections)
- Contract management pages
- Discount shop improvements (order details, product form, categories)
- Wallet management pages
- Commission withdrawal details/reject dialogs
- Orders hub, sales reports
- System hub, global search, breadcrumb
- Club features page, club hub
- UI/UX improvements across all pages
- AppBreadcrumb, GlobalSearch components
- NavMenu updates and styling
2026-02-16 00:56:39 +03:30

432 lines
17 KiB
Plaintext

@page "/network/tree"
@attribute [Authorize]
@implements IAsyncDisposable
@inject IJSRuntime JS
@using CMSMicroservice.Protobuf.Protos.NetworkMembership
@using BackOffice.Pages.AutoComplete
<div>
<MudText Typo="Typo.h4" Class="mb-4">درخت شبکه</MudText>
@* Search & Filter Panel *@
<MudPaper Class="pa-4 mb-4">
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center" Class="mb-3">
<div style="max-width: 300px; min-width: 250px;">
<UserAutoComplete Label="جستجوی کاربر"
@bind-SelectedUserId="_searchUserId" />
</div>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="LoadTree"
StartIcon="@Icons.Material.Filled.Search"
Disabled="_isLoading">
نمایش درخت
</MudButton>
</MudStack>
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center">
<MudSelect T="int" @bind-Value="_selectedDepth" Label="عمق نمایش" Variant="Variant.Outlined"
Dense="true" Margin="Margin.Dense" Style="width: 120px;">
<MudSelectItem T="int" Value="3">3 سطح</MudSelectItem>
<MudSelectItem T="int" Value="5">5 سطح</MudSelectItem>
<MudSelectItem T="int" Value="10">10 سطح</MudSelectItem>
<MudSelectItem T="int" Value="15">15 سطح</MudSelectItem>
<MudSelectItem T="int" Value="100">همه</MudSelectItem>
</MudSelect>
<MudSelect T="bool?" Label="وضعیت باشگاه" @bind-Value="_clubActiveFilter" Variant="Variant.Outlined"
Clearable="true" Dense="true" Style="width: 140px;">
<MudSelectItem T="bool?" Value="@(null)">همه</MudSelectItem>
<MudSelectItem T="bool?" Value="@(true)">فعال</MudSelectItem>
<MudSelectItem T="bool?" Value="@(false)">غیرفعال</MudSelectItem>
</MudSelect>
<WeekNumberPicker Label="هفته فعالسازی"
@bind-SelectedWeekDefinitionId="_activationWeekFilter" />
</MudStack>
</MudPaper>
@* Chart Toolbar *@
@if (_treeData != null && _treeData.Nodes.Any())
{
<MudPaper Class="pa-2 mb-2">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
<MudButtonGroup Variant="Variant.Outlined" Size="Size.Small">
<MudIconButton Icon="@Icons.Material.Filled.UnfoldMore" OnClick="ExpandAll" Title="باز کردن همه" />
<MudIconButton Icon="@Icons.Material.Filled.UnfoldLess" OnClick="CollapseAll" Title="بستن همه" />
<MudIconButton Icon="@Icons.Material.Filled.CenterFocusStrong" OnClick="FitChart" Title="نمایش کامل" />
<MudIconButton Icon="@Icons.Material.Filled.Image" OnClick="ExportPng" Title="خروجی تصویر" />
</MudButtonGroup>
@if (_currentViewUserId.HasValue && _currentViewUserId != _searchUserId)
{
<MudButtonGroup Variant="Variant.Outlined" Size="Size.Small" Color="Color.Secondary">
<MudButton StartIcon="@Icons.Material.Filled.ArrowForward" OnClick="GoBack" Size="Size.Small">
بازگشت
</MudButton>
<MudButton StartIcon="@Icons.Material.Filled.Home" OnClick="GoToRoot" Size="Size.Small">
ریشه
</MudButton>
</MudButtonGroup>
}
<MudStack Row="true" Spacing="2">
<MudChip T="string" Color="Color.Info" Size="Size.Small" Icon="@Icons.Material.Filled.People">
کل: @_totalMembers
</MudChip>
<MudChip T="string" Color="Color.Success" Size="Size.Small">
چپ: @_leftCount
</MudChip>
<MudChip T="string" Color="Color.Warning" Size="Size.Small">
راست: @_rightCount
</MudChip>
</MudStack>
</MudStack>
</MudPaper>
}
@* Chart Container *@
@if (_isLoading)
{
<MudPaper Class="pa-8 d-flex flex-column align-center justify-center" Style="min-height: 500px;">
<MudProgressCircular Color="Color.Primary" Size="Size.Large" Indeterminate="true" />
<MudText Typo="Typo.body1" Class="mt-3">در حال بارگذاری درخت شبکه...</MudText>
</MudPaper>
}
else if (_hasError)
{
<MudPaper Class="pa-8 d-flex flex-column align-center justify-center" Style="min-height: 300px;">
<MudIcon Icon="@Icons.Material.Filled.Error" Color="Color.Error" Size="Size.Large" />
<MudText Typo="Typo.body1" Class="mt-2" Color="Color.Error">خطا در بارگذاری درخت</MudText>
<MudButton Variant="Variant.Text" Color="Color.Primary" OnClick="LoadTree" Class="mt-2">
تلاش مجدد
</MudButton>
</MudPaper>
}
else if (_treeData != null && _treeData.Nodes.Any())
{
<MudPaper Class="pa-0" Style="min-height: 600px; overflow: hidden;">
<div id="admin-org-chart-container" class="admin-org-chart-container"></div>
</MudPaper>
@* Data Grid *@
<MudPaper Class="pa-4 mt-4">
<MudText Typo="Typo.h6" Class="mb-3">جدول اعضای شبکه</MudText>
<MudDataGrid T="NetworkTreeNodeModel" Items="@_treeData.Nodes" Hover="true" Filterable="true" Dense="true"
SortMode="SortMode.Multiple" RowsPerPage="10">
<Columns>
<PropertyColumn Property="x => x.UserId" Title="شناسه" />
<PropertyColumn Property="x => x.UserName" Title="نام کاربر" />
<PropertyColumn Property="x => x.NetworkLeg" Title="موقعیت">
<CellTemplate>
<MudChip T="string"
Color="@(context.Item.NetworkLeg == 0 ? Color.Success : Color.Warning)"
Size="Size.Small">
@(context.Item.NetworkLeg == 0 ? "چپ" : "راست")
</MudChip>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.NetworkLevel" Title="سطح" />
<PropertyColumn Property="x => x.IsClubActive" Title="باشگاه">
<CellTemplate>
<MudChip T="string"
Color="@(context.Item.IsClubActive ? Color.Success : Color.Default)"
Size="Size.Small">
@(context.Item.IsClubActive ? "فعال" : "غیرفعال")
</MudChip>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.ClubActivatedAt" Title="تاریخ فعالسازی">
<CellTemplate>
@if (context.Item.ClubActivatedAt != null)
{
@context.Item.ClubActivatedAt.ToDateTime().ToLocalTime().MiladiToJalali()
}
else
{
<span style="color: #999;">-</span>
}
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.ActivationWeekDefinitionId" Title="هفته">
<CellTemplate>
@if (context.Item.ActivationWeekDefinitionId != null)
{
<MudChip T="string" Size="Size.Small" Color="Color.Info">
W@context.Item.ActivationWeekDefinitionId
</MudChip>
}
else
{
<span style="color: #999;">-</span>
}
</CellTemplate>
</PropertyColumn>
<TemplateColumn Title="عملیات" Sortable="false">
<CellTemplate>
<MudButtonGroup Size="Size.Small" Variant="Variant.Text">
<MudIconButton Icon="@Icons.Material.Filled.AccountTree"
Color="Color.Primary"
Title="نمایش درخت این کاربر"
OnClick="@(() => ViewUserTree(context.Item.UserId))" />
<MudIconButton Icon="@Icons.Material.Filled.Info"
Color="Color.Info"
Title="جزئیات کاربر"
OnClick="@(() => ViewUserDetails(context.Item.UserId))" />
</MudButtonGroup>
</CellTemplate>
</TemplateColumn>
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="NetworkTreeNodeModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد در صفحه" />
</PagerContent>
</MudDataGrid>
</MudPaper>
}
else
{
<MudPaper Class="pa-8">
<MudAlert Severity="Severity.Info" Icon="@Icons.Material.Filled.Info">
برای نمایش درخت شبکه، کاربر مورد نظر را جستجو کرده و دکمه "نمایش درخت" را بزنید.
</MudAlert>
</MudPaper>
}
</div>
@code {
[Inject] public NetworkMembershipContract.NetworkMembershipContractClient NetworkContract { get; set; }
[Inject] public NavigationManager NavigationManager { get; set; }
private long? _searchUserId;
private long? _currentViewUserId;
private GetNetworkTreeResponse _treeData;
private bool _isLoading;
private bool _hasError;
private int _totalMembers;
private int _leftCount;
private int _rightCount;
private int _selectedDepth = 10;
private bool? _clubActiveFilter;
private long? _activationWeekFilter;
private DotNetObjectReference<NetworkTreeViewer> _dotNetRef;
private Stack<long> _navigationHistory = new();
private bool _chartNeedsInit = false;
protected override void OnInitialized()
{
_dotNetRef = DotNetObjectReference.Create(this);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (_chartNeedsInit && _treeData != null && _treeData.Nodes.Any())
{
_chartNeedsInit = false;
await RenderChart();
}
}
private async Task LoadTree()
{
if (!_searchUserId.HasValue || _searchUserId.Value <= 0)
{
Snackbar.Add("لطفاً کاربر را انتخاب کنید", Severity.Warning);
return;
}
_isLoading = true;
_hasError = false;
StateHasChanged();
try
{
var request = new GetNetworkTreeRequest
{
UserId = _searchUserId.Value,
MaxDepth = _selectedDepth,
IsClubActive = _clubActiveFilter,
ActivationWeekDefinitionId = _activationWeekFilter?.ToString()
};
_treeData = await NetworkContract.GetNetworkTreeAsync(request);
_currentViewUserId = _searchUserId;
_navigationHistory.Clear();
CalculateStats();
_isLoading = false;
_chartNeedsInit = true;
StateHasChanged();
Snackbar.Add($"درخت بارگذاری شد - {_treeData.Nodes.Count} عضو", Severity.Success);
}
catch (Exception ex)
{
_hasError = true;
_isLoading = false;
Snackbar.Add($"خطا در بارگذاری درخت: {ex.Message}", Severity.Error);
StateHasChanged();
}
}
private async Task LoadSubTree(long userId)
{
_isLoading = true;
StateHasChanged();
try
{
var request = new GetNetworkTreeRequest
{
UserId = userId,
MaxDepth = _selectedDepth,
IsClubActive = _clubActiveFilter,
ActivationWeekDefinitionId = _activationWeekFilter?.ToString()
};
_treeData = await NetworkContract.GetNetworkTreeAsync(request);
if (_currentViewUserId.HasValue && _currentViewUserId != userId)
{
_navigationHistory.Push(_currentViewUserId.Value);
}
_currentViewUserId = userId;
CalculateStats();
_isLoading = false;
_chartNeedsInit = true;
StateHasChanged();
}
catch (Exception ex)
{
_isLoading = false;
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
StateHasChanged();
}
}
private async Task RenderChart()
{
if (_treeData == null || !_treeData.Nodes.Any()) return;
try
{
await Task.Delay(50); // Wait for DOM
var jsNodes = _treeData.Nodes.Select(n => new
{
id = n.UserId.ToString(),
parentId = n.ParentId > 0 ? n.ParentId.ToString() : "",
userId = n.UserId,
userName = n.UserName ?? $"کاربر {n.UserId}",
networkLevel = n.NetworkLevel,
networkLeg = n.NetworkLeg,
isClubActive = n.IsClubActive,
isActivatedInTargetWeek = n.IsActivatedInTargetWeek,
activationWeekDefinitionId = n.ActivationWeekDefinitionId,
clubActivatedAt = n.ClubActivatedAt?.ToDateTime().ToLocalTime().MiladiToJalali() ?? ""
}).ToArray();
var options = new { filterWeek = _activationWeekFilter };
await JS.InvokeVoidAsync("AdminOrgChart.init", "admin-org-chart-container", jsNodes, _dotNetRef, options);
}
catch (Exception ex)
{
}
}
[JSInvokable]
public async Task OnNodeClicked(long userId)
{
if (userId == _currentViewUserId) return;
await LoadSubTree(userId);
}
private void CalculateStats()
{
if (_treeData == null || !_treeData.Nodes.Any())
{
_totalMembers = _leftCount = _rightCount = 0;
return;
}
_totalMembers = _treeData.Nodes.Count;
_leftCount = _treeData.Nodes.Count(n => n.NetworkLeg == 0);
_rightCount = _treeData.Nodes.Count(n => n.NetworkLeg == 1);
}
private async Task ExpandAll()
{
try { await JS.InvokeVoidAsync("AdminOrgChart.expandAll"); } catch { }
}
private async Task CollapseAll()
{
try { await JS.InvokeVoidAsync("AdminOrgChart.collapseAll"); } catch { }
}
private async Task FitChart()
{
try { await JS.InvokeVoidAsync("AdminOrgChart.fit"); } catch { }
}
private async Task ExportPng()
{
try { await JS.InvokeVoidAsync("AdminOrgChart.exportPng"); } catch { }
}
private async Task GoBack()
{
if (_navigationHistory.Count > 0)
{
var previousUserId = _navigationHistory.Pop();
_currentViewUserId = previousUserId;
await LoadSubTree(previousUserId);
}
}
private async Task GoToRoot()
{
if (_searchUserId.HasValue)
{
_navigationHistory.Clear();
await LoadSubTree(_searchUserId.Value);
}
}
private async Task ViewUserTree(long userId)
{
await LoadSubTree(userId);
}
private void ViewUserDetails(long userId)
{
NavigationManager.NavigateTo($"/network/user-info/{userId}");
}
public async ValueTask DisposeAsync()
{
try
{
await JS.InvokeVoidAsync("AdminOrgChart.dispose");
}
catch { }
_dotNetRef?.Dispose();
}
}