feat: integrate d3-org-chart for network visualization and add SignalR token notification service
This commit is contained in:
@@ -1,120 +1,262 @@
|
||||
using FrontOffice.BFF.User.Protobuf.Protos.User;
|
||||
using Mapster;
|
||||
using FrontOffice.Main.Utilities;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using static FrontOffice.Main.Pages.Profile.Components.OrganizationChartLevel;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace FrontOffice.Main.Pages.Profile.Components;
|
||||
public partial class OrganizationChart
|
||||
{
|
||||
private UserNode? _currentUser;
|
||||
private bool _isExpanded;
|
||||
|
||||
[Inject] private UserContract.UserContractClient UserContract { get; set; } = default!;
|
||||
private GetUserResponse _userProfile = new();
|
||||
public partial class OrganizationChart : IAsyncDisposable
|
||||
{
|
||||
private NetworkTreeDto? _networkTree;
|
||||
private NetworkStatisticsDto? _statistics;
|
||||
private bool _isLoading = true;
|
||||
private bool _hasError = false;
|
||||
private int _selectedDepthValue = 3;
|
||||
private DotNetObjectReference<OrganizationChart>? _dotNetHelper;
|
||||
private bool _chartNeedsInit = false;
|
||||
private bool _jsReady = false;
|
||||
|
||||
// برای نگهداری شناسه کاربر فعلی که درختش نمایش داده شده
|
||||
private long? _currentViewUserId = null;
|
||||
// Stack برای بازگشت به عقب
|
||||
private Stack<long> _navigationHistory = new();
|
||||
|
||||
private int _selectedDepth
|
||||
{
|
||||
get => _selectedDepthValue;
|
||||
set
|
||||
{
|
||||
if (_selectedDepthValue != value)
|
||||
{
|
||||
_selectedDepthValue = value;
|
||||
_ = OnDepthChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Inject] private NetworkMembershipService NetworkService { get; set; } = default!;
|
||||
// JSRuntime is injected globally via _Imports.razor;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadCurrentUser();
|
||||
await LoadData();
|
||||
}
|
||||
private async Task LoadUserProfile()
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
_jsReady = true;
|
||||
}
|
||||
|
||||
// Initialize chart after JS is ready and data is loaded
|
||||
if (_jsReady && _chartNeedsInit && _networkTree?.RootNode != null)
|
||||
{
|
||||
_chartNeedsInit = false;
|
||||
await InitializeChart();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadData()
|
||||
{
|
||||
_isLoading = true;
|
||||
_hasError = false;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
_userProfile = await UserContract.GetUserAsync(request: new());
|
||||
// Load tree and statistics in parallel
|
||||
var treeTask = NetworkService.GetMyNetworkTreeAsync(_selectedDepth);
|
||||
var statsTask = NetworkService.GetMyNetworkStatisticsAsync();
|
||||
|
||||
await Task.WhenAll(treeTask, statsTask);
|
||||
|
||||
_networkTree = await treeTask;
|
||||
_statistics = await statsTask;
|
||||
|
||||
// Mark that chart needs to be initialized
|
||||
_chartNeedsInit = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle the case when user is not authenticated or API fails
|
||||
_userProfile = new GetUserResponse();
|
||||
Console.WriteLine($"Error loading network data: {ex.Message}");
|
||||
_hasError = true;
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
private async Task LoadCurrentUser()
|
||||
{
|
||||
await LoadUserProfile();
|
||||
|
||||
|
||||
// Mock data - replace with actual API call
|
||||
_currentUser = new UserNode
|
||||
finally
|
||||
{
|
||||
Id = _userProfile.Id,
|
||||
FirstName = _userProfile.FirstName,
|
||||
LastName = _userProfile.LastName,
|
||||
Mobile = _userProfile.Mobile,
|
||||
Avatar = _userProfile.AvatarPath,
|
||||
PersonalPurchase = 0,
|
||||
TeamPurchase = 0,
|
||||
Children = await GetUserChildren(userId: _userProfile.Id)
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<List<UserNode>?> GetUserChildren(long userId)
|
||||
{
|
||||
Console.WriteLine("OK0");
|
||||
if (userId != 0)
|
||||
{
|
||||
Console.WriteLine("OK1");
|
||||
var children = await UserContract.GetAllUserByFilterAsync(request: new()
|
||||
{
|
||||
Filter = new()
|
||||
{
|
||||
ParentId = userId,
|
||||
}
|
||||
});
|
||||
if (children?.Models?.Any() == true)
|
||||
{
|
||||
Console.WriteLine("OK2");
|
||||
var result = new List<UserNode>();
|
||||
foreach (var item in children.Models)
|
||||
{
|
||||
var node = new UserNode
|
||||
{
|
||||
Id = item.Id,
|
||||
FirstName = item.FirstName,
|
||||
LastName = item.LastName,
|
||||
Mobile = item.Mobile,
|
||||
Avatar = item.AvatarPath,
|
||||
ReferralCode = item.ReferralCode,
|
||||
PersonalPurchase = 0,
|
||||
TeamPurchase = 0,
|
||||
Children = await GetUserChildren(userId: item.Id)
|
||||
};
|
||||
result.Add(node);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private void ToggleExpand()
|
||||
{
|
||||
_isExpanded = !_isExpanded;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public void ToggleNodeExpand(long userId)
|
||||
{
|
||||
var node = FindNode(_currentUser, userId);
|
||||
if (node != null)
|
||||
{
|
||||
node.IsExpanded = !node.IsExpanded;
|
||||
_isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private UserNode? FindNode(UserNode? node, long userId)
|
||||
private async Task InitializeChart()
|
||||
{
|
||||
if (node == null) return null;
|
||||
if (node.Id == userId) return node;
|
||||
if (_networkTree?.RootNode == null) return;
|
||||
|
||||
if (node.Children != null)
|
||||
try
|
||||
{
|
||||
foreach (var child in node.Children)
|
||||
_dotNetHelper = DotNetObjectReference.Create(this);
|
||||
var flatData = _networkTree.ToFlatArray();
|
||||
|
||||
await JSRuntime.InvokeVoidAsync("OrgChart.init", "org-chart-container", flatData, _dotNetHelper);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error initializing chart: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshData()
|
||||
{
|
||||
await LoadData();
|
||||
|
||||
if (_networkTree?.RootNode != null)
|
||||
{
|
||||
await InitializeChart();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnDepthChanged()
|
||||
{
|
||||
await RefreshData();
|
||||
}
|
||||
|
||||
private async Task ExpandAll()
|
||||
{
|
||||
try
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("OrgChart.expandAll");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private async Task CollapseAll()
|
||||
{
|
||||
try
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("OrgChart.collapseAll");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private async Task CenterChart()
|
||||
{
|
||||
try
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("OrgChart.center");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private async Task FitToScreen()
|
||||
{
|
||||
try
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("OrgChart.fitToScreen");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private async Task ExportPng()
|
||||
{
|
||||
try
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("OrgChart.exportPng");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called from JavaScript when a node is clicked
|
||||
/// </summary>
|
||||
[JSInvokable]
|
||||
public async Task OnNodeClicked(long userId)
|
||||
{
|
||||
// اگر روی همان نود کلیک شده، کاری نکن
|
||||
if (_currentViewUserId == userId) return;
|
||||
|
||||
// ذخیره نود فعلی در history برای بازگشت
|
||||
if (_currentViewUserId.HasValue)
|
||||
{
|
||||
_navigationHistory.Push(_currentViewUserId.Value);
|
||||
}
|
||||
|
||||
await LoadSubordinateTree(userId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// بارگذاری درخت یک زیرمجموعه
|
||||
/// </summary>
|
||||
private async Task LoadSubordinateTree(long targetUserId)
|
||||
{
|
||||
_isLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
_networkTree = await NetworkService.GetSubordinateTreeAsync(targetUserId, _selectedDepth);
|
||||
_currentViewUserId = targetUserId;
|
||||
_chartNeedsInit = true;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
// نمایش پیام خطا - این کاربر زیرمجموعه نیست
|
||||
Console.WriteLine("Unauthorized access to subordinate tree");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error loading subordinate tree: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// بازگشت به درخت قبلی
|
||||
/// </summary>
|
||||
private async Task GoBack()
|
||||
{
|
||||
if (_navigationHistory.Count > 0)
|
||||
{
|
||||
var previousUserId = _navigationHistory.Pop();
|
||||
await LoadSubordinateTree(previousUserId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// بازگشت به درخت خود کاربر
|
||||
_currentViewUserId = null;
|
||||
await LoadData();
|
||||
if (_networkTree?.RootNode != null)
|
||||
{
|
||||
var found = FindNode(child, userId);
|
||||
if (found != null) return found;
|
||||
await InitializeChart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
/// <summary>
|
||||
/// بازگشت به درخت اصلی (خود کاربر)
|
||||
/// </summary>
|
||||
private async Task GoToMyTree()
|
||||
{
|
||||
_navigationHistory.Clear();
|
||||
_currentViewUserId = null;
|
||||
await LoadData();
|
||||
if (_networkTree?.RootNode != null)
|
||||
{
|
||||
await InitializeChart();
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("OrgChart.dispose");
|
||||
}
|
||||
catch { }
|
||||
|
||||
_dotNetHelper?.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user