diff --git a/src/FrontOffice.Main/ConfigureServices.cs b/src/FrontOffice.Main/ConfigureServices.cs index dd24665..7d9ae68 100644 --- a/src/FrontOffice.Main/ConfigureServices.cs +++ b/src/FrontOffice.Main/ConfigureServices.cs @@ -62,6 +62,9 @@ public static class ConfigureServices services.AddTransient(); // PDF generation (Chromium only) services.AddSingleton(); + + // SignalR Token Notification Service + services.AddScoped(); return services; } diff --git a/src/FrontOffice.Main/FrontOffice.Main.csproj b/src/FrontOffice.Main/FrontOffice.Main.csproj index baadf84..0515eae 100644 --- a/src/FrontOffice.Main/FrontOffice.Main.csproj +++ b/src/FrontOffice.Main/FrontOffice.Main.csproj @@ -13,12 +13,12 @@ - + - + @@ -32,6 +32,7 @@ + @@ -39,6 +40,8 @@ + + @@ -60,6 +63,12 @@ .dockerignore + + + + + + diff --git a/src/FrontOffice.Main/Pages/Index.razor.cs b/src/FrontOffice.Main/Pages/Index.razor.cs index 4fe5f36..2c57942 100644 --- a/src/FrontOffice.Main/Pages/Index.razor.cs +++ b/src/FrontOffice.Main/Pages/Index.razor.cs @@ -21,7 +21,7 @@ public partial class Index:IDisposable { await InvokeAsync(StateHasChanged); }; - await LoadPackagesAsync(); + // await LoadPackagesAsync(); //string mobileNumber = "09387342688"; @@ -50,37 +50,37 @@ public partial class Index:IDisposable await base.OnAfterRenderAsync(firstRender); } - private async Task LoadPackagesAsync() - { - _isLoadingPackages = true; - try - { - var response = await PackageClient.GetAllPackageByFilterAsync(request: new()); - if (response?.Models?.Any() == true) - { - _packs = response.Models.Select(p => new Pack( - Id: p.Id, - Title: p.Title, - Body: p.Description, - Image: UrlUtility.DownloadUrl + p.ImagePath, - Price: p.Price - )).ToList(); - } - else - _packs = new List(); - } - catch (Exception ex) - { - Snackbar.Add($"خطا در بارگذاری پکیج‌ها: {ex.Message}", Severity.Error); - // Fallback to empty list - _packs = new List(); - } - finally - { - _isLoadingPackages = false; - await InvokeAsync(StateHasChanged); - } - } + // private async Task LoadPackagesAsync() + // { + // _isLoadingPackages = true; + // try + // { + // var response = await PackageClient.GetAllPackageByFilterAsync(request: new()); + // if (response?.Models?.Any() == true) + // { + // _packs = response.Models.Select(p => new Pack( + // Id: p.Id, + // Title: p.Title, + // Body: p.Description, + // Image: UrlUtility.DownloadUrl + p.ImagePath, + // Price: p.Price + // )).ToList(); + // } + // else + // _packs = new List(); + // } + // catch (Exception ex) + // { + // Snackbar.Add($"خطا در بارگذاری پکیج‌ها: {ex.Message}", Severity.Error); + // // Fallback to empty list + // _packs = new List(); + // } + // finally + // { + // _isLoadingPackages = false; + // await InvokeAsync(StateHasChanged); + // } + // } private void JoinWaitlist() { diff --git a/src/FrontOffice.Main/Pages/Profile/Components/AddAddressDialog.razor.cs b/src/FrontOffice.Main/Pages/Profile/Components/AddAddressDialog.razor.cs index 7664d36..a092d06 100644 --- a/src/FrontOffice.Main/Pages/Profile/Components/AddAddressDialog.razor.cs +++ b/src/FrontOffice.Main/Pages/Profile/Components/AddAddressDialog.razor.cs @@ -29,7 +29,7 @@ public partial class AddAddressDialog : ComponentBase var response = await CityContract.GetAllCitiesByFilterAsync(new CityProto.GetAllCitiesByFilterRequest { PaginationState = new CityProto.PaginationState { PageNumber = 1, PageSize = 20 }, - Filter = new CityProto.GetAllCitiesByFilterFilter { Native = value } + Filter = new CityProto.GetAllCitiesByFilterFilter { Name = value } }, cancellationToken: ct); return response?.Models?.ToList() ?? new List(); diff --git a/src/FrontOffice.Main/Pages/Profile/Components/OrganizationChart.razor b/src/FrontOffice.Main/Pages/Profile/Components/OrganizationChart.razor index 38ea96f..1068a79 100644 --- a/src/FrontOffice.Main/Pages/Profile/Components/OrganizationChart.razor +++ b/src/FrontOffice.Main/Pages/Profile/Components/OrganizationChart.razor @@ -1,62 +1,82 @@ -@if (_currentUser != null) -{ -
-
-
-
-
- @if (!string.IsNullOrWhiteSpace(_currentUser.Avatar)) - { - - - - } - else - { - - @(string.IsNullOrWhiteSpace(_currentUser.FirstName) ? "N" : _currentUser.FirstName.Substring(0, 1)) - - } +@implements IAsyncDisposable - @if (_currentUser?.Children?.Any() == true) - { - - } -
-
- @if (!string.IsNullOrWhiteSpace(_currentUser.FirstName) || !string.IsNullOrWhiteSpace(_currentUser.LastName)) - { -
@_currentUser.FirstName @_currentUser.LastName
- } - else - { -
@_currentUser.Mobile
- } - @*
-
- خرید شخصی: - @(_currentUser?.PersonalPurchase?.ToThousands().ToCurrencyUnitIRT() ?? "0 تومان") -
-
- خرید تیمی: - @(_currentUser?.TeamPurchase?.ToThousands().ToCurrencyUnitIRT() ?? "0 تومان") -
-
*@ -
-
-
- - @if (_isExpanded && _currentUser?.Children?.Any() == true) - { - - - - } -
+
+ @* Toolbar *@ +
+ + + + 2 سطح + 3 سطح + 4 سطح + 5 سطح + 6 سطح + همه + + + + + + + + + + @if (_currentViewUserId.HasValue) + { + + + + + }
-} + + @* Chart Container *@ + @if (_isLoading) + { +
+ + در حال بارگذاری... +
+ } + else if (_hasError) + { +
+ + خطا در بارگذاری داده‌ها + تلاش مجدد +
+ } + else + { +
+ } + + @* Stats Bar *@ + @if (_statistics != null && !_isLoading) + { +
+
+ کل اعضا: + @_statistics.TotalMembers +
+
+ پای چپ: + @_statistics.LeftLegCount +
+
+ پای راست: + @_statistics.RightLegCount +
+
+ عمق: + @_statistics.TreeDepth +
+
+ پای ضعیف: + @(_statistics.WeakerLeg == "Left" ? "چپ" : "راست") +
+
+ } +
diff --git a/src/FrontOffice.Main/Pages/Profile/Components/OrganizationChart.razor.cs b/src/FrontOffice.Main/Pages/Profile/Components/OrganizationChart.razor.cs index d64ef65..a7ab481 100644 --- a/src/FrontOffice.Main/Pages/Profile/Components/OrganizationChart.razor.cs +++ b/src/FrontOffice.Main/Pages/Profile/Components/OrganizationChart.razor.cs @@ -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? _dotNetHelper; + private bool _chartNeedsInit = false; + private bool _jsReady = false; + + // برای نگهداری شناسه کاربر فعلی که درختش نمایش داده شده + private long? _currentViewUserId = null; + // Stack برای بازگشت به عقب + private Stack _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?> 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(); - 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 { } + } + + /// + /// Called from JavaScript when a node is clicked + /// + [JSInvokable] + public async Task OnNodeClicked(long userId) + { + // اگر روی همان نود کلیک شده، کاری نکن + if (_currentViewUserId == userId) return; + + // ذخیره نود فعلی در history برای بازگشت + if (_currentViewUserId.HasValue) + { + _navigationHistory.Push(_currentViewUserId.Value); + } + + await LoadSubordinateTree(userId); + } + + /// + /// بارگذاری درخت یک زیرمجموعه + /// + 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(); + } + } + + /// + /// بازگشت به درخت قبلی + /// + 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; + /// + /// بازگشت به درخت اصلی (خود کاربر) + /// + 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(); } } \ No newline at end of file diff --git a/src/FrontOffice.Main/Pages/Profile/Components/OrganizationChartLevel.razor.cs b/src/FrontOffice.Main/Pages/Profile/Components/OrganizationChartLevel.razor.cs index 579fd09..0ea0720 100644 --- a/src/FrontOffice.Main/Pages/Profile/Components/OrganizationChartLevel.razor.cs +++ b/src/FrontOffice.Main/Pages/Profile/Components/OrganizationChartLevel.razor.cs @@ -4,6 +4,12 @@ using MudBlazor; using FrontOffice.Main.Utilities; namespace FrontOffice.Main.Pages.Profile.Components; + +/// +/// DEPRECATED: This component is no longer used. +/// d3-org-chart is now used for tree visualization. +/// Kept for backward compatibility. +/// public partial class OrganizationChartLevel { [Parameter] public List? Nodes { get; set; } @@ -25,18 +31,12 @@ public partial class OrganizationChartLevel private void ToggleNodeExpand(long userId) { - if (ParentChart != null) - { - ParentChart.ToggleNodeExpand(userId); - } + // No longer needed - d3-org-chart handles this internally } private void SafeToggleNodeExpand(long userId) { - if (ParentChart != null) - { - ParentChart.ToggleNodeExpand(userId); - } + // No longer needed - d3-org-chart handles this internally } private async Task CopyReferralCode(string referralCode) diff --git a/src/FrontOffice.Main/Pages/Profile/Index.razor.cs b/src/FrontOffice.Main/Pages/Profile/Index.razor.cs index b170b1f..3340c33 100644 --- a/src/FrontOffice.Main/Pages/Profile/Index.razor.cs +++ b/src/FrontOffice.Main/Pages/Profile/Index.razor.cs @@ -56,6 +56,9 @@ public partial class Index await base.OnAfterRenderAsync(firstRender); if (firstRender) { + // Refresh token to get latest user claims + await AuthService.RefreshTokenAsync(); + await LoadUserAuthInfo(); await LoadUserProfile(); await LoadAddresses(); diff --git a/src/FrontOffice.Main/Pages/_Host.cshtml b/src/FrontOffice.Main/Pages/_Host.cshtml index fcd16a8..f99261c 100644 --- a/src/FrontOffice.Main/Pages/_Host.cshtml +++ b/src/FrontOffice.Main/Pages/_Host.cshtml @@ -17,6 +17,8 @@ + + @@ -34,6 +36,13 @@ + + + + + + +