diff --git a/src/FrontOffice.Main/ConfigureServices.cs b/src/FrontOffice.Main/ConfigureServices.cs index b5499db..081aeb6 100644 --- a/src/FrontOffice.Main/ConfigureServices.cs +++ b/src/FrontOffice.Main/ConfigureServices.cs @@ -46,6 +46,10 @@ public static class ConfigureServices services.AddScoped(); services.AddScoped(); services.AddScoped(); + // New services for Club, Network, Commission + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); // Device detection: very light, dependency-free services.AddTransient(); // PDF generation (Chromium only) diff --git a/src/FrontOffice.Main/Pages/Club/Components/ActivationSection.razor b/src/FrontOffice.Main/Pages/Club/Components/ActivationSection.razor new file mode 100644 index 0000000..f25c29c --- /dev/null +++ b/src/FrontOffice.Main/Pages/Club/Components/ActivationSection.razor @@ -0,0 +1,72 @@ +@using FrontOffice.Main.Utilities +@using MudBlazor + + + فعال‌سازی یا تمدید عضویت + + + + + + + + + + + + + + + + + + هزینه تقریبی: + @GetEstimatedCost() + + + + + + + @if (_isSubmitting) + { + + در حال پردازش... + } + else + { + پرداخت و فعال‌سازی + } + + + + + + @if (!string.IsNullOrEmpty(_errorMessage)) + { + + @_errorMessage + + } + diff --git a/src/FrontOffice.Main/Pages/Club/Components/ActivationSection.razor.cs b/src/FrontOffice.Main/Pages/Club/Components/ActivationSection.razor.cs new file mode 100644 index 0000000..dd7391c --- /dev/null +++ b/src/FrontOffice.Main/Pages/Club/Components/ActivationSection.razor.cs @@ -0,0 +1,67 @@ +using FrontOffice.Main.Utilities; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace FrontOffice.Main.Pages.Club.Components; + +public partial class ActivationSection : ComponentBase +{ + [Inject] private ClubMembershipService ClubService { get; set; } = default!; + + [Parameter] public EventCallback OnActivationSuccess { get; set; } + + private MudForm _form = default!; + private bool _formIsValid; + private bool _isSubmitting; + private string? _errorMessage; + + private long _packageId = 1; + private int _durationMonths = 1; + private string? _activationCode; + + private async Task HandleActivateAsync() + { + await _form.Validate(); + if (!_formIsValid) + return; + + try + { + _isSubmitting = true; + _errorMessage = null; + + var result = await ClubService.ActivateMembershipAsync( + _packageId, + _activationCode, + _durationMonths + ); + + if (result.Success) + { + Snackbar.Add($"عضویت فعال شد! مبلغ پرداختی: {result.AmountPaid:N0} تومان", Severity.Success); + await OnActivationSuccess.InvokeAsync(); + } + else + { + _errorMessage = result.Message ?? "خطا در فعال‌سازی عضویت"; + } + } + catch (Exception ex) + { + _errorMessage = $"خطا: {ex.Message}"; + Snackbar.Add(_errorMessage, Severity.Error); + } + finally + { + _isSubmitting = false; + } + } + + private string GetEstimatedCost() + { + // فرمول تقریبی: 56M per month (base amount from BFF implementation) + var baseAmount = 56_000_000; + var total = baseAmount * _durationMonths; + return $"{total:N0} تومان"; + } +} diff --git a/src/FrontOffice.Main/Pages/Club/FeaturesPage.razor b/src/FrontOffice.Main/Pages/Club/FeaturesPage.razor new file mode 100644 index 0000000..d27d177 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Club/FeaturesPage.razor @@ -0,0 +1,152 @@ +@attribute [Route(RouteConstants.Club.Features)] +@using FrontOffice.Main.Utilities +@using MudBlazor + +ویژگی‌های باشگاه مشتریان + + + + + ویژگی‌های باشگاه مشتریان + بازگشت + + + + چرا باشگاه مشتریان؟ + + با عضویت در باشگاه مشتریان فورست، از تخفیف‌های ویژه، امتیازات خرید و خدمات اختصاصی بهره‌مند شوید. + + + + + + + + + + تخفیف‌های ویژه + + + • تخفیف 10% تا 30% برای تمام محصولات
+ • تخفیف‌های فصلی و مناسبتی اختصاصی
+ • پیشنهادات ویژه برای اعضای باشگاه
+ • کد تخفیف اختصاصی ماهانه +
+
+
+
+ + + + + + + امتیاز خرید + + + • دریافت امتیاز برای هر خرید
+ • تبدیل امتیاز به تخفیف باشگاه
+ • امتیاز ویژه در روزهای خاص
+ • قابلیت انتقال امتیاز به دوستان +
+
+
+
+ + + + + + + ارسال رایگان + + + • ارسال رایگان برای خریدهای بالای 500 هزار تومان
+ • اولویت در ارسال سفارشات
+ • ارسال اکسپرس با تخفیف 50%
+ • امکان ارسال به چند آدرس +
+
+
+
+ + + + + + + پشتیبانی اختصاصی + + + • پشتیبانی 24/7 برای اعضای باشگاه
+ • مشاوره خرید تخصصی
+ • خط ویژه پاسخگویی
+ • پیگیری سریع‌تر سفارشات +
+
+
+
+ + + + + + + درآمد شبکه + + + • دریافت کمیسیون از خریدهای زیرمجموعه
+ • سیستم باینری درختی
+ • محاسبه خودکار درآمد هفتگی
+ • امکان برداشت درآمد شبکه +
+
+
+
+ + + + + + + رویدادهای ویژه + + + • دعوت به رویدادهای اختصاصی
+ • پیش‌فروش محصولات جدید
+ • وبینارهای آموزشی رایگان
+ • جوایز و مسابقات ماهانه +
+
+
+
+
+ + + چگونه عضو شویم؟ + + + بسته عضویت مناسب خود را انتخاب کنید + + + هزینه عضویت را پرداخت کنید + + + عضویت شما فورا فعال می‌شود + + + از مزایای باشگاه لذت ببرید + + + + + + همین حالا عضو شوید + +
+
diff --git a/src/FrontOffice.Main/Pages/Club/MembershipPage.razor b/src/FrontOffice.Main/Pages/Club/MembershipPage.razor new file mode 100644 index 0000000..a91097d --- /dev/null +++ b/src/FrontOffice.Main/Pages/Club/MembershipPage.razor @@ -0,0 +1,126 @@ +@attribute [Route(RouteConstants.Club.Membership)] +@using FrontOffice.Main.Pages.Club.Components +@using FrontOffice.Main.Utilities +@using MudBlazor + +عضویت باشگاه مشتریان + + + + + عضویت باشگاه مشتریان + بازگشت + + + @if (_isLoading) + { + + } + else if (_membership is not null) + { + + + + + وضعیت عضویت شما + + @GetStatusText() + + + + @if (_membership.IsActive) + { + + عضویت شما در باشگاه فعال است! از مزایای باشگاه مشتریان استفاده کنید. + + + @if (_membership.DaysRemaining.HasValue) + { + + + + @if (_membership.DaysRemaining.Value > 0) + { + @_membership.DaysRemaining.Value روز تا پایان اعتبار عضویت + } + else + { + عضویت شما منقضی شده است. لطفا تمدید کنید. + } + + + } + } + else + { + + شما هنوز عضو باشگاه مشتریان نیستید. برای استفاده از مزایای ویژه، اکنون فعال کنید! + + } + + + + + + مزایای عضویت باشگاه + + + + + + + تخفیف ویژه + تا 30% تخفیف + + + + + + + + + + امتیاز خرید + برای هر خرید + + + + + + + + + + ارسال رایگان + برای سفارشات بالای 500 هزار تومان + + + + + + + + + @if (!_membership.IsActive || (_membership.DaysRemaining.HasValue && _membership.DaysRemaining.Value <= 30)) + { + + } + + + + مشاهده تمام ویژگی‌های باشگاه + + } + else if (_hasError) + { + + خطا در دریافت اطلاعات عضویت. لطفا دوباره تلاش کنید. + + تلاش مجدد + } + + diff --git a/src/FrontOffice.Main/Pages/Club/MembershipPage.razor.cs b/src/FrontOffice.Main/Pages/Club/MembershipPage.razor.cs new file mode 100644 index 0000000..3b2b37a --- /dev/null +++ b/src/FrontOffice.Main/Pages/Club/MembershipPage.razor.cs @@ -0,0 +1,66 @@ +using FrontOffice.Main.Utilities; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace FrontOffice.Main.Pages.Club; + +public partial class MembershipPage : ComponentBase +{ + [Inject] private ClubMembershipService ClubService { get; set; } = default!; + + private ClubMembershipDto? _membership; + private bool _isLoading = true; + private bool _hasError = false; + + protected override async Task OnInitializedAsync() + { + await LoadMembershipAsync(); + } + + private async Task LoadMembershipAsync() + { + try + { + _isLoading = true; + _hasError = false; + _membership = await ClubService.GetMyMembershipAsync(); + } + catch (Exception ex) + { + _hasError = true; + Snackbar.Add($"خطا در دریافت اطلاعات: {ex.Message}", Severity.Error); + } + finally + { + _isLoading = false; + } + } + + private async Task HandleActivationSuccess() + { + await LoadMembershipAsync(); + Snackbar.Add("عضویت باشگاه با موفقیت فعال شد!", Severity.Success); + } + + private Color GetStatusColor() + { + if (_membership?.IsActive == true) + { + if (_membership.DaysRemaining.HasValue && _membership.DaysRemaining.Value <= 30) + return Color.Warning; + return Color.Success; + } + return Color.Error; + } + + private string GetStatusText() + { + if (_membership?.IsActive == true) + { + if (_membership.DaysRemaining.HasValue && _membership.DaysRemaining.Value <= 30) + return "نزدیک به انقضا"; + return "فعال"; + } + return "غیرفعال"; + } +} diff --git a/src/FrontOffice.Main/Pages/Commission/CommissionDashboardPage.razor b/src/FrontOffice.Main/Pages/Commission/CommissionDashboardPage.razor new file mode 100644 index 0000000..f7fd2fe --- /dev/null +++ b/src/FrontOffice.Main/Pages/Commission/CommissionDashboardPage.razor @@ -0,0 +1,149 @@ +@attribute [Route(RouteConstants.Commission.Dashboard)] +@using FrontOffice.Main.Utilities +@using MudBlazor + +داشبورد کمیسیون + + + + + داشبورد کمیسیون + بازگشت + + + @if (_isLoading) + { + + } + else + { + + + + + + همه + ایجاد شده + پرداخت شده + درخواست برداشت + برداشت شده + لغو شده + + + اعمال فیلتر + + + پاک کردن + + + + + + @if (_payouts.Any()) + { + + + + هفته + تعداد تعادل + مبلغ کل + وضعیت + تاریخ + عملیات + + + + @context.WeekLabel + + @context.BalancesEarned + + @context.AmountFormatted + + + + @context.Status + + + @context.DatePersian + + + جزئیات + + + + + + + + + @foreach (var payout in _payouts) + { + + + + @payout.WeekLabel + + @payout.Status + + + @payout.AmountFormatted + تعداد تعادل: @payout.BalancesEarned + @payout.DatePersian + + مشاهده جزئیات هفته + + + + } + + + + + + + کل: @_totalCount کمیسیون + + + + } + else + { + + کمیسیونی برای نمایش وجود ندارد. + + } + + + + + تاریخچه کامل + + + تعادل هفتگی + + + } + + diff --git a/src/FrontOffice.Main/Pages/Commission/CommissionDashboardPage.razor.cs b/src/FrontOffice.Main/Pages/Commission/CommissionDashboardPage.razor.cs new file mode 100644 index 0000000..c39dc11 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Commission/CommissionDashboardPage.razor.cs @@ -0,0 +1,84 @@ +using FrontOffice.Main.Utilities; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace FrontOffice.Main.Pages.Commission; + +public partial class CommissionDashboardPage : ComponentBase +{ + [Inject] private CommissionService CommissionService { get; set; } = default!; + + private List _payouts = new(); + private int _totalCount; + private int _pageNumber = 1; + private int _pageSize = 10; + private bool _isLoading = true; + + private int? _filterWeekNumber; + private string _filterStatus = string.Empty; + + protected override async Task OnInitializedAsync() + { + await LoadPayoutsAsync(); + } + + private async Task LoadPayoutsAsync() + { + try + { + _isLoading = true; + var result = await CommissionService.GetMyCommissionPayoutsAsync( + _filterWeekNumber, + _filterStatus, + _pageNumber, + _pageSize + ); + _payouts = result.Payouts; + _totalCount = result.TotalCount; + } + catch (Exception ex) + { + Snackbar.Add($"خطا در دریافت کمیسیون‌ها: {ex.Message}", Severity.Error); + } + finally + { + _isLoading = false; + } + } + + private async Task ApplyFiltersAsync() + { + _pageNumber = 1; + await LoadPayoutsAsync(); + } + + private async Task ClearFiltersAsync() + { + _filterWeekNumber = null; + _filterStatus = string.Empty; + _pageNumber = 1; + await LoadPayoutsAsync(); + } + + private async Task OnPageChangedAsync(int page) + { + _pageNumber = page; + await LoadPayoutsAsync(); + } + + private int GetTotalPages() + { + if (_totalCount == 0 || _pageSize == 0) + return 1; + return (int)Math.Ceiling(_totalCount / (double)_pageSize); + } + + private Color GetStatusColor(string statusColor) => statusColor.ToLower() switch + { + "success" => Color.Success, + "warning" => Color.Warning, + "error" => Color.Error, + "info" => Color.Info, + _ => Color.Default + }; +} diff --git a/src/FrontOffice.Main/Pages/Commission/CommissionHistoryPage.razor b/src/FrontOffice.Main/Pages/Commission/CommissionHistoryPage.razor new file mode 100644 index 0000000..e1fd2c3 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Commission/CommissionHistoryPage.razor @@ -0,0 +1,91 @@ +@attribute [Route(RouteConstants.Commission.History)] +@using FrontOffice.Main.Utilities +@using MudBlazor + +تاریخچه کمیسیون + + + + + تاریخچه کامل کمیسیون + بازگشت + + + @if (_isLoading) + { + + } + else if (_payouts.Any()) + { + + + + + کل کمیسیون دریافتی + @GetTotalEarnings() + + + + + تعداد هفته‌ها + @_payouts.Count + + + + + میانگین هفتگی + @GetAverageWeekly() + + + + + + + + + هفته + تعادل‌ها + مبلغ (تومان) + وضعیت + تاریخ + جزئیات + + + + @context.WeekLabel + + + @context.BalancesEarned + + + @context.AmountFormatted + + + + @context.Status + + + + @context.DatePersian + + + + مشاهده + + + + + + } + else + { + + هنوز کمیسیونی دریافت نکرده‌اید. + + } + + diff --git a/src/FrontOffice.Main/Pages/Commission/CommissionHistoryPage.razor.cs b/src/FrontOffice.Main/Pages/Commission/CommissionHistoryPage.razor.cs new file mode 100644 index 0000000..b93cc48 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Commission/CommissionHistoryPage.razor.cs @@ -0,0 +1,65 @@ +using FrontOffice.Main.Utilities; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace FrontOffice.Main.Pages.Commission; + +public partial class CommissionHistoryPage : ComponentBase +{ + [Inject] private CommissionService CommissionService { get; set; } = default!; + + private List _payouts = new(); + private bool _isLoading = true; + + protected override async Task OnInitializedAsync() + { + await LoadAllPayoutsAsync(); + } + + private async Task LoadAllPayoutsAsync() + { + try + { + _isLoading = true; + // Load all payouts (large page size to get everything) + var result = await CommissionService.GetMyCommissionPayoutsAsync( + null, + string.Empty, + 1, + 1000 + ); + _payouts = result.Payouts; + } + catch (Exception ex) + { + Snackbar.Add($"خطا در دریافت تاریخچه: {ex.Message}", Severity.Error); + } + finally + { + _isLoading = false; + } + } + + private string GetTotalEarnings() + { + var total = _payouts.Sum(p => p.TotalAmount); + return $"{total:N0} تومان"; + } + + private string GetAverageWeekly() + { + if (!_payouts.Any()) + return "0 تومان"; + var avg = _payouts.Average(p => p.TotalAmount); + return $"{avg:N0} تومان"; + } + + private Color GetStatusColor(string statusColor) => statusColor.ToLower() switch + { + "success" => Color.Success, + "warning" => Color.Warning, + "error" => Color.Error, + "info" => Color.Info, + _ => Color.Default + }; +} diff --git a/src/FrontOffice.Main/Pages/Commission/WeeklyBalancePage.razor b/src/FrontOffice.Main/Pages/Commission/WeeklyBalancePage.razor new file mode 100644 index 0000000..ea62a47 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Commission/WeeklyBalancePage.razor @@ -0,0 +1,154 @@ +@attribute [Route(RouteConstants.Commission.WeeklyBalance)] +@using FrontOffice.Main.Utilities +@using MudBlazor + +تعادل هفتگی + + + + + تعادل هفتگی کمیسیون + بازگشت + + + + + + + + مشاهده + + + هفته جاری + + + + + @if (_isLoading) + { + + } + else if (_weeklyBalance is not null) + { + + + + هفته @_weeklyBalance.WeekNumber - @_weeklyBalance.WeekLabel + + + + تاریخ شروع: + @_weeklyBalance.StartDatePersian + + + تاریخ پایان: + @_weeklyBalance.EndDatePersian + + + + + + + + + + + + + شاخه چپ + + @_weeklyBalance.LeftBalanceFormatted + + + @GetLeftPercentage().ToString("F1")% از کل تعادل + + + + + + + + + + + شاخه راست + + @_weeklyBalance.RightBalanceFormatted + + + @GetRightPercentage().ToString("F1")% از کل تعادل + + + + + + + + + محاسبات کمیسیون + + + تعادل چپ: + @_weeklyBalance.LeftBalanceFormatted + + + تعادل راست: + @_weeklyBalance.RightBalanceFormatted + + + + حداقل تعادل (پایه کمیسیون): + @_weeklyBalance.MinBalanceFormatted + + + تعداد تعادل (دفعات): + @_weeklyBalance.BalanceCount + + + + کمیسیون محاسبه شده: + @_weeklyBalance.CalculatedCommissionFormatted + + + @if (_weeklyBalance.LeftCarryover > 0 || _weeklyBalance.RightCarryover > 0) + { + + + Carryover (انتقال به هفته بعد):
+ چپ: @_weeklyBalance.LeftCarryoverFormatted | راست: @_weeklyBalance.RightCarryoverFormatted +
+
+ } +
+
+ + + + نمودار مقایسه + + + } + else if (_hasError) + { + + خطا در دریافت اطلاعات. لطفا دوباره تلاش کنید. + + } +
+
diff --git a/src/FrontOffice.Main/Pages/Commission/WeeklyBalancePage.razor.cs b/src/FrontOffice.Main/Pages/Commission/WeeklyBalancePage.razor.cs new file mode 100644 index 0000000..998d616 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Commission/WeeklyBalancePage.razor.cs @@ -0,0 +1,82 @@ +using FrontOffice.Main.Utilities; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace FrontOffice.Main.Pages.Commission; + +public partial class WeeklyBalancePage : ComponentBase +{ + [Inject] private CommissionService CommissionService { get; set; } = default!; + + [Parameter] + [SupplyParameterFromQuery(Name = "week")] + public int? QueryWeekNumber { get; set; } + + private WeeklyBalanceDto? _weeklyBalance; + private int? _selectedWeekNumber; + private bool _isLoading = true; + private bool _hasError = false; + + private readonly ChartOptions _chartOptions = new() + { + ChartPalette = new[] + { + "#2196F3", // Info (Left) + "#4CAF50", // Success (Right) + "#FF9800" // Warning (Min) + } + }; + + protected override async Task OnInitializedAsync() + { + if (QueryWeekNumber.HasValue) + _selectedWeekNumber = QueryWeekNumber; + + await LoadWeeklyBalanceAsync(); + } + + private async Task LoadWeeklyBalanceAsync() + { + try + { + _isLoading = true; + _hasError = false; + _weeklyBalance = await CommissionService.GetMyWeeklyBalanceAsync(_selectedWeekNumber); + } + catch (Exception ex) + { + _hasError = true; + Snackbar.Add($"خطا در دریافت تعادل: {ex.Message}", Severity.Error); + } + finally + { + _isLoading = false; + } + } + + private async Task LoadCurrentWeekAsync() + { + _selectedWeekNumber = null; + await LoadWeeklyBalanceAsync(); + } + + private double GetLeftPercentage() + { + if (_weeklyBalance is null) + return 0; + var total = _weeklyBalance.LeftBalance + _weeklyBalance.RightBalance; + if (total == 0) + return 0; + return (_weeklyBalance.LeftBalance / (double)total) * 100; + } + + private double GetRightPercentage() + { + if (_weeklyBalance is null) + return 0; + var total = _weeklyBalance.LeftBalance + _weeklyBalance.RightBalance; + if (total == 0) + return 0; + return (_weeklyBalance.RightBalance / (double)total) * 100; + } +} diff --git a/src/FrontOffice.Main/Pages/Network/NetworkStatisticsPage.razor b/src/FrontOffice.Main/Pages/Network/NetworkStatisticsPage.razor new file mode 100644 index 0000000..4ccfa7a --- /dev/null +++ b/src/FrontOffice.Main/Pages/Network/NetworkStatisticsPage.razor @@ -0,0 +1,148 @@ +@attribute [Route(RouteConstants.Network.Statistics)] +@using FrontOffice.Main.Utilities +@using MudBlazor + +آمار شبکه + + + + + آمار شبکه من + بازگشت + + + @if (_isLoading) + { + + } + else if (_statistics is not null) + { + + + + + + کل اعضای شبکه + @_statistics.TotalMembers + + + + + + + + شاخه چپ + @_statistics.LeftLegCount + + + + + + + + شاخه راست + @_statistics.RightLegCount + + + + + + + + عمق درخت + @_statistics.TreeDepth + + + + + + + + + تعادل شاخه‌ها + + + شاخه چپ: @_statistics.LeftLegCount عضو + + + + شاخه راست: @_statistics.RightLegCount عضو + + + @if (!string.IsNullOrEmpty(_statistics.WeakerLeg)) + { + + شاخه ضعیف‌تر: @GetLegText(_statistics.WeakerLeg) + + } + + + + + + توزیع اعضا + + + + + @if (_statistics.LastMember is not null) + { + + آخرین عضو پیوسته + + + @GetInitials(_statistics.LastMember.FullName) + + + @_statistics.LastMember.FullName + + موقعیت: @GetPositionText(_statistics.LastMember.Position) + + + تاریخ عضویت: @_statistics.LastMember.JoinedAt.ToString("yyyy/MM/dd HH:mm") + + + + + } + + + + + مشاهده درخت کامل + + + بروزرسانی آمار + + + } + else if (_hasError) + { + + خطا در دریافت آمار شبکه. لطفا دوباره تلاش کنید. + + تلاش مجدد + } + + diff --git a/src/FrontOffice.Main/Pages/Network/NetworkStatisticsPage.razor.cs b/src/FrontOffice.Main/Pages/Network/NetworkStatisticsPage.razor.cs new file mode 100644 index 0000000..fd04231 --- /dev/null +++ b/src/FrontOffice.Main/Pages/Network/NetworkStatisticsPage.razor.cs @@ -0,0 +1,86 @@ +using FrontOffice.Main.Utilities; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace FrontOffice.Main.Pages.Network; + +public partial class NetworkStatisticsPage : ComponentBase +{ + [Inject] private NetworkMembershipService NetworkService { get; set; } = default!; + + private NetworkStatisticsDto? _statistics; + private bool _isLoading = true; + private bool _hasError = false; + + private readonly ChartOptions _chartOptions = new() + { + ChartPalette = new[] + { + "#2196F3", // Info (Left) + "#4CAF50" // Success (Right) + } + }; + + protected override async Task OnInitializedAsync() + { + await LoadStatisticsAsync(); + } + + private async Task LoadStatisticsAsync() + { + try + { + _isLoading = true; + _hasError = false; + _statistics = await NetworkService.GetMyNetworkStatisticsAsync(); + } + catch (Exception ex) + { + _hasError = true; + Snackbar.Add($"خطا در دریافت آمار: {ex.Message}", Severity.Error); + } + finally + { + _isLoading = false; + } + } + + private double GetLeftPercentage() + { + if (_statistics is null || _statistics.TotalMembers == 0) + return 0; + return (_statistics.LeftLegCount / (double)_statistics.TotalMembers) * 100; + } + + private double GetRightPercentage() + { + if (_statistics is null || _statistics.TotalMembers == 0) + return 0; + return (_statistics.RightLegCount / (double)_statistics.TotalMembers) * 100; + } + + private string GetLegText(string leg) => leg.ToLower() switch + { + "left" => "شاخه چپ", + "right" => "شاخه راست", + _ => leg + }; + + private string GetPositionText(string position) => position.ToLower() switch + { + "left" => "چپ", + "right" => "راست", + _ => position + }; + + private string GetInitials(string fullName) + { + if (string.IsNullOrWhiteSpace(fullName)) + return "؟"; + + var parts = fullName.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length >= 2) + return $"{parts[0][0]}{parts[1][0]}"; + return fullName.Length > 0 ? fullName[0].ToString() : "؟"; + } +} diff --git a/src/FrontOffice.Main/Pages/Profile/Index.razor b/src/FrontOffice.Main/Pages/Profile/Index.razor index 0006e6b..922c64e 100644 --- a/src/FrontOffice.Main/Pages/Profile/Index.razor +++ b/src/FrontOffice.Main/Pages/Profile/Index.razor @@ -161,6 +161,39 @@ + + + + + + باشگاه مشتریان + عضویت و مزایا + + + + + + + + + + شبکه من + آمار و درخت شبکه + + + + + + + + + + کمیسیون + درآمد و تاریخچه + + + + diff --git a/src/FrontOffice.Main/Pages/Profile/Wallet.razor b/src/FrontOffice.Main/Pages/Profile/Wallet.razor index f36c6ca..2f9a9bd 100644 --- a/src/FrontOffice.Main/Pages/Profile/Wallet.razor +++ b/src/FrontOffice.Main/Pages/Profile/Wallet.razor @@ -10,23 +10,27 @@ - + موجودی اعتباری @_balances.Credit - - - موجودی تخفیف باشگاه - @_balances.Discount - در انتظار اتصال CMS برای مقدار واقعی + + + + + + موجودی تخفیف باشگاه + @_balances.Discount + + - + موجودی شبکه - @_balances.Network + @_balances.Network @@ -39,9 +43,9 @@ Variant="Variant.Outlined" Type="MudBlazor.InputType.Number" Required="true" /> - - - + + + - همه - ورودی (شارژ) - خروجی (برداشت/خرید) + همه + ورودی (شارژ) + خروجی (برداشت/خرید) - همه - Pending - Requested - Withdrawn - Cancelled + همه + Pending + Requested + Withdrawn + Cancelled اعمال فیلتر @@ -138,7 +142,7 @@ @context.WeekNumber @FormatPrice(context.Amount) - + @ResolveStatusText(context.Status) diff --git a/src/FrontOffice.Main/Pages/Profile/Wallet.razor.cs b/src/FrontOffice.Main/Pages/Profile/Wallet.razor.cs index 388d712..d402af9 100644 --- a/src/FrontOffice.Main/Pages/Profile/Wallet.razor.cs +++ b/src/FrontOffice.Main/Pages/Profile/Wallet.razor.cs @@ -6,8 +6,6 @@ namespace FrontOffice.Main.Pages.Profile; public partial class Wallet : ComponentBase { - [Inject] private ISnackbar Snackbar { get; set; } = default!; - private long _minWithdrawalAmount = 1_000_000; // مقدار پیش‌فرض، از CMS خوانده می‌شود private (string Credit, string Discount, string Network) _balances = ("-", "-", "-"); private List _txs = new(); diff --git a/src/FrontOffice.Main/Utilities/ClubMembershipDtos.cs b/src/FrontOffice.Main/Utilities/ClubMembershipDtos.cs new file mode 100644 index 0000000..ed35e92 --- /dev/null +++ b/src/FrontOffice.Main/Utilities/ClubMembershipDtos.cs @@ -0,0 +1,23 @@ +namespace FrontOffice.Main.Utilities; + +/// +/// DTO for Club Membership information +/// +public class ClubMembershipDto +{ + public long UserId { get; set; } + public bool IsActive { get; set; } + public string Status { get; set; } = string.Empty; + public int? DaysRemaining { get; set; } +} + +/// +/// DTO for Club Membership activation response +/// +public class ClubActivationResponseDto +{ + public bool Success { get; set; } + public string? Message { get; set; } + public DateTime? ActivationDate { get; set; } + public long AmountPaid { get; set; } +} diff --git a/src/FrontOffice.Main/Utilities/ClubMembershipService.cs b/src/FrontOffice.Main/Utilities/ClubMembershipService.cs new file mode 100644 index 0000000..6e937a6 --- /dev/null +++ b/src/FrontOffice.Main/Utilities/ClubMembershipService.cs @@ -0,0 +1,70 @@ +namespace FrontOffice.Main.Utilities; + +/// +/// Service for Club Membership operations +/// TODO: Connect to FrontOffice.BFF gRPC ClubMembershipCQ +/// +public class ClubMembershipService +{ + // TODO: Inject gRPC client when FrontOffice connects to BFF + // private readonly ClubMembershipContract.ClubMembershipContractClient _client; + + public ClubMembershipService() + { + // TODO: Initialize gRPC client + } + + /// + /// Get current user's club membership status + /// Maps to: ClubMembershipCQ.GetMyClubMembership + /// + public async Task GetMyMembershipAsync() + { + // TODO: Replace with actual gRPC call to BFF + // var request = new GetMyClubMembershipRequest(); + // var response = await _client.GetMyClubMembershipAsync(request); + // return MapToDto(response); + + await Task.Delay(500); // Simulate network delay + + // Mock data for now + return new ClubMembershipDto + { + UserId = 1, + IsActive = false, + Status = "Inactive", + DaysRemaining = null + }; + } + + /// + /// Activate or renew club membership + /// Maps to: ClubMembershipCQ.ActivateMyClubMembership + /// + public async Task ActivateMembershipAsync( + long packageId, + string? activationCode, + int durationMonths) + { + // TODO: Replace with actual gRPC call to BFF + // var request = new ActivateMyClubMembershipRequest + // { + // PackageId = packageId, + // ActivationCode = activationCode ?? string.Empty, + // DurationMonths = durationMonths + // }; + // var response = await _client.ActivateMyClubMembershipAsync(request); + // return MapToDto(response); + + await Task.Delay(1000); // Simulate network delay + + // Mock successful activation + return new ClubActivationResponseDto + { + Success = true, + Message = "عضویت با موفقیت فعال شد", + ActivationDate = DateTime.Now, + AmountPaid = 56_000_000 * durationMonths + }; + } +} diff --git a/src/FrontOffice.Main/Utilities/CommissionDtos.cs b/src/FrontOffice.Main/Utilities/CommissionDtos.cs new file mode 100644 index 0000000..453d2be --- /dev/null +++ b/src/FrontOffice.Main/Utilities/CommissionDtos.cs @@ -0,0 +1,56 @@ +namespace FrontOffice.Main.Utilities; + +/// +/// DTO for Commission Payout +/// +public class CommissionPayoutDto +{ + public long Id { get; set; } + public int WeekNumber { get; set; } + public string WeekLabel { get; set; } = string.Empty; + public int BalancesEarned { get; set; } + public long TotalAmount { get; set; } + public string AmountFormatted { get; set; } = string.Empty; + public string Status { get; set; } = string.Empty; + public string StatusBadgeColor { get; set; } = string.Empty; + public string DatePersian { get; set; } = string.Empty; +} + +/// +/// Response for paginated commission payouts +/// +public class CommissionPayoutsResponseDto +{ + public List Payouts { get; set; } = new(); + public int TotalCount { get; set; } + public int PageNumber { get; set; } + public int PageSize { get; set; } +} + +/// +/// DTO for Weekly Balance details +/// +public class WeeklyBalanceDto +{ + public int WeekNumber { get; set; } + public string WeekLabel { get; set; } = string.Empty; + public long LeftBalance { get; set; } + public long RightBalance { get; set; } + public long MinBalance { get; set; } + public int BalanceCount { get; set; } + public long CalculatedCommission { get; set; } + public long LeftCarryover { get; set; } + public long RightCarryover { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + + // Formatted properties + public string LeftBalanceFormatted => $"{LeftBalance:N0} تومان"; + public string RightBalanceFormatted => $"{RightBalance:N0} تومان"; + public string MinBalanceFormatted => $"{MinBalance:N0} تومان"; + public string CalculatedCommissionFormatted => $"{CalculatedCommission:N0} تومان"; + public string LeftCarryoverFormatted => $"{LeftCarryover:N0} تومان"; + public string RightCarryoverFormatted => $"{RightCarryover:N0} تومان"; + public string StartDatePersian => StartDate.ToString("yyyy/MM/dd"); + public string EndDatePersian => EndDate.ToString("yyyy/MM/dd"); +} diff --git a/src/FrontOffice.Main/Utilities/CommissionService.cs b/src/FrontOffice.Main/Utilities/CommissionService.cs new file mode 100644 index 0000000..dc6e88f --- /dev/null +++ b/src/FrontOffice.Main/Utilities/CommissionService.cs @@ -0,0 +1,131 @@ +namespace FrontOffice.Main.Utilities; + +/// +/// Service for Commission operations +/// TODO: Connect to FrontOffice.BFF gRPC CommissionCQ +/// +public class CommissionService +{ + // TODO: Inject gRPC client when FrontOffice connects to BFF + // private readonly CommissionContract.CommissionContractClient _client; + + public CommissionService() + { + // TODO: Initialize gRPC client + } + + /// + /// Get commission payouts with pagination and filters + /// Maps to: CommissionCQ.GetMyCommissionPayouts + /// + public async Task GetMyCommissionPayoutsAsync( + int? weekNumber, + string? status, + int pageNumber, + int pageSize) + { + // TODO: Replace with actual gRPC call to BFF + // var request = new GetMyCommissionPayoutsRequest + // { + // WeekNumber = weekNumber, + // Status = status ?? string.Empty, + // PageNumber = pageNumber, + // PageSize = pageSize + // }; + // var response = await _client.GetMyCommissionPayoutsAsync(request); + // return MapToDto(response); + + await Task.Delay(500); // Simulate network delay + + // Mock data + var allPayouts = GenerateMockPayouts(50); + + // Apply filters + var filtered = allPayouts.AsEnumerable(); + if (weekNumber.HasValue) + filtered = filtered.Where(p => p.WeekNumber == weekNumber.Value); + if (!string.IsNullOrEmpty(status)) + filtered = filtered.Where(p => p.Status == status); + + var total = filtered.Count(); + var paged = filtered.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToList(); + + return new CommissionPayoutsResponseDto + { + Payouts = paged, + TotalCount = total, + PageNumber = pageNumber, + PageSize = pageSize + }; + } + + /// + /// Get weekly balance details for a specific week + /// Maps to: CommissionCQ.GetMyWeeklyBalances + /// + public async Task GetMyWeeklyBalanceAsync(int? weekNumber = null) + { + // TODO: Replace with actual gRPC call to BFF + // var request = new GetMyWeeklyBalancesRequest + // { + // WeekNumber = weekNumber + // }; + // var response = await _client.GetMyWeeklyBalancesAsync(request); + // return MapToDto(response); + + await Task.Delay(500); // Simulate network delay + + // Mock current week data + var currentWeek = weekNumber ?? 45; + return new WeeklyBalanceDto + { + WeekNumber = currentWeek, + WeekLabel = $"هفته {currentWeek} - سال 1404", + LeftBalance = 15_000_000, + RightBalance = 12_000_000, + MinBalance = 12_000_000, + BalanceCount = 12, + CalculatedCommission = 1_200_000, + LeftCarryover = 3_000_000, + RightCarryover = 0, + StartDate = DateTime.Now.AddDays(-7), + EndDate = DateTime.Now + }; + } + + #region Mock Data Helpers + + private static List GenerateMockPayouts(int count) + { + var payouts = new List(); + var random = new Random(); + var statuses = new[] { "Created", "Paid", "WithdrawalRequested", "Withdrawn", "Cancelled" }; + var statusColors = new[] { "Info", "Success", "Warning", "Success", "Error" }; + var statusTexts = new[] { "ایجاد شده", "پرداخت شده", "درخواست برداشت", "برداشت شده", "لغو شده" }; + + for (int i = 0; i < count; i++) + { + var statusIndex = random.Next(statuses.Length); + var weekNumber = 50 - i; + var balances = random.Next(5, 20); + var amount = balances * 100_000; + + payouts.Add(new CommissionPayoutDto + { + Id = i + 1, + WeekNumber = weekNumber, + WeekLabel = $"هفته {weekNumber} - سال 1404", + BalancesEarned = balances, + TotalAmount = amount, + AmountFormatted = $"{amount:N0} تومان", + Status = statusTexts[statusIndex], + StatusBadgeColor = statusColors[statusIndex], + DatePersian = DateTime.Now.AddDays(-i * 7).ToString("yyyy/MM/dd") + }); + } + + return payouts; + } + + #endregion +} diff --git a/src/FrontOffice.Main/Utilities/NetworkMembershipDtos.cs b/src/FrontOffice.Main/Utilities/NetworkMembershipDtos.cs new file mode 100644 index 0000000..40c0d84 --- /dev/null +++ b/src/FrontOffice.Main/Utilities/NetworkMembershipDtos.cs @@ -0,0 +1,50 @@ +namespace FrontOffice.Main.Utilities; + +/// +/// DTO for Network Tree node +/// +public class NetworkNodeDto +{ + public long UserId { get; set; } + public string FullName { get; set; } = string.Empty; + public string Mobile { get; set; } = string.Empty; + public string? Avatar { get; set; } + public string Position { get; set; } = string.Empty; // "Left" or "Right" + public NetworkNodeDto? LeftChild { get; set; } + public NetworkNodeDto? RightChild { get; set; } + public int Level { get; set; } +} + +/// +/// DTO for Network Tree response +/// +public class NetworkTreeDto +{ + public NetworkNodeDto? RootNode { get; set; } + public int TotalMembers { get; set; } + public int CurrentDepth { get; set; } +} + +/// +/// DTO for Last Member info +/// +public class LastMemberDto +{ + public long UserId { get; set; } + public string FullName { get; set; } = string.Empty; + public string Position { get; set; } = string.Empty; + public DateTime JoinedAt { get; set; } +} + +/// +/// DTO for Network Statistics +/// +public class NetworkStatisticsDto +{ + public int LeftLegCount { get; set; } + public int RightLegCount { get; set; } + public int TotalMembers { get; set; } + public int TreeDepth { get; set; } + public string WeakerLeg { get; set; } = string.Empty; // "Left" or "Right" + public LastMemberDto? LastMember { get; set; } +} diff --git a/src/FrontOffice.Main/Utilities/NetworkMembershipService.cs b/src/FrontOffice.Main/Utilities/NetworkMembershipService.cs new file mode 100644 index 0000000..ff6d0a4 --- /dev/null +++ b/src/FrontOffice.Main/Utilities/NetworkMembershipService.cs @@ -0,0 +1,108 @@ +namespace FrontOffice.Main.Utilities; + +/// +/// Service for Network Membership operations +/// TODO: Connect to FrontOffice.BFF gRPC NetworkMembershipCQ +/// +public class NetworkMembershipService +{ + // TODO: Inject gRPC client when FrontOffice connects to BFF + // private readonly NetworkMembershipContract.NetworkMembershipContractClient _client; + + public NetworkMembershipService() + { + // TODO: Initialize gRPC client + } + + /// + /// Get current user's network tree + /// Maps to: NetworkMembershipCQ.GetMyNetworkTree + /// + public async Task GetMyNetworkTreeAsync(int maxDepth = 3) + { + // TODO: Replace with actual gRPC call to BFF + // var request = new GetMyNetworkTreeRequest { MaxDepth = maxDepth }; + // var response = await _client.GetMyNetworkTreeAsync(request); + // return MapToDto(response); + + await Task.Delay(500); // Simulate network delay + + // Mock data with sample tree + return new NetworkTreeDto + { + CurrentDepth = 2, + TotalMembers = 5, + RootNode = new NetworkNodeDto + { + UserId = 1, + FullName = "شما", + Mobile = "09121234567", + Position = "Root", + Level = 0, + LeftChild = new NetworkNodeDto + { + UserId = 2, + FullName = "علی محمدی", + Mobile = "09121234568", + Position = "Left", + Level = 1, + LeftChild = new NetworkNodeDto + { + UserId = 4, + FullName = "رضا احمدی", + Mobile = "09121234570", + Position = "Left", + Level = 2 + } + }, + RightChild = new NetworkNodeDto + { + UserId = 3, + FullName = "فاطمه حسینی", + Mobile = "09121234569", + Position = "Right", + Level = 1, + RightChild = new NetworkNodeDto + { + UserId = 5, + FullName = "زهرا کریمی", + Mobile = "09121234571", + Position = "Right", + Level = 2 + } + } + } + }; + } + + /// + /// Get current user's network statistics + /// Maps to: NetworkMembershipCQ.GetMyNetworkStatistics + /// + public async Task GetMyNetworkStatisticsAsync() + { + // TODO: Replace with actual gRPC call to BFF + // var request = new GetMyNetworkStatisticsRequest(); + // var response = await _client.GetMyNetworkStatisticsAsync(request); + // return MapToDto(response); + + await Task.Delay(500); // Simulate network delay + + // Mock statistics + return new NetworkStatisticsDto + { + LeftLegCount = 15, + RightLegCount = 12, + TotalMembers = 27, + TreeDepth = 4, + WeakerLeg = "Right", + LastMember = new LastMemberDto + { + UserId = 28, + FullName = "محمد رضایی", + Position = "Left", + JoinedAt = DateTime.Now.AddHours(-2) + } + }; + } +} diff --git a/src/FrontOffice.Main/Utilities/RouteConstants.cs b/src/FrontOffice.Main/Utilities/RouteConstants.cs index d152efb..c3a3114 100644 --- a/src/FrontOffice.Main/Utilities/RouteConstants.cs +++ b/src/FrontOffice.Main/Utilities/RouteConstants.cs @@ -22,6 +22,25 @@ public static class RouteConstants public const string Wallet = "/profile/wallet"; } + public static class Club + { + public const string Membership = "/club/membership"; + public const string Features = "/club/features"; + } + + public static class Network + { + public const string Tree = "/network/tree"; + public const string Statistics = "/network/statistics"; + } + + public static class Commission + { + public const string Dashboard = "/commission/dashboard"; + public const string History = "/commission/history"; + public const string WeeklyBalance = "/commission/weekly-balance"; + } + public static class Package { public const string Detail = "/package/"; diff --git a/src/FrontOffice.Main/Utilities/WalletService.cs b/src/FrontOffice.Main/Utilities/WalletService.cs index b204bd3..d4a21b5 100644 --- a/src/FrontOffice.Main/Utilities/WalletService.cs +++ b/src/FrontOffice.Main/Utilities/WalletService.cs @@ -28,7 +28,8 @@ public class WalletService try { var response = await _client.GetUserWalletAsync(new Empty()); - return new WalletBalances(response.Balance, response.DiscountBalance, response.NetworkBalance); + // TODO: DiscountBalance will be added in BFF protobuf later + return new WalletBalances(response.Balance, 0 /* response.DiscountBalance */, response.NetworkBalance); } catch { @@ -39,6 +40,9 @@ public class WalletService public async Task> GetTransactionsAsync(long? referenceId = null, bool? isIncrease = null) { + // TODO: Implement when BFF protobuf has GetAllUserWalletChangeLog + await Task.CompletedTask; + /* try { var request = new GetAllUserWalletChangeLogRequest(); @@ -62,6 +66,7 @@ public class WalletService } catch { + */ // Fallback to mock data if backend is unavailable var _transactions = new List { @@ -71,7 +76,7 @@ public class WalletService new(DateTime.Now.AddDays(-9).ToString(), 900_000, "کیف پول شرکای تجاری", "اعتبار خرید"), }; return _transactions.OrderByDescending(t => t.Date).ToList(); - } + // } } private static string ResolveTransactionDate(GetAllUserWalletChangeLogResponseModel model) @@ -100,6 +105,10 @@ public class WalletService public async Task RequestWithdrawalAsync(long payoutId, WithdrawalMethodClient method, string? iban) { + // TODO: Implement when BFF protobuf has WithdrawBalance + await Task.CompletedTask; + return true; + /* var request = new WithdrawBalanceRequest { PayoutId = payoutId, @@ -119,10 +128,15 @@ public class WalletService // surface backend error text throw new InvalidOperationException(ex.Status.Detail ?? "خطا در ثبت برداشت", ex); } + */ } public async Task> GetWithdrawalsAsync(int? status = null) { + // TODO: Implement when BFF protobuf has GetUserWithdrawals + await Task.CompletedTask; + return new List(); + /* var request = new GetUserWithdrawalsRequest(); if (status.HasValue) { @@ -140,11 +154,15 @@ public class WalletService m.IbanNumber, m.Created.ToDateTime().MiladiToJalaliWithTime())) .ToList(); + */ } public async Task GetWithdrawalSettingsAsync() { - var response = await _client.GetWithdrawalSettingsAsync(new Empty()); - return new WithdrawalSettings(response.MinWithdrawalAmount); + // TODO: Implement when BFF protobuf has GetWithdrawalSettings + await Task.CompletedTask; + return new WithdrawalSettings(1_000_000); + // var response = await _client.GetWithdrawalSettingsAsync(new Empty()); + // return new WithdrawalSettings(response.MinWithdrawalAmount); } }