Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
@page "/network/tree"
|
||||
@attribute [Authorize]
|
||||
@implements IAsyncDisposable
|
||||
@inject IJSRuntime JS
|
||||
|
||||
@using Foursat.BackOffice.BFF.NetworkMembership.Protos
|
||||
@@ -8,6 +9,7 @@
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-4">
|
||||
<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;">
|
||||
@@ -24,50 +26,97 @@
|
||||
</MudStack>
|
||||
|
||||
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center">
|
||||
<div style="max-width: 200px; min-width: 150px;">
|
||||
<MudSelect T="bool?" Label="وضعیت باشگاه" @bind-Value="_clubActiveFilter" Variant="Variant.Outlined" Clearable="true">
|
||||
<MudSelectItem T="bool?" Value="@(null)">همه</MudSelectItem>
|
||||
<MudSelectItem T="bool?" Value="@(true)">فعال</MudSelectItem>
|
||||
<MudSelectItem T="bool?" Value="@(false)">غیرفعال</MudSelectItem>
|
||||
</MudSelect>
|
||||
</div>
|
||||
<MudSelect T="int" @bind-Value="_selectedDepth" Label="عمق نمایش" Variant="Variant.Outlined"
|
||||
Dense="true" Margin="Margin.Dense" Style="width: 120px;">
|
||||
<MudSelectItem Value="3">3 سطح</MudSelectItem>
|
||||
<MudSelectItem Value="5">5 سطح</MudSelectItem>
|
||||
<MudSelectItem Value="10">10 سطح</MudSelectItem>
|
||||
<MudSelectItem Value="15">15 سطح</MudSelectItem>
|
||||
<MudSelectItem Value="20">همه</MudSelectItem>
|
||||
</MudSelect>
|
||||
|
||||
<div style="max-width: 200px; min-width: 150px;">
|
||||
<WeekNumberPicker Label="هفته فعالسازی"
|
||||
@bind-SelectedWeekDefinitionId="_activationWeekFilter" />
|
||||
</div>
|
||||
<MudSpacer />
|
||||
<MudStack Row="true" Spacing="2">
|
||||
<MudChip T="string" Color="Color.Info" Size="Size.Small">
|
||||
کل اعضا: @_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>
|
||||
<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 justify-center">
|
||||
<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-4" Style="min-height: 800px;">
|
||||
<div id="network-tree-container" style="width: 100%; height: 800px; overflow: auto;"></div>
|
||||
<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">
|
||||
<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.UserId" Title="شناسه" />
|
||||
<PropertyColumn Property="x => x.UserName" Title="نام کاربر" />
|
||||
|
||||
<PropertyColumn Property="x => x.NetworkLeg" Title="موقعیت">
|
||||
@@ -82,56 +131,17 @@
|
||||
|
||||
<PropertyColumn Property="x => x.NetworkLevel" Title="سطح" />
|
||||
|
||||
<PropertyColumn Property="x => x.IsClubActive" Title="وضعیت">
|
||||
<PropertyColumn Property="x => x.IsClubActive" Title="باشگاه">
|
||||
<CellTemplate>
|
||||
<MudChip T="string"
|
||||
Color="@(context.Item.IsClubActive ? Color.Success : Color.Error)"
|
||||
Color="@(context.Item.IsClubActive ? Color.Success : Color.Default)"
|
||||
Size="Size.Small">
|
||||
@(context.Item.IsClubActive ? "فعال" : "غیرفعال")
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.JoinedAt" Title="تاریخ عضویت">
|
||||
<CellTemplate>
|
||||
@if (context.Item.JoinedAt != null)
|
||||
{
|
||||
@context.Item.JoinedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd")
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.body2" Color="Color.Default">-</MudText>
|
||||
}
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.IsClubActive" Title="باشگاه">
|
||||
<CellTemplate>
|
||||
@if (context.Item.IsClubActive)
|
||||
{
|
||||
<MudChip T="string" Color="Color.Success" Size="Size.Small">فعال</MudChip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip T="string" Color="Color.Default" Size="Size.Small">غیرفعال</MudChip>
|
||||
}
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.ActivationWeekDefinitionId" Title="هفته فعالسازی">
|
||||
<CellTemplate>
|
||||
@if (context.Item.ActivationWeekDefinitionId != null)
|
||||
{
|
||||
<MudText Typo="Typo.body2">@context.Item.ActivationWeekDefinitionId</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.body2" Color="Color.Default">-</MudText>
|
||||
}
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
|
||||
<PropertyColumn Property="x => x.ClubActivatedAt" Title="تاریخ فعالسازی باشگاه">
|
||||
<PropertyColumn Property="x => x.ClubActivatedAt" Title="تاریخ فعالسازی">
|
||||
<CellTemplate>
|
||||
@if (context.Item.ClubActivatedAt != null)
|
||||
{
|
||||
@@ -139,30 +149,52 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.body2" Color="Color.Default">-</MudText>
|
||||
<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>
|
||||
<MudButton Size="Size.Small"
|
||||
Variant="Variant.Text"
|
||||
Color="Color.Primary"
|
||||
OnClick="@(() => ViewUserDetails(context.Item.UserId))">
|
||||
جزئیات
|
||||
</MudButton>
|
||||
<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>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="NetworkTreeNodeModel" />
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudPaper>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Class="pa-8">
|
||||
<MudAlert Severity="Severity.Info">
|
||||
برای نمایش درخت شبکه، شناسه کاربر را وارد کنید و دکمه "نمایش درخت" را بزنید.
|
||||
<MudAlert Severity="Severity.Info" Icon="@Icons.Material.Filled.Info">
|
||||
برای نمایش درخت شبکه، کاربر مورد نظر را جستجو کرده و دکمه "نمایش درخت" را بزنید.
|
||||
</MudAlert>
|
||||
</MudPaper>
|
||||
}
|
||||
@@ -173,14 +205,19 @@
|
||||
[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 DotNetObjectReference<NetworkTreeViewer> _dotNetRef;
|
||||
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()
|
||||
{
|
||||
@@ -189,9 +226,10 @@
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
if (_chartNeedsInit && _treeData != null && _treeData.Nodes.Any())
|
||||
{
|
||||
await JS.InvokeVoidAsync("NetworkTreeViewer.setDotNetReference", _dotNetRef);
|
||||
_chartNeedsInit = false;
|
||||
await RenderChart();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,81 +242,186 @@
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
StateHasChanged(); // Force render to show loading state
|
||||
_hasError = false;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
var request = new GetNetworkTreeRequest
|
||||
{
|
||||
UserId = _searchUserId.Value,
|
||||
MaxDepth = 20,
|
||||
IsClubActive = _clubActiveFilter.HasValue ? _clubActiveFilter.Value : null,
|
||||
ActivationWeekDefinitionId = _activationWeekFilter!=null ? _activationWeekFilter : null
|
||||
MaxDepth = _selectedDepth,
|
||||
IsClubActive = _clubActiveFilter,
|
||||
ActivationWeekDefinitionId = _activationWeekFilter
|
||||
};
|
||||
|
||||
_treeData = await NetworkContract.GetNetworkTreeAsync(request);
|
||||
_currentViewUserId = _searchUserId;
|
||||
_navigationHistory.Clear();
|
||||
|
||||
CalculateStats();
|
||||
|
||||
_isLoading = false;
|
||||
StateHasChanged(); // Render the container first
|
||||
|
||||
await Task.Delay(100); // Wait for DOM to be ready
|
||||
await RenderTree();
|
||||
_chartNeedsInit = true;
|
||||
StateHasChanged();
|
||||
|
||||
Snackbar.Add($"درخت بارگذاری شد - {_treeData.Nodes.Count} عضو", Severity.Success);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"خطا در بارگذاری درخت: {ex.Message}", Severity.Error);
|
||||
_hasError = true;
|
||||
_isLoading = false;
|
||||
Snackbar.Add($"خطا در بارگذاری درخت: {ex.Message}", Severity.Error);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RenderTree()
|
||||
private async Task LoadSubTree(long userId)
|
||||
{
|
||||
_isLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
var request = new GetNetworkTreeRequest
|
||||
{
|
||||
UserId = userId,
|
||||
MaxDepth = _selectedDepth,
|
||||
IsClubActive = _clubActiveFilter,
|
||||
ActivationWeekDefinitionId = _activationWeekFilter
|
||||
};
|
||||
|
||||
_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;
|
||||
|
||||
var jsNodes = _treeData.Nodes.Select(n => new
|
||||
try
|
||||
{
|
||||
userId = n.UserId,
|
||||
userName = n.UserName,
|
||||
parentId = n.ParentId,
|
||||
networkLevel = n.NetworkLevel,
|
||||
networkLeg = n.NetworkLeg,
|
||||
isActive = n.IsClubActive,
|
||||
isClubActive = n.IsClubActive,
|
||||
isActivatedInTargetWeek = n.IsActivatedInTargetWeek,
|
||||
activationWeekNumber = _activationWeekFilter ?? 0, // فیلتر UI
|
||||
clubActivatedAt = n.ClubActivatedAt?.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd") ?? "",
|
||||
userCreated = n.UserCreated?.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd") ?? ""
|
||||
}).ToArray();
|
||||
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().ToString("yyyy/MM/dd") ?? ""
|
||||
}).ToArray();
|
||||
|
||||
await JS.InvokeVoidAsync("NetworkTreeViewer.initialize", "network-tree-container", jsNodes);
|
||||
var options = new { filterWeek = _activationWeekFilter };
|
||||
|
||||
await JS.InvokeVoidAsync("AdminOrgChart.init", "admin-org-chart-container", jsNodes, _dotNetRef, options);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error rendering chart: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public async Task OnNodeClicked(long userId)
|
||||
{
|
||||
_searchUserId = userId;
|
||||
await LoadTree();
|
||||
if (userId == _currentViewUserId) return;
|
||||
await LoadSubTree(userId);
|
||||
}
|
||||
|
||||
private void CalculateStats()
|
||||
{
|
||||
if (_treeData == null || !_treeData.Nodes.Any()) return;
|
||||
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 void Dispose()
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await JS.InvokeVoidAsync("AdminOrgChart.dispose");
|
||||
}
|
||||
catch { }
|
||||
|
||||
_dotNetRef?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
/* Admin Org Chart Styles */
|
||||
|
||||
.admin-org-chart-container {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
background: #fafafa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Node Card */
|
||||
.admin-node-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
border: 2px solid #e0e0e0;
|
||||
min-width: 140px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.admin-node-card:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Position colors */
|
||||
.admin-node-card.leg-left {
|
||||
border-left: 4px solid #4caf50;
|
||||
}
|
||||
|
||||
.admin-node-card.leg-right {
|
||||
border-left: 4px solid #ff9800;
|
||||
}
|
||||
|
||||
.admin-node-card.leg-root {
|
||||
border-left: 4px solid #1976d2;
|
||||
}
|
||||
|
||||
/* Club status */
|
||||
.admin-node-card.club-active {
|
||||
border-top: 2px solid #4caf50;
|
||||
}
|
||||
|
||||
.admin-node-card.club-inactive {
|
||||
border-top: 2px solid #e0e0e0;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* Node header */
|
||||
.admin-node-card .node-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.admin-node-card .node-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #1976d2 0%, #42a5f5 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.admin-node-card.leg-left .node-avatar {
|
||||
background: linear-gradient(135deg, #388e3c 0%, #66bb6a 100%);
|
||||
}
|
||||
|
||||
.admin-node-card.leg-right .node-avatar {
|
||||
background: linear-gradient(135deg, #f57c00 0%, #ffb74d 100%);
|
||||
}
|
||||
|
||||
.admin-node-card .node-main-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-node-card .node-name {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.admin-node-card .node-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.admin-node-card .level-badge {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.admin-node-card .leg-text-left {
|
||||
color: #4caf50;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.admin-node-card .leg-text-right {
|
||||
color: #ff9800;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Node footer */
|
||||
.admin-node-card .node-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 6px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.admin-node-card .club-status {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.admin-node-card .club-status.club-active {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.admin-node-card .club-status.club-inactive {
|
||||
color: #9e9e9e;
|
||||
}
|
||||
|
||||
.admin-node-card .activation-date {
|
||||
color: #757575;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
/* Week badge */
|
||||
.admin-node-card .week-badge {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
font-size: 9px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.admin-node-card .week-badge.week-match {
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.admin-node-card .week-badge.week-other {
|
||||
background: #ff5722;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Expand button */
|
||||
.admin-expand-btn {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #1976d2;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.admin-expand-btn:hover {
|
||||
background: #1565c0;
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
.admin-org-chart-container .no-data-message,
|
||||
.admin-org-chart-container .error-message {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* RTL support */
|
||||
[dir="rtl"] .admin-node-card {
|
||||
border-left: none;
|
||||
border-right: 4px solid #e0e0e0;
|
||||
}
|
||||
|
||||
[dir="rtl"] .admin-node-card.leg-left {
|
||||
border-right-color: #4caf50;
|
||||
}
|
||||
|
||||
[dir="rtl"] .admin-node-card.leg-right {
|
||||
border-right-color: #ff9800;
|
||||
}
|
||||
|
||||
[dir="rtl"] .admin-node-card.leg-root {
|
||||
border-right-color: #1976d2;
|
||||
}
|
||||
|
||||
[dir="rtl"] .admin-node-card .week-badge {
|
||||
right: auto;
|
||||
left: -8px;
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
|
||||
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
|
||||
<link href="_content/Tizzani.MudBlazor.HtmlEditor/MudHtmlEditor.css" rel="stylesheet" />
|
||||
<link href="css/admin-org-chart.css" rel="stylesheet" />
|
||||
|
||||
<!-- If you add any scoped CSS files, uncomment the following to load them
|
||||
<link href="BackOffice.styles.css" rel="stylesheet" /> -->
|
||||
@@ -35,6 +36,9 @@
|
||||
</div>
|
||||
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||
<script src="/js/d3.v7.min.js"></script>
|
||||
<script src="/js/d3-flextree.min.js"></script>
|
||||
<script src="/js/d3-org-chart3.js"></script>
|
||||
<script src="/js/admin-org-chart.js"></script>
|
||||
<script src="/js/main.js"></script>
|
||||
<script src="/js/quill.js"></script>
|
||||
<script src="/js/network-tree.js"></script>
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* d3-org-chart integration for BackOffice Admin
|
||||
* Network Binary Tree visualization with admin features
|
||||
*/
|
||||
|
||||
window.AdminOrgChart = {
|
||||
chart: null,
|
||||
dotNetHelper: null,
|
||||
containerId: null,
|
||||
|
||||
/**
|
||||
* Initialize the organization chart
|
||||
* @param {string} containerId - The ID of the container element
|
||||
* @param {object} data - The tree data in flat array format
|
||||
* @param {object} dotNetHelper - Blazor .NET helper for callbacks
|
||||
* @param {object} options - Chart options (filterWeek, etc.)
|
||||
*/
|
||||
init: function (containerId, data, dotNetHelper, options = {}) {
|
||||
this.dotNetHelper = dotNetHelper;
|
||||
this.containerId = containerId;
|
||||
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) {
|
||||
console.error('AdminOrgChart: Container not found:', containerId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear previous chart
|
||||
container.innerHTML = '';
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
container.innerHTML = '<div class="no-data-message" style="padding: 40px; text-align: center; color: #666;">دادهای برای نمایش وجود ندارد</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const filterWeek = options.filterWeek || null;
|
||||
|
||||
this.chart = new d3.OrgChart()
|
||||
.container('#' + containerId)
|
||||
.data(data)
|
||||
.nodeWidth((d) => 160)
|
||||
.nodeHeight((d) => 80)
|
||||
.childrenMargin((d) => 60)
|
||||
.compactMarginBetween((d) => 20)
|
||||
.compactMarginPair((d) => 20)
|
||||
.neighbourMargin((a, b) => 20)
|
||||
.siblingsMargin((d) => 20)
|
||||
.buttonContent(({ node, state }) => {
|
||||
const hasChildren = node.data._directSubordinates > 0;
|
||||
const isExpanded = node.children;
|
||||
return hasChildren ? `<div class="admin-expand-btn">
|
||||
<span>${isExpanded ? '−' : '+'}</span>
|
||||
</div>` : '';
|
||||
})
|
||||
.linkUpdate(function (d, i, arr) {
|
||||
d3.select(this)
|
||||
.attr('stroke', (d) => d.data._highlighted || d.data._upToTheRootHighlighted ? '#1976d2' : '#bdbdbd')
|
||||
.attr('stroke-width', (d) => d.data._highlighted || d.data._upToTheRootHighlighted ? 3 : 2);
|
||||
})
|
||||
.nodeContent(function (d, i, arr, state) {
|
||||
const data = d.data;
|
||||
const isRoot = !data.parentId || data.parentId === '';
|
||||
|
||||
// Position styling
|
||||
const positionClass = data.networkLeg === 0 ? 'leg-left' :
|
||||
data.networkLeg === 1 ? 'leg-right' : 'leg-root';
|
||||
|
||||
// Club status
|
||||
const clubClass = data.isClubActive ? 'club-active' : 'club-inactive';
|
||||
|
||||
// Week activation status (if filtering by week)
|
||||
let weekIndicator = '';
|
||||
if (filterWeek && data.activationWeekDefinitionId) {
|
||||
const isTargetWeek = data.isActivatedInTargetWeek;
|
||||
weekIndicator = `<div class="week-badge ${isTargetWeek ? 'week-match' : 'week-other'}">
|
||||
W${data.activationWeekDefinitionId}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Avatar - first letter of name
|
||||
const firstChar = data.userName ? data.userName.charAt(0).toUpperCase() : '?';
|
||||
|
||||
// Level badge
|
||||
const levelBadge = `<span class="level-badge">L${data.networkLevel || 0}</span>`;
|
||||
|
||||
// Leg indicator
|
||||
const legText = isRoot ? '' : (data.networkLeg === 0 ? 'چپ' : 'راست');
|
||||
const legClass = data.networkLeg === 0 ? 'leg-text-left' : 'leg-text-right';
|
||||
|
||||
// Build tooltip text
|
||||
const tooltipLines = [
|
||||
`👤 ${data.userName || 'کاربر ' + data.userId}`,
|
||||
`🆔 شناسه: ${data.userId}`,
|
||||
`📊 سطح: ${data.networkLevel || 0}`,
|
||||
`${data.networkLeg === 0 ? '⬅️' : '➡️'} موقعیت: ${data.networkLeg === 0 ? 'چپ' : 'راست'}`,
|
||||
`${data.isClubActive ? '✅' : '❌'} باشگاه: ${data.isClubActive ? 'فعال' : 'غیرفعال'}`
|
||||
];
|
||||
|
||||
if (data.clubActivatedAt) {
|
||||
tooltipLines.push(`📅 فعالسازی: ${data.clubActivatedAt}`);
|
||||
}
|
||||
if (data.activationWeekDefinitionId) {
|
||||
tooltipLines.push(`📆 هفته: ${data.activationWeekDefinitionId}`);
|
||||
}
|
||||
|
||||
const tooltipText = tooltipLines.join(' ');
|
||||
|
||||
return `
|
||||
<div class="admin-node-card ${positionClass} ${clubClass}"
|
||||
data-user-id="${data.userId}"
|
||||
title="${tooltipText}">
|
||||
${weekIndicator}
|
||||
<div class="node-header">
|
||||
<div class="node-avatar">${firstChar}</div>
|
||||
<div class="node-main-info">
|
||||
<div class="node-name">${data.userName || 'کاربر ' + data.userId}</div>
|
||||
<div class="node-meta">
|
||||
${levelBadge}
|
||||
${legText ? `<span class="${legClass}">${legText}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-footer">
|
||||
<span class="club-status ${clubClass}">
|
||||
${data.isClubActive ? '✓ فعال' : '✗ غیرفعال'}
|
||||
</span>
|
||||
${data.clubActivatedAt ? `<span class="activation-date">${data.clubActivatedAt}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.onNodeClick((d) => {
|
||||
if (this.dotNetHelper) {
|
||||
const userId = parseInt(d.data.userId, 10);
|
||||
this.dotNetHelper.invokeMethodAsync('OnNodeClicked', userId);
|
||||
}
|
||||
})
|
||||
.render();
|
||||
|
||||
// Initial fit
|
||||
setTimeout(() => {
|
||||
if (this.chart) {
|
||||
this.chart.fit();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
} catch (error) {
|
||||
console.error('AdminOrgChart: Error initializing chart:', error);
|
||||
container.innerHTML = '<div class="error-message" style="padding: 40px; text-align: center; color: #f44336;">خطا در بارگذاری نمودار</div>';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the chart with new data
|
||||
*/
|
||||
update: function (data, options = {}) {
|
||||
if (this.chart && this.containerId) {
|
||||
// Re-init with new data
|
||||
this.init(this.containerId, data, this.dotNetHelper, options);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Expand all nodes
|
||||
*/
|
||||
expandAll: function () {
|
||||
if (this.chart) {
|
||||
this.chart.expandAll().render();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Collapse all nodes
|
||||
*/
|
||||
collapseAll: function () {
|
||||
if (this.chart) {
|
||||
this.chart.collapseAll().render();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Center/Fit the chart
|
||||
*/
|
||||
fit: function () {
|
||||
if (this.chart) {
|
||||
this.chart.fit();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Zoom to specific node
|
||||
*/
|
||||
zoomToNode: function (nodeId) {
|
||||
if (this.chart) {
|
||||
this.chart.setCentered(nodeId.toString()).render();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Export chart as PNG
|
||||
*/
|
||||
exportPng: function () {
|
||||
if (this.chart) {
|
||||
this.chart.exportImg({ full: true });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Export chart as SVG
|
||||
*/
|
||||
exportSvg: function () {
|
||||
if (this.chart) {
|
||||
this.chart.exportSvg();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Dispose the chart
|
||||
*/
|
||||
dispose: function () {
|
||||
if (this.chart) {
|
||||
this.chart = null;
|
||||
this.dotNetHelper = null;
|
||||
this.containerId = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
+69
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user