diff --git a/docs/ADMIN-SERVICES.md b/docs/ADMIN-SERVICES.md index bad2c63..794cf1d 100644 --- a/docs/ADMIN-SERVICES.md +++ b/docs/ADMIN-SERVICES.md @@ -4,7 +4,7 @@ - **مسیر wrapper سرویس‌ها:** `BackOffice/src/BackOffice/Services/` - **ثبت gRPC:** `BackOffice/src/BackOffice/Common/Configure/ConfigureService.cs` -- **تاریخ:** ۱۴۰۵/۰۳/۱۷ +- **تاریخ:** ۱۴۰۵/۰۳/۱۷ (بروز: خرداد ۱۴۰۵ — گزارش‌های read-only + proto 0.0.201؛ **بروز ۱۴۰۵/۰۳/۱۸ — دفتر تراکنش‌های مالی proto 0.0.203**) --- @@ -138,13 +138,26 @@ ## ۱۰. پرداخت دستی و تراکنش‌ها +> **دو گزارش تراکنش — اشتباه نگیرید:** +> +> | مسیر | جدول DB | کاربرد | +> |------|---------|--------| +> | `/payment/transactions` | `PaymentTransactions` | ممیزی درگاه (Authority، ZarinPal، کارت) | +> | `/payment/ledger-transactions` | `Transactions` | دفتر مالی داخلی (نوع تراکنش، وضعیت، RefId) | + | # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد | |---|-------------|---------------|-----------------|--------| | 10.1 | `ManualPaymentContract.GetAllManualPayments` | `ManualPaymentContract` | `/payment/manual-payments` | لیست پرداخت‌های دستی | | 10.2 | `ManualPaymentContract.CreateManualPayment` | `ManualPaymentContract` | `ManualPaymentDialog` | ثبت پرداخت دستی (مثلاً Daya) | | 10.3 | `ManualPaymentContract.ApproveManualPayment` | `ManualPaymentContract` | ManualPayments, Dialog | تأیید پرداخت | | 10.4 | `ManualPaymentContract.RejectManualPayment` | `ManualPaymentContract` | ManualPayments, Dialog | رد پرداخت | -| 10.5 | `/payment/transactions` | — | Transactions page | **TODO — هنوز به API وصل نشده** | +| 10.5 | `PaymentTransactionContract.GetAllPaymentTransactionByFilter` | `PaymentTransactionContract` | `/payment/transactions` | گزارش read-only تراکنش **درگاه** (`PaymentTransactions`) | +| 10.5b | `PaymentTransactionContract.GetPaymentTransactionSummary` | `PaymentTransactionContract` | `/payment/transactions` | کارت خلاصه گزارش درگاه | +| 10.6 | `TransactionsContract.GetAllTransactionsByFilter` | `TransactionsContract` | `/payment/ledger-transactions` | گزارش read-only **دفتر مالی** (`Transactions`) | +| 10.7 | `TransactionsContract.GetTransactionSummary` | `TransactionsContract` | `/payment/ledger-transactions` | کارت خلاصه دفتر تراکنش | +| 10.8 | `UserPackagePurchaseContract.GetAllUserPackagePurchaseByFilter` | `UserPackagePurchaseContract` | `/club/purchases` | گزارش خرید پکیج | +| 10.9 | `ClubMembershipCycleContract.GetAllClubMembershipCycleByFilter` | `ClubMembershipCycleContract` | `/club/cycles` | گزارش دوره عضویت | +| 10.10 | `ClubMembershipCycleHistoryContract.GetAllClubMembershipCycleHistoryByFilter` | `ClubMembershipCycleHistoryContract` | `/club/history` (زیرتب دوره) | تاریخچه تغییرات دوره | --- @@ -206,7 +219,7 @@ | 15.1 | `ClubMembershipContract.GetAllClubMemberships` | `ClubMembershipContract` | `/club/members`, Index | لیست اعضا | | 15.2 | `ClubMembershipContract.ActivateClubMembership` | `ClubMembershipContract` | `ActivateClubDialog` | فعال‌سازی دستی عضویت | | 15.3 | `ClubMembershipContract.DeactivateClubMembership` | `ClubMembershipContract` | `DeactivateClubDialog` | غیرفعال‌سازی | -| 15.4 | `ClubMembershipContract.GetClubMembershipHistory` | `ClubMembershipContract` | `MemberDetailsDialog` | تاریخچه عضویت | +| 15.4 | `ClubMembershipContract.GetClubMembershipHistory` | `ClubMembershipContract` | `/club/history` (زیرتب عضویت) | گزارش تاریخچه عضویت | | 15.5 | `ClubMembershipContract.GetClubStatistics` | `ClubMembershipContract` | `/club/statistics` | آمار باشگاه | | 15.6 | `ConfigurationContract.GetClubFeatures` | `ConfigurationContract` | `/club/features` | لیست featureهای باشگاه | | 15.7 | `ClubMembershipContract.GetUserClubFeatures` | `ClubMembershipContract` | `/club/features` | featureهای یک کاربر | @@ -316,7 +329,6 @@ | سرویس / صفحه | وضعیت | |--------------|--------| | `IBlogPostImageService` (`BlogPostImageContract`) | ثبت در DI؛ **هیچ صفحه‌ای inject نمی‌کند** | -| `/payment/transactions` | UI آماده؛ **LoadData خالی — API وصل نشده** | | `RolePermissionsDialog` | UI permission؛ **ذخیره در backend TODO** | | `AuthorizationService` | فقط JWT role؛ **Permission API از CMS/BFF TODO** | | `/system/alerts` | داده mock؛ **AlertContract وجود ندارد** | @@ -349,6 +361,11 @@ flowchart LR | `CategoryContract` | Categories | | `UserOrderContract` | Orders | | `ManualPaymentContract` | Manual payments | +| `PaymentTransactionContract` | Gateway transaction report (`/payment/transactions`) | +| `TransactionsContract` | Ledger transaction report (`/payment/ledger-transactions`) | +| `UserPackagePurchaseContract` | Club package purchase report | +| `ClubMembershipCycleContract` | Club cycle report | +| `ClubMembershipCycleHistoryContract` | Club cycle history report | | `UserWalletContract` | Wallet management | | `UserWalletHistoryContract` | Wallet change log | | `UserContractContract` | Signed contracts | @@ -385,3 +402,18 @@ flowchart LR | Wallet | My balances | All users wallet + history log | مرجع مشابه برای فرانت مشتری: [`FrontOffice/docs/CUSTOMER-SERVICES.md`](../FrontOffice/docs/CUSTOMER-SERVICES.md) + +--- + +## یادداشت deploy (خرداد ۱۴۰۵) + +| گام | جزئیات | +|-----|--------| +| Proto | `Foursat.CMSMicroservice.Protobuf` **0.0.203** (گزارش دفتر تراکنش) | +| CMS | deploy WebApi (شامل `GetTransactionSummary` + فیلتر تاریخ) | +| BackOffice | NuGet 0.0.203 یا ProjectReference در monorepo | +| FrontOffice | **redeploy لازم نیست** برای گزارش ledger؛ برای شارژ کیف اصلی proto **0.0.202** لازم است | + +گزارش‌های read-only (بخش ۱۰ و ۱۵): فقط فیلتر + grid + summary + CSV — بدون CRUD یا detail dialog. + +مستندات فنی کامل: [`totalDoc/technical/TECH-08-SESSION-LOG-202606.md`](../../totalDoc/technical/TECH-08-SESSION-LOG-202606.md) — §10 Ledger transactions diff --git a/src/BackOffice/BackOffice.csproj b/src/BackOffice/BackOffice.csproj index ff82465..78d7ffa 100644 --- a/src/BackOffice/BackOffice.csproj +++ b/src/BackOffice/BackOffice.csproj @@ -138,9 +138,12 @@ - - - + + + + + + diff --git a/src/BackOffice/Common/Configure/ConfigureService.cs b/src/BackOffice/Common/Configure/ConfigureService.cs index cdcf926..441c667 100644 --- a/src/BackOffice/Common/Configure/ConfigureService.cs +++ b/src/BackOffice/Common/Configure/ConfigureService.cs @@ -31,6 +31,11 @@ using CMSMicroservice.Protobuf.Protos.SitePageSettings; using CMSMicroservice.Protobuf.Protos.UserContract; using CMSMicroservice.Protobuf.Protos.UserWallet; using CMSMicroservice.Protobuf.Protos.UserWalletHistory; +using CMSMicroservice.Protobuf.Protos.PaymentTransaction; +using CMSMicroservice.Protobuf.Protos.Transactions; +using CMSMicroservice.Protobuf.Protos.UserPackagePurchase; +using CMSMicroservice.Protobuf.Protos.ClubMembershipCycle; +using CMSMicroservice.Protobuf.Protos.ClubMembershipCycleHistory; using BackOffice.Common.Utilities; using BackOffice.Services.DiscountProduct; using BackOffice.Services.DiscountCategory; @@ -169,6 +174,13 @@ public static class ConfigureServices services.AddTransient(sp => new UserWalletContract.UserWalletContractClient(sp.GetRequiredService())); services.AddTransient(sp => new UserWalletHistoryContract.UserWalletHistoryContractClient(sp.GetRequiredService())); + // Admin Report Services + services.AddTransient(sp => new PaymentTransactionContract.PaymentTransactionContractClient(sp.GetRequiredService())); + services.AddTransient(sp => new TransactionsContract.TransactionsContractClient(sp.GetRequiredService())); + services.AddTransient(sp => new UserPackagePurchaseContract.UserPackagePurchaseContractClient(sp.GetRequiredService())); + services.AddTransient(sp => new ClubMembershipCycleContract.ClubMembershipCycleContractClient(sp.GetRequiredService())); + services.AddTransient(sp => new ClubMembershipCycleHistoryContract.ClubMembershipCycleHistoryContractClient(sp.GetRequiredService())); + return services; } diff --git a/src/BackOffice/Common/Utilities/ReportLabels.cs b/src/BackOffice/Common/Utilities/ReportLabels.cs new file mode 100644 index 0000000..4c6c281 --- /dev/null +++ b/src/BackOffice/Common/Utilities/ReportLabels.cs @@ -0,0 +1,81 @@ +namespace BackOffice.Common.Utilities; + +using CMSMicroservice.Protobuf.Protos; +using MudBlazor; + +public static class ReportLabels +{ + public static string GetPaymentStatusLabel(bool success) => success ? "موفق" : "در انتظار / ناموفق"; + + public static string GetTransactionPaymentStatusLabel(PaymentStatus? status) => status switch + { + PaymentStatus.Success => "موفق", + PaymentStatus.Pending => "در انتظار", + PaymentStatus.Reject => "رد شده", + _ => "—" + }; + + public static Color GetTransactionPaymentStatusColor(PaymentStatus? status) => status switch + { + PaymentStatus.Success => Color.Success, + PaymentStatus.Pending => Color.Warning, + PaymentStatus.Reject => Color.Error, + _ => Color.Default + }; + + public static string GetTransactionTypeLabel(TransactionType? type) => type switch + { + TransactionType.Buy => "خرید", + TransactionType.DepositIpg => "واریز درگاه", + TransactionType.DepositExternal1 => "واریز خارجی", + TransactionType.Withdraw => "برداشت", + TransactionType.NetworkCommission => "کمیسیون شبکه", + TransactionType.ClubActivation => "فعال‌سازی باشگاه", + TransactionType.DiscountWalletCharge => "شارژ کیف تخفیف", + TransactionType.DiscountShopPurchase => "خرید فروشگاه تخفیف", + TransactionType.MagicWalletDeposit => "واریز کیف جادویی", + TransactionType.MagicWalletBonus => "بونوس کیف جادویی", + TransactionType.CreditWalletCharge => "شارژ کیف اصلی", + _ => type?.ToString() ?? "—" + }; + + public static string GetPurchaseMethodLabel(int method) => method switch + { + 1 => "وام دایا", + 2 => "خرید مستقیم", + _ => "نامشخص" + }; + + public static string GetClubMembershipActionLabel(int action) => action switch + { + 0 => "فعال‌سازی", + 1 => "غیرفعال‌سازی", + 2 => "ویرایش", + 3 => "اصلاح دستی", + _ => action.ToString() + }; + + public static string GetClubCycleActionLabel(int action) => action switch + { + 0 => "شروع دوره", + 1 => "ورود به حالت جادویی", + 2 => "پایان حالت جادویی", + 3 => "تکمیل دوره", + 4 => "لغو", + 5 => "اصلاح دستی", + _ => action.ToString() + }; + + public static string GetMagicStateLabel(int? magicStarted, DateTime? started, DateTime? completed) + { + if (completed.HasValue) return "جادویی تکمیل‌شده"; + if (started.HasValue) return "در حالت جادویی"; + return "عادی"; + } + + public static string FormatActiveChange(bool oldActive, bool newActive) => + $"{(oldActive ? "فعال" : "غیرفعال")} → {(newActive ? "فعال" : "غیرفعال")}"; + + public static string FormatBoolChange(bool oldValue, bool newValue) => + $"{(oldValue ? "بله" : "خیر")} → {(newValue ? "بله" : "خیر")}"; +} diff --git a/src/BackOffice/Pages/Club/ClubHub.razor b/src/BackOffice/Pages/Club/ClubHub.razor index bc90f85..ef21fcd 100644 --- a/src/BackOffice/Pages/Club/ClubHub.razor +++ b/src/BackOffice/Pages/Club/ClubHub.razor @@ -1,17 +1,30 @@ @page "/club" @page "/club/members" +@page "/club/purchases" +@page "/club/cycles" +@page "/club/history" @page "/club/statistics" @attribute [Authorize] +@using BackOffice.Pages.Club.Reports
باشگاه مشتریان - + - + + + + + + + + + + @@ -25,7 +38,10 @@ var uri = Navigation.ToBaseRelativePath(Navigation.Uri).ToLowerInvariant().TrimEnd('/'); _activeTab = uri switch { - "club/statistics" => 1, + "club/purchases" => 1, + "club/cycles" => 2, + "club/history" => 3, + "club/statistics" => 4, _ => 0 }; } diff --git a/src/BackOffice/Pages/Club/Components/MemberDetailsDialog.razor b/src/BackOffice/Pages/Club/Components/MemberDetailsDialog.razor index 16912ef..9485092 100644 --- a/src/BackOffice/Pages/Club/Components/MemberDetailsDialog.razor +++ b/src/BackOffice/Pages/Club/Components/MemberDetailsDialog.razor @@ -33,23 +33,11 @@ پکیج: - @Member.PackageName - - - کد فعال‌سازی: - @Member.ActivationCode + @(string.IsNullOrEmpty(Member.PackageName) ? "—" : Member.PackageName) تاریخ فعال‌سازی: - @Member.ActivatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() - - - تاریخ انقضا: - - - @Member.ExpiresAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() - - + @(Member.ActivatedAt != null ? Member.ActivatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() : "—") وضعیت: @@ -61,60 +49,17 @@ - - منقضی شده: - - - @(Member.IsExpired ? "بله" : "خیر") - - - - - - - - - تاریخچه - - - تاریخ ایجاد: @Member.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() - - @if (_historyLoading) - { - - } - else if (_history.Any()) - { - - - عملیات - تاریخ - دلیل - توسط - - - - @context.Action - - @context.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() - @(string.IsNullOrEmpty(context.Reason) ? "-" : context.Reason) - @(string.IsNullOrEmpty(context.PerformedBy) ? "-" : context.PerformedBy) - - - } - else - { - تاریخچه‌ای یافت نشد. - } + + + تاریخچه تغییرات عضویت در تب «گزارش تاریخچه» باشگاه قابل مشاهده است. + } @@ -126,39 +71,5 @@ @code { [CascadingParameter] IMudDialogInstance MudDialog { get; set; } [Parameter] public ClubMembershipModel Member { get; set; } - [Inject] public ClubMembershipContract.ClubMembershipContractClient ClubClient { get; set; } - - private List _history = new(); - private bool _historyLoading; - - protected override async Task OnInitializedAsync() - { - await LoadHistory(); - } - - private async Task LoadHistory() - { - _historyLoading = true; - try - { - var request = new GetClubMembershipHistoryRequest - { - UserId = Member.UserId, - PageIndex = 0, - PageSize = 50 - }; - var response = await ClubClient.GetClubMembershipHistoryAsync(request); - _history = response.Models?.ToList() ?? new(); - } - catch - { - _history = new(); - } - finally - { - _historyLoading = false; - } - } - private void Close() => MudDialog.Close(); } diff --git a/src/BackOffice/Pages/Club/Reports/ClubCyclesReport.razor b/src/BackOffice/Pages/Club/Reports/ClubCyclesReport.razor new file mode 100644 index 0000000..e2e4c09 --- /dev/null +++ b/src/BackOffice/Pages/Club/Reports/ClubCyclesReport.razor @@ -0,0 +1,153 @@ +@using BackOffice.Common.BaseComponents +@using BackOffice.Pages.AutoComplete +@using CMSMicroservice.Protobuf.Protos.ClubMembershipCycle +@using Google.Protobuf.WellKnownTypes + +@inject ClubMembershipCycleContract.ClubMembershipCycleContractClient CycleClient + + + گزارش دوره‌های عضویت باشگاه — هر خرید پکیج یک دور جدید ایجاد می‌کند. + + + + + + + + + + فقط دوره جاری + دوره‌های قبلی + + + + + عادی + در حالت جادویی + جادویی تکمیل‌شده + + + + + + + + + + + کل دوره‌ها + @_totalCount.ToString("N0") + + + + + دوره جاری + @_currentCount.ToString("N0") + + + + + جادویی فعال + @_magicActive.ToString("N0") + + + + + + + + + + + + @context.Item.PackageAmount.ToString("N0") + + + @ReportLabels.GetPurchaseMethodLabel(context.Item.PurchaseMethod) + + + @context.Item.PackagePurchasedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() + + + + @ReportLabels.GetMagicStateLabel(null, + context.Item.MagicStartedAt?.ToDateTime(), + context.Item.MagicCompletedAt?.ToDateTime()) + + + + + + @(context.Item.IsCurrentCycle ? "بله" : "خیر") + + + + + + موردی یافت نشد. + + + + + + + + +@code { + private BasePageComponent? _basePage; + private MudDataGrid? _grid; + private long? _userId; + private bool? _isCurrent; + private int? _magicState; + private DateTime? _fromDate, _toDate; + private long _totalCount, _currentCount, _magicActive; + + private GetAllClubMembershipCycleByFilterFilter BuildFilter() => new() + { + UserId = _userId > 0 ? _userId : null, + IsCurrentCycle = _isCurrent, + MagicState = _magicState, + PackagePurchasedFrom = _fromDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_fromDate.Value, DateTimeKind.Utc)) : null, + PackagePurchasedTo = _toDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_toDate.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc)) : null + }; + + private async Task> LoadData(GridState state) + { + try + { + var filter = BuildFilter(); + var summary = await CycleClient.GetClubMembershipCycleSummaryAsync(new GetClubMembershipCycleSummaryRequest { Filter = filter }); + _totalCount = summary.TotalCount; + _currentCount = summary.CurrentCycleCount; + _magicActive = summary.MagicActiveCount; + + var result = await CycleClient.GetAllClubMembershipCycleByFilterAsync(new GetAllClubMembershipCycleByFilterRequest + { + PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize }, + Filter = filter + }); + return new GridData + { + Items = result?.Models?.ToList() ?? new(), + TotalItems = (int)(result?.MetaData?.TotalCount ?? 0) + }; + } + catch (Exception ex) + { + Snackbar.Add($"خطا: {ex.Message}", Severity.Error); + return new GridData(); + } + } + + private async Task OnFilterSubmit() { if (_grid != null) await _grid.ReloadServerData(); } + private async Task OnFilterCleared() + { + _userId = null; _isCurrent = null; _magicState = null; _fromDate = _toDate = null; + if (_grid != null) await _grid.ReloadServerData(); + } +} diff --git a/src/BackOffice/Pages/Club/Reports/ClubHistoriesReport.razor b/src/BackOffice/Pages/Club/Reports/ClubHistoriesReport.razor new file mode 100644 index 0000000..11f9eca --- /dev/null +++ b/src/BackOffice/Pages/Club/Reports/ClubHistoriesReport.razor @@ -0,0 +1,153 @@ +@using BackOffice.Common.BaseComponents +@using BackOffice.Pages.AutoComplete +@using CMSMicroservice.Protobuf.Protos.ClubMembership +@using CMSMicroservice.Protobuf.Protos.ClubMembershipCycleHistory +@using Google.Protobuf.WellKnownTypes + +@inject ClubMembershipContract.ClubMembershipContractClient ClubClient +@inject ClubMembershipCycleHistoryContract.ClubMembershipCycleHistoryContractClient CycleHistoryClient + + + + + تاریخچه فعال/غیرفعال شدن عضویت باشگاه. + + + + + فعال‌سازی + غیرفعال‌سازی + ویرایش + اصلاح دستی + + جستجو + + + + + + + @ReportLabels.FormatActiveChange(context.Item.OldIsActive, context.Item.NewIsActive) + + + + @(context.Item.OldInitialContribution != null ? context.Item.OldInitialContribution.Value.ToString("N0") : "—") + → @(context.Item.NewInitialContribution != null ? context.Item.NewInitialContribution.Value.ToString("N0") : "—") + + + + @ReportLabels.GetClubMembershipActionLabel(context.Item.Action) + + + + + @context.Item.CreatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() + + + + موردی یافت نشد. + + + + + + + + + + ردیابی تغییرات دوره از زمان deploy فعال شد. رکوردهای قدیمی‌تر ممکن است خالی باشند. + + + + جستجو + + + + + + + + @ReportLabels.FormatBoolChange(context.Item.OldIsCurrentCycle, context.Item.NewIsCurrentCycle) + + + @ReportLabels.GetClubCycleActionLabel(context.Item.Action) + + + + + @context.Item.CreatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() + + + + موردی یافت نشد. + + + + + + + + +@code { + private MudDataGrid? _memGrid; + private MudDataGrid? _cycleGrid; + private long? _memUserId, _cycleUserId; + private int? _memAction; + + private async Task> LoadMembershipHistory(GridState state) + { + try + { + var result = await ClubClient.GetClubMembershipHistoryAsync(new GetClubMembershipHistoryRequest + { + PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize }, + Filter = new GetClubMembershipHistoryFilter + { + UserId = _memUserId > 0 ? _memUserId : null, + Action = _memAction + } + }); + return new GridData + { + Items = result?.Models?.ToList() ?? new(), + TotalItems = (int)(result?.MetaData?.TotalCount ?? 0) + }; + } + catch (Exception ex) + { + Snackbar.Add($"خطا: {ex.Message}", Severity.Error); + return new GridData(); + } + } + + private async Task> LoadCycleHistory(GridState state) + { + try + { + var result = await CycleHistoryClient.GetAllClubMembershipCycleHistoryByFilterAsync(new GetAllClubMembershipCycleHistoryByFilterRequest + { + PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize }, + Filter = new GetAllClubMembershipCycleHistoryByFilterFilter + { + UserId = _cycleUserId > 0 ? _cycleUserId : null + } + }); + return new GridData + { + Items = result?.Models?.ToList() ?? new(), + TotalItems = (int)(result?.MetaData?.TotalCount ?? 0) + }; + } + catch (Exception ex) + { + Snackbar.Add($"خطا: {ex.Message}", Severity.Error); + return new GridData(); + } + } + + private async Task ReloadMembership() { if (_memGrid != null) await _memGrid.ReloadServerData(); } + private async Task ReloadCycle() { if (_cycleGrid != null) await _cycleGrid.ReloadServerData(); } +} diff --git a/src/BackOffice/Pages/Club/Reports/ClubPackagePurchasesReport.razor b/src/BackOffice/Pages/Club/Reports/ClubPackagePurchasesReport.razor new file mode 100644 index 0000000..a4e7a7e --- /dev/null +++ b/src/BackOffice/Pages/Club/Reports/ClubPackagePurchasesReport.razor @@ -0,0 +1,130 @@ +@using BackOffice.Common.BaseComponents +@using BackOffice.Pages.AutoComplete +@using CMSMicroservice.Protobuf.Protos.UserPackagePurchase +@using Google.Protobuf.WellKnownTypes + +@inject UserPackagePurchaseContract.UserPackagePurchaseContractClient PurchaseClient + + + گزارش read-only خریدهای پکیج — فقط مشاهده و فیلتر. + + + + + + + + + + وام دایا + خرید مستقیم + + + + + + + + + + + تعداد + @_totalCount.ToString("N0") + + + + + جمع مبلغ (ریال) + @_totalAmount.ToString("N0") + + + + + + + + + + + @context.Item.UserName + @context.Item.UserMobile + + + + + + @ReportLabels.GetPurchaseMethodLabel(context.Item.PurchaseMethod) + + + @context.Item.Amount.ToString("N0") + + + @context.Item.PurchasedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() + + + + + + موردی یافت نشد. + + + + + + + + +@code { + private BasePageComponent? _basePage; + private MudDataGrid? _grid; + private long? _userId; + private int? _purchaseMethod; + private DateTime? _fromDate, _toDate; + private long _totalCount, _totalAmount; + + private GetAllUserPackagePurchaseByFilterFilter BuildFilter() => new() + { + UserId = _userId > 0 ? _userId : null, + PurchaseMethod = _purchaseMethod, + PurchasedFrom = _fromDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_fromDate.Value, DateTimeKind.Utc)) : null, + PurchasedTo = _toDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_toDate.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc)) : null + }; + + private async Task> LoadData(GridState state) + { + try + { + var filter = BuildFilter(); + var summary = await PurchaseClient.GetUserPackagePurchaseSummaryAsync(new GetUserPackagePurchaseSummaryRequest { Filter = filter }); + _totalCount = summary.TotalCount; + _totalAmount = summary.TotalAmount; + + var result = await PurchaseClient.GetAllUserPackagePurchaseByFilterAsync(new GetAllUserPackagePurchaseByFilterRequest + { + PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize }, + Filter = filter + }); + return new GridData + { + Items = result?.Models?.ToList() ?? new(), + TotalItems = (int)(result?.MetaData?.TotalCount ?? 0) + }; + } + catch (Exception ex) + { + Snackbar.Add($"خطا: {ex.Message}", Severity.Error); + return new GridData(); + } + } + + private async Task OnFilterSubmit() { if (_grid != null) await _grid.ReloadServerData(); } + private async Task OnFilterCleared() + { + _userId = null; _purchaseMethod = null; _fromDate = _toDate = null; + if (_grid != null) await _grid.ReloadServerData(); + } +} diff --git a/src/BackOffice/Pages/Payment/LedgerTransactions.razor b/src/BackOffice/Pages/Payment/LedgerTransactions.razor new file mode 100644 index 0000000..2ea08eb --- /dev/null +++ b/src/BackOffice/Pages/Payment/LedgerTransactions.razor @@ -0,0 +1,246 @@ +@page "/payment/ledger-transactions" +@using BackOffice.Common.BaseComponents +@using CMSMicroservice.Protobuf.Protos +@using CMSMicroservice.Protobuf.Protos.Transactions +@using Google.Protobuf.WellKnownTypes + +@inject TransactionsContract.TransactionsContractClient TransactionsClient + + + + + + گزارش read-only دفتر تراکنش‌های مالی (جدول Transactions). برای جزئیات درگاه پرداخت به «گزارش تراکنش درگاه» مراجعه کنید. + + + + + + + + + + + + + + موفق + در انتظار + رد شده + + + + + خرید + واریز درگاه + واریز خارجی + برداشت + کمیسیون شبکه + فعال‌سازی باشگاه + شارژ کیف تخفیف + خرید فروشگاه تخفیف + واریز کیف جادویی + بونوس کیف جادویی + شارژ کیف اصلی + + + + + + + + + + + تعداد + @_summaryTotalCount.ToString("N0") + + + + + جمع مبلغ (تومان) + @_summaryTotalAmount.ToString("N0") + + + + + موفق + @_summarySuccess.ToString("N0") + + + + + در انتظار + @_summaryPending.ToString("N0") + + + + + رد شده + @_summaryReject.ToString("N0") + + + + + + + دفتر تراکنش‌های مالی + + CSV + + + + + + + @ReportLabels.GetTransactionTypeLabel(context.Item.HasType ? context.Item.Type : null) + + + + + @context.Item.Amount.ToString("N0") + + + + + + @ReportLabels.GetTransactionPaymentStatusLabel(context.Item.HasPaymentStatus ? context.Item.PaymentStatus : null) + + + + + + + @(context.Item.PaymentDate != null + ? context.Item.PaymentDate.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() + : "—") + + + + @context.Item.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() + + + + + با فیلتر فعلی موردی یافت نشد — بازه تاریخ یا فیلترها را تغییر دهید. + + + + + + + + + +@code { + private BasePageComponent? _basePage; + private MudDataGrid? _dataGrid; + private long? _id; + private string? _refId, _description; + private PaymentStatus? _paymentStatus; + private TransactionType? _type; + private DateTime? _createdFrom, _createdTo; + private long _summaryTotalCount, _summaryTotalAmount, _summarySuccess, _summaryPending, _summaryReject; + + private GetAllTransactionsByFilterFilter BuildFilter() + { + var filter = new GetAllTransactionsByFilterFilter + { + Id = _id > 0 ? _id : null, + RefId = string.IsNullOrWhiteSpace(_refId) ? null : _refId, + Description = string.IsNullOrWhiteSpace(_description) ? null : _description, + CreatedFrom = _createdFrom.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_createdFrom.Value, DateTimeKind.Utc)) : null, + CreatedTo = _createdTo.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_createdTo.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc)) : null + }; + + if (_paymentStatus.HasValue) + filter.PaymentStatus = _paymentStatus.Value; + else + filter.ClearPaymentStatusItem(); + + if (_type.HasValue) + filter.Type = _type.Value; + else + filter.ClearTypeItem(); + + return filter; + } + + private async Task LoadSummary() + { + try + { + var s = await TransactionsClient.GetTransactionSummaryAsync(new GetTransactionSummaryRequest { Filter = BuildFilter() }); + _summaryTotalCount = s.TotalCount; + _summaryTotalAmount = s.TotalAmount; + _summarySuccess = s.SuccessCount; + _summaryPending = s.PendingCount; + _summaryReject = s.RejectCount; + } + catch { } + } + + private async Task> LoadData(GridState state) + { + try + { + await LoadSummary(); + var result = await TransactionsClient.GetAllTransactionsByFilterAsync(new GetAllTransactionsByFilterRequest + { + PaginationState = new PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize }, + SortBy = "-Created", + Filter = BuildFilter() + }); + return new GridData + { + Items = result?.Models?.ToList() ?? new(), + TotalItems = (int)(result?.MetaData?.TotalCount ?? 0) + }; + } + catch (Exception ex) + { + Snackbar.Add($"خطا: {ex.Message}", Severity.Error); + return new GridData(); + } + } + + private async Task OnFilterSubmit() { if (_dataGrid != null) await _dataGrid.ReloadServerData(); } + private async Task OnFilterCleared() + { + _id = null; _refId = _description = null; + _paymentStatus = null; _type = null; + _createdFrom = _createdTo = null; + if (_dataGrid != null) await _dataGrid.ReloadServerData(); + } + + private async Task ExportCsv() + { + try + { + var result = await TransactionsClient.GetAllTransactionsByFilterAsync(new GetAllTransactionsByFilterRequest + { + PaginationState = new PaginationState { PageNumber = 1, PageSize = 5000 }, + SortBy = "-Created", + Filter = BuildFilter() + }); + var sb = new System.Text.StringBuilder(); + sb.AppendLine("Id,Type,Amount,Description,Status,RefId,PaymentDate,Created"); + foreach (var r in result.Models) + { + var type = r.HasType ? ReportLabels.GetTransactionTypeLabel(r.Type) : ""; + var status = r.HasPaymentStatus ? ReportLabels.GetTransactionPaymentStatusLabel(r.PaymentStatus) : ""; + var paymentDate = r.PaymentDate != null ? r.PaymentDate.ToDateTime().ToString("yyyy-MM-dd HH:mm") : ""; + sb.AppendLine($"{r.Id},{type},{r.Amount},\"{r.Description}\",{status},{r.RefId},{paymentDate},{r.Created.ToDateTime():yyyy-MM-dd HH:mm}"); + } + await jsRuntime.InvokeVoidAsync("jsSaveAsFile", "ledger-transactions.csv", sb.ToString()); + } + catch (Exception ex) { Snackbar.Add($"خطا در export: {ex.Message}", Severity.Error); } + } +} diff --git a/src/BackOffice/Pages/Payment/Transactions.razor b/src/BackOffice/Pages/Payment/Transactions.razor index ac7aef5..f25ecd1 100644 --- a/src/BackOffice/Pages/Payment/Transactions.razor +++ b/src/BackOffice/Pages/Payment/Transactions.razor @@ -1,78 +1,115 @@ @page "/payment/transactions" @using BackOffice.Common.BaseComponents +@using BackOffice.Pages.AutoComplete +@using CMSMicroservice.Protobuf.Protos.PaymentTransaction +@using Google.Protobuf.WellKnownTypes + +@inject PaymentTransactionContract.PaymentTransactionContractClient PaymentTxClient - - + + + گزارش read-only تراکنش‌های درگاه پرداخت. برای ثبت پرداخت دستی به «پرداخت‌های دستی» مراجعه کنید. + - - - در انتظار - موفق - ناموفق - بازگشت وجه + + + + + + + + + + + + + + + + + + + + + موفق + ناموفق / در انتظار - - نامشخص - خرید پکیج - پرداخت دستی - شارژ کیف‌پول - بازگشت وجه - + - + + + + تعداد + @_summaryTotalCount.ToString("N0") + + + + + جمع مبلغ (تومان) + @_summaryTotalAmount.ToString("N0") + + + + + موفق + @_summarySuccessful.ToString("N0") + + + + + در انتظار + @_summaryPending.ToString("N0") + + + + + + + گزارش تراکنش درگاه + + CSV + - - - - @context.Item.Amount.ToString("N0") - - + + + + + @context.Item.Amount.ToString("N0") + + + + - - @GetStatusText(context.Item.PaymentStatus) + + @ReportLabels.GetPaymentStatusLabel(context.Item.PaymentStatus) - - - @context.Item.PaymentDate.ToLocalTime().MiladiToJalaliWithTime() - - - - - @GetTypeText(context.Item.Type) - - - - - - - - + + + + @context.Item.CreatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() - موردی یافت نشد. + + با فیلتر فعلی موردی یافت نشد — بازه تاریخ یا کاربر را تغییر دهید. + - @@ -80,3 +117,87 @@ +@code { + private BasePageComponent? _basePage; + private MudDataGrid? _dataGrid; + private long? _userId; + private string? _mobile, _authority, _refId, _orderId, _gateway; + private bool? _paymentStatus; + private DateTime? _fromDate, _toDate; + private long _summaryTotalCount, _summaryTotalAmount, _summarySuccessful, _summaryPending; + + private GetAllPaymentTransactionByFilterFilter BuildFilter() => new() + { + UserId = _userId > 0 ? _userId : null, + Mobile = string.IsNullOrWhiteSpace(_mobile) ? null : _mobile, + Authority = string.IsNullOrWhiteSpace(_authority) ? null : _authority, + RefId = string.IsNullOrWhiteSpace(_refId) ? null : _refId, + OrderId = string.IsNullOrWhiteSpace(_orderId) ? null : _orderId, + GatewayProvider = string.IsNullOrWhiteSpace(_gateway) ? null : _gateway, + PaymentStatus = _paymentStatus, + CreatedFrom = _fromDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_fromDate.Value, DateTimeKind.Utc)) : null, + CreatedTo = _toDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_toDate.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc)) : null + }; + + private async Task LoadSummary() + { + try + { + var s = await PaymentTxClient.GetPaymentTransactionSummaryAsync(new GetPaymentTransactionSummaryRequest { Filter = BuildFilter() }); + _summaryTotalCount = s.TotalCount; + _summaryTotalAmount = s.TotalAmount; + _summarySuccessful = s.SuccessfulCount; + _summaryPending = s.PendingCount; + } + catch { } + } + + private async Task> LoadData(GridState state) + { + try + { + await LoadSummary(); + var result = await PaymentTxClient.GetAllPaymentTransactionByFilterAsync(new GetAllPaymentTransactionByFilterRequest + { + PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize }, + Filter = BuildFilter() + }); + return new GridData + { + Items = result?.Models?.ToList() ?? new(), + TotalItems = (int)(result?.MetaData?.TotalCount ?? 0) + }; + } + catch (Exception ex) + { + Snackbar.Add($"خطا: {ex.Message}", Severity.Error); + return new GridData(); + } + } + + private async Task OnFilterSubmit() { if (_dataGrid != null) await _dataGrid.ReloadServerData(); } + private async Task OnFilterCleared() + { + _userId = null; _mobile = _authority = _refId = _orderId = _gateway = null; + _paymentStatus = null; _fromDate = _toDate = null; + if (_dataGrid != null) await _dataGrid.ReloadServerData(); + } + + private async Task ExportCsv() + { + try + { + var result = await PaymentTxClient.GetAllPaymentTransactionByFilterAsync(new GetAllPaymentTransactionByFilterRequest + { + PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = 1, PageSize = 5000 }, + Filter = BuildFilter() + }); + var sb = new System.Text.StringBuilder(); + sb.AppendLine("Id,UserId,Mobile,Gateway,Amount,Authority,RefId,Status,OrderId,Created"); + foreach (var r in result.Models) + sb.AppendLine($"{r.Id},{r.UserId},{r.Mobile},{r.GatewayProvider},{r.Amount},{r.Authority},{r.RefId},{r.PaymentStatus},{r.OrderId},{r.CreatedAt.ToDateTime():yyyy-MM-dd HH:mm}"); + await jsRuntime.InvokeVoidAsync("jsSaveAsFile", "payment-transactions.csv", sb.ToString()); + } + catch (Exception ex) { Snackbar.Add($"خطا در export: {ex.Message}", Severity.Error); } + } +} diff --git a/src/BackOffice/Pages/Payment/Transactions.razor.cs b/src/BackOffice/Pages/Payment/Transactions.razor.cs deleted file mode 100644 index 0dd1825..0000000 --- a/src/BackOffice/Pages/Payment/Transactions.razor.cs +++ /dev/null @@ -1,95 +0,0 @@ -using BackOffice.Common.BaseComponents; -using MudBlazor; - -namespace BackOffice.Pages.Payment; - -public partial class Transactions -{ - private BasePageComponent? _basePage; - private MudDataGrid? _dataGrid; - - private DateTime? _fromDate; - private DateTime? _toDate; - private int? _statusFilter; - private int? _typeFilter; - - private async Task> LoadData(GridState state) - { - // TODO: Connect to BackOffice.BFF Transactions when API is ready - await Task.CompletedTask; - - return new GridData - { - Items = Array.Empty(), - TotalItems = 0 - }; - } - - private async Task OnFilterSubmit() - { - if (_dataGrid != null) - await _dataGrid.ReloadServerData(); - } - - private async Task OnFilterCleared() - { - _fromDate = null; - _toDate = null; - _statusFilter = null; - _typeFilter = null; - if (_dataGrid != null) - await _dataGrid.ReloadServerData(); - } - - private void OpenDetails(TransactionModel model) - { - // TODO: Open TransactionDetailsDialog with model - } - - private static string GetStatusText(int status) - { - return status switch - { - 0 => "در انتظار", - 1 => "موفق", - 2 => "ناموفق", - 3 => "بازگشت وجه", - _ => "نامشخص" - }; - } - - private static Color GetStatusColor(int status) - { - return status switch - { - 0 => Color.Warning, - 1 => Color.Success, - 2 => Color.Error, - 3 => Color.Info, - _ => Color.Default - }; - } - - private static string GetTypeText(int type) - { - return type switch - { - 1 => "خرید پکیج", - 2 => "پرداخت دستی", - 3 => "شارژ کیف‌پول", - 4 => "بازگشت وجه", - _ => "نامشخص" - }; - } - - private class TransactionModel - { - public long Id { get; set; } - public string? RefId { get; set; } - public long Amount { get; set; } - public int PaymentStatus { get; set; } - public DateTime PaymentDate { get; set; } - public int Type { get; set; } - } -} - diff --git a/src/BackOffice/Shared/AppBreadcrumb.razor b/src/BackOffice/Shared/AppBreadcrumb.razor index e6d7712..630a226 100644 --- a/src/BackOffice/Shared/AppBreadcrumb.razor +++ b/src/BackOffice/Shared/AppBreadcrumb.razor @@ -96,6 +96,7 @@ // Payment "payment" => "پرداخت", "transactions" => "تراکنش‌ها", + "ledger-transactions" => "دفتر تراکنش‌های مالی", "manual-payments" => "پرداخت دستی", // Network diff --git a/src/BackOffice/Shared/NavMenu.razor b/src/BackOffice/Shared/NavMenu.razor index 6a61a93..77418c8 100644 --- a/src/BackOffice/Shared/NavMenu.razor +++ b/src/BackOffice/Shared/NavMenu.razor @@ -75,7 +75,7 @@ - اعضا و آمار + اعضا و گزارش‌ها } + + گزارش تراکنش درگاه + + + + دفتر تراکنش‌های مالی + +