Compare commits

..

21 Commits

Author SHA1 Message Date
masoodafar-web 3eff5b6dfa Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 10m5s
2026-08-09 02:33:49 +03:30
masoodafar-web aa97d25302 feat(report): optimize customer package purchases report by removing pagination and enhancing filter application
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m55s
- Simplified the data retrieval process by eliminating pagination, allowing for all matching records to be fetched in a single request.
- Improved filter application logic to streamline the report generation process.
- Updated the handling of the result set to ensure compatibility with the new data retrieval method.
2026-08-09 00:40:36 +03:30
masoodafar-web 24ae4ec15c feat(date-range-picker): enhance date range picker functionality with external parameters and notifications
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 7m20s
- Added support for external From and To parameters to synchronize date range selections.
- Implemented OnParametersSet to handle updates from parent components without triggering unnecessary updates.
- Introduced NotifyRangeChangedAsync to streamline notifications for date range changes.
- Refactored date range application logic to improve clarity and maintainability.
2026-08-09 00:02:54 +03:30
masoodafar-web c3ee46ff62 feat(club): update club reports and navigation with new customer package insights
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m14s
- Updated ReportLabels.cs to refine purchase method labels for clarity.
- Enhanced ClubHub.razor by adding a new tab for customer package reports and adjusting tab indices.
- Modified ClubPackagePurchasesReport.razor to reflect updated purchase method options.
- Updated NavMenu.razor to include a link to the new customer packages report.
- Bumped Foursat.CMSMicroservice.Protobuf version to 0.0.208 for compatibility with recent changes.
2026-08-08 21:59:30 +03:30
masoodafar-web e9f8328404 feat(export): enhance CSV and Excel export functionality across multiple pages
- Updated CsvExportHelper to support Excel XML format for exporting data, improving compatibility with Excel.
- Refactored export logic in DiscountOrdersMainPage, SalesReports, OrderSalesReports, and UserOrderMainPage to utilize the new Excel export method.
- Changed file extensions from .csv to .xls for better clarity in exported files.
- Improved header and row data handling for consistency in exported reports.
2026-08-08 21:43:54 +03:30
masoodafar-web 6a9d142dc6 refactor(csv-export): streamline CSV export logic and remove redundant methods
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m36s
- Replaced manual CSV string construction with CsvExportHelper for better readability and maintainability across DiscountOrdersMainPage, SalesReports, OrderSalesReports, and UserOrderMainPage.
- Removed obsolete EscapeCsv methods to simplify the codebase.
- Enhanced CSV export functionality to utilize a unified base64 conversion method for file saving.
2026-08-07 18:40:00 +03:30
masoodafar-web 1d78633e17 feat(order-status): add "Ready for Office Pickup" status and update related components
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m50s
- Introduced a new order status "Ready for Office Pickup" across various components including DiscountOrdersMainPage, SalesReports, and UserOrder pages.
- Updated the color and text representations for the new status in relevant dialogs and services.
- Enhanced the DiscountOrderService to handle the new status in delivery processing logic.
- Bumped Foursat.CMSMicroservice.Protobuf version to 0.0.207 for compatibility with the new order status features.
2026-08-07 17:35:19 +03:30
masoodafar-web cbc61ad0f5 feat(order-details): add mobile display logic and remove unused status methods
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m36s
- Introduced a new property to display the user's mobile number or fallback to the address phone number in OrderDetailsDialog.
- Removed obsolete methods for determining order status color and text, streamlining the component's code.
2026-08-07 17:10:30 +03:30
masoodafar-web 68d0aed468 feat(invoice): add print functionality for order invoices and enhance order details
- Implemented a print button in OrderDetailsDialog and UserOrderDetailsDialog for generating invoices.
- Updated DiscountOrdersMainPage to populate order details from the list.
- Enhanced DiscountOrderDetailsDto to include UserFullName, UserMobile, and VatAmount for invoice generation.
- Integrated JavaScript functionality to convert HTML invoices to PDF format.
2026-08-07 17:08:23 +03:30
masoodafar-web a9aa03d6a8 feat(payment): enhance order details with payment status indicators and update proto version
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 9m37s
- Updated OrderDetailsDialog.razor to display payment status using dynamic color and icon based on the payment status value.
- Added helper methods in OrderDetailsDialog.razor.cs to determine payment status color, text, and icon.
- Enhanced UserOrderDetailsDialog.razor to conditionally display postal code and user mobile information.
- Updated DiscountOrderService to include PaymentStatusValue from the response.
- Bumped Foursat.CMSMicroservice.Protobuf version to 0.0.206 for compatibility with new payment features.
2026-08-07 15:52:55 +03:30
masoodafar-web e63b80fe47 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Failing after 24m44s
2026-07-26 00:52:14 +03:30
masoodafar-web 4115f64fff feat(payment): add manual credit charge functionality and update related components
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
- Introduced new manual credit charge endpoints in ADMIN-SERVICES.md.
- Updated ReportLabels.cs and LedgerTransactions.razor to include manual credit wallet charge options.
- Enhanced NavMenu.razor with a link to the manual credit charges page.
- Bumped Foursat.CMSMicroservice.Protobuf version to 0.0.205 for compatibility with new features.
2026-07-25 23:54:57 +03:30
masoodafar-web 7d892c2f6a Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Failing after 20m45s
2026-06-26 19:30:31 +03:30
masoodafar-web d757c8220c docs: update ADMIN-SERVICES.md with new ledger transaction reports and additional details
- Added new transaction report paths and clarified their purposes in the documentation.
- Updated the proto version to 0.0.203 and included deployment notes for the new ledger transactions feature.
- Enhanced read-only report descriptions to include CSV export capabilities.
2026-06-26 19:30:20 +03:30
masoodafar-web b53d9facc9 feat(transactions): add ledger transactions feature and update related components
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
- Introduced new ledger transactions page in NavMenu.razor and AppBreadcrumb.razor.
- Enhanced ReportLabels.cs with transaction status and type labels.
- Updated ConfigureService.cs to include TransactionsContractClient for gRPC.
- Bumped Foursat.CMSMicroservice.Protobuf version to 0.0.203 for compatibility with new features.
- Added deployment notes for June 1405 regarding read-only reports and proto updates.
2026-06-26 16:38:58 +03:30
masoodafar-web 5a004e5c88 feat(club): enhance club management with new reports and update navigation
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 19m16s
- Added new report tabs for package purchases, cycles, and history in ClubHub.razor.
- Updated MemberDetailsDialog.razor to improve display of membership details and alert for history access.
- Introduced PaymentTransaction reporting in Transactions.razor with enhanced filtering options and summary statistics.
- Updated BackOffice.csproj to conditionally reference the CMS Protobuf project for local development.
- Modified NavMenu.razor to include a link to the new transaction report page.
2026-06-22 23:38:23 +03:30
masoodafar-web 5d83eb4e05 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 5m32s
2026-06-20 23:46:06 +03:30
masoodafar-web 6126ecf023 chore: update .gitignore, change imagePullPolicy to Always in backoffice deployment, and add new columns for ChangeDiscountValue and CurrentDiscountBalance in WalletManagementPage. Bump Foursat.CMSMicroservice.Protobuf to 0.0.200.
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
2026-06-20 23:38:14 +03:30
masoodafar-web 31f0fc4860 fix(k8s): change imagePullPolicy to IfNotPresent in backoffice deployment
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 14m45s
2026-06-14 00:08:31 +03:30
masoodafar-web 0a2c5f913b Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 6m49s
2026-06-08 02:09:47 +03:30
masoodafar-web 2f040cab02 refactor: update terminology for team bonuses across multiple components
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 14m2s
- Changed "پاداش تیمی" to "پاداش های دریافتی" in WithdrawalReports, ManualPayments, ManualPaymentDialog, and WalletManagementPage for consistency and clarity.
2026-06-08 02:09:33 +03:30
43 changed files with 2978 additions and 647 deletions
+421
View File
@@ -0,0 +1,421 @@
# سرویس‌های BackOffice (پنل ادمین)
این سند تمام سرویس‌هایی را که **BackOffice** (فرانت ادمین) به **CMS** از طریق gRPC Gateway (`GwUrl`) فراخوانی می‌کند، به ترتیب **Use Case** فهرست می‌کند.
- **مسیر wrapper سرویس‌ها:** `BackOffice/src/BackOffice/Services/`
- **ثبت gRPC:** `BackOffice/src/BackOffice/Common/Configure/ConfigureService.cs`
- **تاریخ:** ۱۴۰۵/۰۳/۱۷ (بروز: خرداد ۱۴۰۵ — گزارش‌های read-only + proto 0.0.201؛ **بروز ۱۴۰۵/۰۳/۱۸ — دفتر تراکنش‌های مالی proto 0.0.203**)
---
## ۱. ورود و احراز هویت ادمین
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 1.1 | `OtpTokenContract.CreateNewOtpToken` | `OtpTokenContract` | `/Login/` (`LoginPage`) | ارسال OTP ورود ادمین |
| 1.2 | `UserContract.VerifyOtpToken` | `UserContract` | `/VerifyCodePage/{Mobile}` | تأیید OTP و دریافت JWT |
| 1.3 | `OtpTokenContract.CreateNewOtpToken` (resend) | `OtpTokenContract` | `/VerifyCodePage/{Mobile}` | ارسال مجدد OTP |
| 1.4 | `AuthorizationService.HasPermissionAsync` | — (JWT Role/Claim) | `NavMenu`, صفحات محافظت‌شده | کنترل نمایش منو بر اساس نقش (فعلاً role-based، نه API) |
| 1.5 | `AppTokenProvider` / `TokenHandler` | — (localStorage) | سراسری | attach کردن Bearer token به همه gRPC callها |
---
## ۲. داشبورد
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 2.1 | `UserContract.GetAllUserByFilter` | `UserContract` | `/` (Index) | شمارش کاربران |
| 2.2 | `ProductsContract.GetAllProductsByFilter` | `ProductsContract` | `/` | شمارش محصولات |
| 2.3 | `PackageContract.GetAllPackageByFilter` | `PackageContract` | `/` | شمارش پکیج‌ها |
| 2.4 | `CategoryContract.GetAllCategoryByFilter` | `CategoryContract` | `/` | شمارش دسته‌بندی‌ها |
| 2.5 | `CommissionContract.GetAvailableWeeks` | `CommissionContract` | `/` | هفته جاری کمیسیون |
| 2.6 | `CommissionContract.GetWeeklyCommissionPool` | `CommissionContract` | `/` | وضعیت pool هفته جاری |
| 2.7 | `ClubMembershipContract.GetAllClubMemberships` | `ClubMembershipContract` | `/` | آمار اعضای باشگاه |
| 2.8 | `UserOrderContract.GetAllUserOrderByFilter` | `UserOrderContract` | `/` | سفارش‌های اخیر |
| 2.9 | `IDiscountOrderService.GetOrdersAsync` | `DiscountOrderContract` | `Dashboard/DiscountShopWidget` | ویجت سفارش‌های فروشگاه تخفیفی |
| 2.10 | `CommissionContract.GetWithdrawalRequests` | `CommissionContract` | `NavMenu` | badge درخواست‌های برداشت معلق |
| 2.11 | `GlobalSearch``UserContract.GetAllUserByFilter` | `UserContract` | `MainLayout/GlobalSearch` | جستجوی سریع کاربر + ناوبری صفحات |
---
## ۳. مدیریت کاربران
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 3.1 | `UserContract.GetAllUserByFilter` | `UserContract` | `/UserPage/`, `UserAutoComplete`, GlobalSearch | لیست/جستجوی کاربران |
| 3.2 | `UserContract.DeleteUser` | `UserContract` | `/UserPage/` | حذف کاربر |
| 3.3 | Export users | `UserContract.GetAllUserByFilter` | `/UserPage/` | خروجی CSV کاربران |
---
## ۴. نقش‌ها و دسترسی‌ها
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 4.1 | `RoleContract.GetAllRoleByFilter` | `RoleContract` | `/RolePage/`, `RoleAutoComplete` | لیست نقش‌ها |
| 4.2 | `RoleContract.CreateNewRole` | `RoleContract` | `Role/Components/CreateDialog` | ایجاد نقش |
| 4.3 | `RoleContract.UpdateRole` | `RoleContract` | `Role/Components/UpdateDialog` | ویرایش نقش |
| 4.4 | `RoleContract.DeleteRole` | `RoleContract` | `/RolePage/` | حذف نقش |
| 4.5 | `RolePermissionsDialog` | — (UI فقط) | `/RolePage/` | ویرایش permissionها — **هنوز به API وصل نشده** |
---
## ۵. نقش کاربر و آدرس
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 5.1 | `UserRoleContract.GetAllUserRoleByFilter` | `UserRoleContract` | `/UserRolePage/{UserId}` | نقش‌های یک کاربر |
| 5.2 | `UserRoleContract.CreateNewUserRole` | `UserRoleContract` | CreateDialog, `UserRoleDialog` | تخصیص نقش |
| 5.3 | `UserRoleContract.UpdateUserRole` | `UserRoleContract` | UpdateDialog | ویرایش نقش کاربر |
| 5.4 | `UserRoleContract.DeleteUserRole` | `UserRoleContract` | `/UserRolePage/{UserId}` | حذف نقش کاربر |
| 5.5 | `RoleContract.GetAllRoleByFilter` | `RoleContract` | `UserRoleDialog`, `RoleTitleColumn` | نمایش عنوان نقش |
| 5.6 | `UserAddressContract.GetAllUserAddressByFilter` | `UserAddressContract` | `/UserAddressPage/{UserId}` | آدرس‌های کاربر |
| 5.7 | `UserAddressContract.CreateNewUserAddress` | `UserAddressContract` | CreateDialog | ثبت آدرس |
| 5.8 | `UserAddressContract.UpdateUserAddress` | `UserAddressContract` | UpdateDialog | ویرایش آدرس |
| 5.9 | `UserAddressContract.DeleteUserAddress` | `UserAddressContract` | `/UserAddressPage/{UserId}` | حذف آدرس |
---
## ۶. پکیج‌ها
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 6.1 | `PackageContract.GetAllPackageByFilter` | `PackageContract` | `/PackagePage/`, `PackageSelect`, ManualPaymentDialog | لیست پکیج‌ها |
| 6.2 | `PackageContract.CreateNewPackage` | `PackageContract` | `Package/Components/CreateDialog` | ایجاد پکیج |
| 6.3 | `PackageContract.UpdatePackage` | `PackageContract` | `Package/Components/UpdateDialog` | ویرایش پکیج |
| 6.4 | `PackageContract.DeletePackage` | `PackageContract` | `/PackagePage/` | حذف پکیج |
| 6.5 | `ConfigurationContract.GetClubFeatures` | `ConfigurationContract` | Create/Update Dialog | انتخاب featureهای باشگاه مرتبط با پکیج |
---
## ۷. فروشگاه اصلی — محصولات و دسته‌بندی
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 7.1 | `ProductsContract.GetAllProductsByFilter` | `ProductsContract` | `/ProductsPage/`, BulkEdit, AutoComplete | لیست محصولات |
| 7.2 | `ProductsContract.CreateNewProducts` | `ProductsContract` | `Products/Components/CreateDialog` | ایجاد محصول |
| 7.3 | `ProductsContract.UpdateProducts` | `ProductsContract` | UpdateDialog | ویرایش محصول |
| 7.4 | `ProductsContract.DeleteProducts` | `ProductsContract` | `/ProductsPage/` | حذف محصول |
| 7.5 | `ProductsContract.ToggleProductStatus` | `ProductsContract` | `/ProductsPage/`, BulkEdit | فعال/غیرفعال |
| 7.6 | `ProductsContract.BulkUpdateProductPrices` | `ProductsContract` | `/ProductsBulkEditPage/` | ویرایش گروهی قیمت |
| 7.7 | `ProductsContract.GetProductGallery` / Add/Remove Image | `ProductsContract` | `GalleryDialog` | گالری تصاویر محصول |
| 7.8 | `ProductsContract.GetProducts` / `UpdateProductCategories` | `ProductsContract` | `/ProductCategoriesPage/{ProductId}` | drag-drop دسته‌بندی محصول |
| 7.9 | `CategoryContract.GetAllCategoryByFilter` | `CategoryContract` | `/CategoryPage/`, AutoComplete | لیست دسته‌ها |
| 7.10 | `CategoryContract.GetCategory` / Create / Update / Delete | `CategoryContract` | CategoryMainPage, Dialogs | CRUD دسته‌بندی |
| 7.11 | `ProductsContract.GetProductsForCategory` / `UpdateCategoryProducts` | `ProductsContract` | `/CategoryProductsPage/{CategoryId}` | drag-drop محصولات یک دسته |
---
## ۸. انبارداری (Inventory)
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 8.1 | `InventoryContract.GetAllWarehouses` | `InventoryContract` | `/InventoryPage/`, Warehouses, LowStock | لیست انبارها |
| 8.2 | `InventoryContract.GetAllInventoryItems` | `InventoryContract` | `/InventoryPage/` | موجودی کالا |
| 8.3 | `InventoryContract.GetLowStockItems` | `InventoryContract` | `/InventoryPage/`, `/InventoryLowStockPage/` | اقلام کم‌موجود |
| 8.4 | `InventoryContract.AddStock` | `InventoryContract` | `AddStockDialog` | ورود کالا به انبار |
| 8.5 | `InventoryContract.AdjustStock` | `InventoryContract` | `AdjustStockDialog` | تعدیل موجودی |
| 8.6 | `InventoryContract.RecordLoss` | `InventoryContract` | `RecordLossDialog` | ثبت ضایعات |
| 8.7 | `InventoryContract.CreateWarehouse` | `InventoryContract` | `CreateWarehouseDialog` | ایجاد انبار |
| 8.8 | `InventoryContract.UpdateInventorySettings` | `InventoryContract` | `InventorySettingsDialog` | تنظیمات انبار |
| 8.9 | `InventoryContract.GetStockMovements` | `InventoryContract` | `/InventoryMovementsPage/` | تاریخچه جابجایی موجودی |
---
## ۹. سفارش‌های فروشگاه اصلی
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 9.1 | `UserOrderContract.GetAllUserOrderByFilter` | `UserOrderContract` | `/OrdersPage/`, `/UserOrderPage/{UserId}`, SalesReports | لیست سفارش‌ها |
| 9.2 | `UserOrderContract.GetUserOrder` | `UserOrderContract` | OrderDetailsDialog, Index | جزئیات سفارش |
| 9.3 | `UserOrderContract.UpdateUserOrder` | `UserOrderContract` | UserOrderDetailsDialog, ChangeOrderStatus | ویرایش سفارش / tracking |
| 9.4 | `UserOrderContract.UpdateOrderStatus` | `UserOrderContract` | ChangeOrderStatusDialog | تغییر وضعیت تحویل |
| 9.5 | `UserOrderContract.CancelOrder` | `UserOrderContract` | CancelOrderDialog | لغو سفارش |
| 9.6 | `UserOrderContract.DeleteUserOrder` | `UserOrderContract` | `/UserOrderPage/{UserId}` | حذف سفارش |
| 9.7 | `UserOrderContract.ApplyDiscountToOrder` | `UserOrderContract` | ApplyDiscountDialog | اعمال تخفیف روی سفارش |
---
## ۱۰. پرداخت دستی و تراکنش‌ها
> **دو گزارش تراکنش — اشتباه نگیرید:**
>
> | مسیر | جدول 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 | `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` (زیرتب دوره) | تاریخچه تغییرات دوره |
---
## ۱۱. کیف پول و تاریخچه
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 11.1 | `UserWalletContract.GetAllUserWalletByFilter` | `UserWalletContract` | `/wallets` | لیست کیف پول کاربران |
| 11.2 | `UserWalletHistoryContract.GetAllUserWalletHistoryByFilter` | `UserWalletHistoryContract` | `/wallets` (تب تاریخچه) | لاگ تغییرات کیف پول |
| 11.3 | `UserWalletContract.AdminManualCreditCharge` | `UserWalletContract` | `/payment/manual-credit-charges` | شارژ دستی کیف پول اصلی (Balance) |
| 11.4 | `UserWalletContract.GetManualCreditCharges` | `UserWalletContract` | `/payment/manual-credit-charges` | لیست شارژهای دستی (Type=17) |
---
## ۱۲. قراردادهای کاربران
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 12.1 | `UserContractContract.GetAllUserContractByFilter` | `UserContractContract` | `/contracts` | لیست قراردادهای امضاشده (اصلی / باشگاه) |
| 12.2 | Download PDF | — (local JS) | `/contracts` | دانلود PDF قرارداد ذخیره‌شده |
---
## ۱۳. کمیسیون و پاداش
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 13.1 | `CommissionContract.GetAvailableWeeks` | `CommissionContract` | `/commission/dashboard`, WeekNumberPicker | انتخاب هفته |
| 13.2 | `CommissionContract.GetWeeklyCommissionPool` | `CommissionContract` | `/commission/dashboard` | جزئیات pool هفتگی |
| 13.3 | `CommissionContract.TriggerWeeklyCalculation` | `CommissionContract` | Dashboard, WorkerControl | اجرای دستی محاسبه هفتگی |
| 13.4 | `CommissionContract.GetAllWeeklyPools` | `CommissionContract` | `/commission/reports` | گزارش poolهای هفتگی |
| 13.5 | `CommissionContract.GetUserCommissionPayouts` | `CommissionContract` | `/commission/payouts` | payoutهای کاربران |
| 13.6 | `CommissionContract.ProcessWithdrawal` | `CommissionContract` | UserPayouts, WithdrawalRequests | پردازش/واریز برداشت |
| 13.7 | `CommissionContract.BulkCreditPayouts` | `CommissionContract` | `/commission/payouts` | واریز گروهی payoutها |
| 13.8 | `CommissionContract.GetWithdrawalRequests` | `CommissionContract` | `/commission/withdrawals`, NavMenu | درخواست‌های برداشت کاربران |
| 13.9 | `CommissionContract.ApproveWithdrawal` | `CommissionContract` | WithdrawalRequests | تأیید درخواست برداشت |
| 13.10 | `CommissionContract.RejectWithdrawal` | `CommissionContract` | WithdrawalRequests | رد درخواست برداشت |
| 13.11 | `CommissionContract.GetWithdrawalReports` | `CommissionContract` | `/commission/withdrawal-reports` | گزارش برداشت‌ها |
| 13.12 | `CommissionContract.GetUserWeeklyBalances` | `CommissionContract` | `/network/balances` | گزارش بالانس هفتگی کاربران |
| 13.13 | `CommissionContract.GetWorkerStatus` | `CommissionContract` | `/system/worker-control` | وضعیت worker محاسبه |
| 13.14 | `CommissionContract.GetWorkerExecutionLogs` | `CommissionContract` | `/system/worker-control` | لاگ اجرای worker |
---
## ۱۴. شبکه فروش (MLM)
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 14.1 | `NetworkMembershipContract.GetNetworkTree` | `NetworkMembershipContract` | `/network/tree` | درخت شبکه (admin view) |
| 14.2 | `NetworkMembershipContract.GetUserNetwork` | `NetworkMembershipContract` | `/network/user-info/{UserId}` | اطلاعات شبکه یک کاربر |
| 14.3 | `NetworkMembershipContract.GetNetworkMembershipHistory` | `NetworkMembershipContract` | UserNetworkInfo | تاریخچه عضویت شبکه |
| 14.4 | `NetworkMembershipContract.ChangeNetworkParent` | `NetworkMembershipContract` | `ChangeParentDialog` | تغییر والد در درخت |
| 14.5 | `NetworkMembershipContract.GetNetworkStatistics` | `NetworkMembershipContract` | `/network/statistics` | آمار کلی شبکه (admin) |
---
## ۱۵. باشگاه مشتریان
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 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` | `/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های یک کاربر |
| 15.8 | `ClubMembershipContract.ToggleUserClubFeature` | `ClubMembershipContract` | `/club/features` | فعال/غیرفعال feature برای کاربر |
---
## ۱۶. فروشگاه تخفیفی (Discount Shop)
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 16.1 | `IDiscountProductService` — CRUD | `DiscountProductContract` | `/discount-products` | مدیریت محصولات تخفیفی |
| 16.2 | `IDiscountCategoryService` — CRUD | `DiscountCategoryContract` | `/discount-categories` | مدیریت دسته‌بندی تخفیفی |
| 16.3 | `IDiscountOrderService.GetOrdersAsync` | `DiscountOrderContract.GetAllDiscountOrders` | `/discount-orders`, Dashboard widget | لیست سفارش‌ها (admin) |
| 16.4 | `IDiscountOrderService.GetByIdAsync` | `DiscountOrderContract` | OrderDetailsDialog | جزئیات سفارش |
| 16.5 | `IDiscountOrderService.UpdateStatusAsync` | `DiscountOrderContract` | Orders page, OrderDetailsDialog | تغییر وضعیت پرداخت/ارسال |
| 16.6 | `IDiscountOrderService.GetSalesReportAsync` | `DiscountOrderContract` | `/discount-sales-reports` | گزارش فروش |
| 16.7 | `DiscountProductContract.GetDiscountProducts` | `DiscountProductContract` | `DiscountProductsAutoComplete` | autocomplete محصول |
---
## ۱۷. تگ محصولات
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 17.1 | `ITagService.GetTagsAsync` | `TagContract` | `/tags` | لیست تگ‌ها |
| 17.2 | `ITagService.CreateAsync` / `UpdateAsync` / `DeleteAsync` | `TagContract` | TagEditDialog, TagManagement | CRUD تگ |
| 17.3 | `ITagService.GetProductTagsAsync` | `ProductTagContract` | AssignTagsDialog | تگ‌های یک محصول |
| 17.4 | `ITagService.AssignToProductAsync` / `RemoveProductTagAsync` | `ProductTagContract` | AssignTagsDialog, Discount products | تخصیص/حذف تگ |
---
## ۱۸. پیام‌های عمومی
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 18.1 | `IPublicMessageService.GetMessagesAsync` | `PublicMessageContract` | `/public-messages` | لیست پیام‌ها |
| 18.2 | `IPublicMessageService.GetByIdAsync` | `PublicMessageContract` | PublicMessages | جزئیات |
| 18.3 | `IPublicMessageService.CreateAsync` / `UpdateAsync` | `PublicMessageContract` | MessageFormDialog | ایجاد/ویرایش |
| 18.4 | `IPublicMessageService.PublishAsync` / `ArchiveAsync` | `PublicMessageContract` | PublicMessages | انتشار/بایگانی |
| 18.5 | `IPublicMessageService.DeleteAsync` | `PublicMessageContract` | PublicMessages | حذف |
---
## ۱۹. بلاگ
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 19.1 | `IBlogPostService.GetAllAsync` | `BlogPostContract` | `/blog/posts` | لیست پست‌ها |
| 19.2 | `IBlogPostService.GetByIdAsync` | `BlogPostContract` | BlogPostEditDialog | جزئیات پست |
| 19.3 | `IBlogPostService.CreateAsync` / `UpdateAsync` | `BlogPostContract` | BlogPostEditDialog | ایجاد/ویرایش |
| 19.4 | `IBlogPostService.DeleteAsync` | `BlogPostContract` | BlogPostManagement | حذف |
| 19.5 | `IBlogPostService.PublishAsync` / `ArchiveAsync` | `BlogPostContract` | BlogPostManagement | انتشار/بایگانی |
| 19.6 | `IBlogCategoryService` — CRUD | `BlogCategoryContract` | `/blog/categories` | مدیریت دسته‌بندی بلاگ |
---
## ۲۰. مدیریت محتوای سایت
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 20.1 | `ISitePageSettingsService.GetAllAsync` | `SitePageSettingsContract` | `/content/page-settings` | لیست صفحات (landing, about, contact, licenses) |
| 20.2 | `ISitePageSettingsService.GetByKeyAsync` | `SitePageSettingsContract` | PageSettings dialogs | جزئیات یک صفحه |
| 20.3 | `ISitePageSettingsService.SaveSettingsAsync` | `SitePageSettingsContract` | PageSettingsEditDialog | ذخیره hero + JSON تنظیمات |
| 20.4 | `ISitePageSettingsService.SaveImageAsync` / `DeleteImageAsync` | `SitePageSettingsContract` | PageImagesDialog | مدیریت تصاویر گروه‌بندی‌شده |
| 20.5 | `ISitePageService` — CRUD sections | `SitePageContract` | `/content/pages` | صفحات داینامیک قدیمی (section-based) |
---
## ۲۱. تنظیمات سیستم و سلامت
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 21.1 | `ConfigurationContract.GetAllConfigurations` | `ConfigurationContract` | `/system/configuration` | خواندن key-value تنظیمات (کمیسیون، شبکه، باشگاه، VAT…) |
| 21.2 | `ConfigurationContract.CreateOrUpdateConfiguration` | `ConfigurationContract` | `/system/configuration` | ذخیره تنظیمات |
| 21.3 | `HealthContract.GetSystemHealth` | `HealthContract` | `/system/health` | وضعیت سلامت سرویس‌ها |
| 21.4 | `AlertsMonitoring` | — (mock) | `/system/alerts` | **TODO — API هشدار هنوز وصل نشده** |
---
## ۲۲. نسخه اپلیکیشن
| # | سرویس / متد | gRPC Contract | صفحه / کامپوننت | کاربرد |
|---|-------------|---------------|-----------------|--------|
| 22.1 | `IAppVersionService.GetAllAsync` | `AppVersionContract` | `/settings/app-versions` | لیست نسخه FrontOffice/App |
| 22.2 | `IAppVersionService.GetByNameAsync` | `AppVersionContract` | AppVersions | جزئیات یک app |
| 22.3 | `IAppVersionService.UpdateAsync` | `AppVersionContract.UpdateAppVersion` | AppVersions dialog | به‌روزرسانی نسخه + force update |
---
## ۲۳. کامپوننت‌های مشترک (AutoComplete / Hub)
| کامپوننت | Contract | کاربرد |
|----------|----------|--------|
| `UserAutoComplete` | `UserContract` | جستجوی کاربر در فرم‌ها |
| `ProductsAutoComplete` | `ProductsContract` | جستجوی محصول |
| `CategoryAutoComplete` / MultiSelect | `CategoryContract` | انتخاب دسته |
| `PackageSelect` | `PackageContract` | انتخاب پکیج |
| `WeekNumberPicker` | `CommissionContract` | انتخاب هفته |
| `DiscountProductsAutoComplete` | `DiscountProductContract` | جستجوی محصول تخفیفی |
| Hub pages (`ClubHub`, `DiscountShopHub`, `SystemHub`, `OrdersHub`, `BlogHub`) | — | مسیریابی tab-based؛ RPC در child component |
---
## ۲۴. سرویس‌های ثبت‌شده ولی بدون استفاده / ناقص
| سرویس / صفحه | وضعیت |
|--------------|--------|
| `IBlogPostImageService` (`BlogPostImageContract`) | ثبت در DI؛ **هیچ صفحه‌ای inject نمی‌کند** |
| `RolePermissionsDialog` | UI permission؛ **ذخیره در backend TODO** |
| `AuthorizationService` | فقط JWT role؛ **Permission API از CMS/BFF TODO** |
| `/system/alerts` | داده mock؛ **AlertContract وجود ندارد** |
---
## نمودار جریان ورود ادمین
```mermaid
flowchart LR
A[LoginPage] -->|CreateNewOtpToken| B[SMS OTP]
B --> C[VerifyCodePage]
C -->|VerifyOtpToken| D[JWT in localStorage]
D --> E[All gRPC via TokenHandler]
```
---
## فهرست gRPC Contractهای فعال BackOffice
| Contract | Wrapper / استفاده مستقیم |
|----------|---------------------------|
| `OtpTokenContract` | Login |
| `UserContract` | Login, Users, Dashboard, Search |
| `RoleContract` | Roles, UserRoles |
| `UserRoleContract` | UserRole pages |
| `UserAddressContract` | UserAddress pages |
| `PackageContract` | Packages, ManualPayment |
| `ProductsContract` | Products, Categories drag-drop |
| `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 |
| `CommissionContract` | Commission, Network balances, Worker |
| `NetworkMembershipContract` | Network tree/info |
| `ClubMembershipContract` | Club members/features |
| `ConfigurationContract` | System config, Club features, Package |
| `DiscountProductContract` | DiscountProductService |
| `DiscountCategoryContract` | DiscountCategoryService |
| `DiscountOrderContract` | DiscountOrderService |
| `TagContract` / `ProductTagContract` | TagService |
| `PublicMessageContract` | PublicMessageService |
| `InventoryContract` | Inventory pages |
| `BlogPostContract` | BlogPostService |
| `BlogCategoryContract` | BlogCategoryService |
| `BlogPostImageContract` | BlogPostImageService *(unused)* |
| `SitePageSettingsContract` | SitePageSettingsService |
| `SitePageContract` | SitePageService |
| `AppVersionContract` | AppVersionService |
| `HealthContract` | Health dashboard |
---
## مقایسه با FrontOffice
| موضوع | FrontOffice (مشتری) | BackOffice (ادمین) |
|-------|----------------------|-------------------|
| Auth | OTP + JWT (UserContract) | OTP + JWT (همان flow) |
| Orders | Customer RPC | Admin filter + status management |
| Discount Shop | User orders + checkout | Full CRUD + admin order management |
| Commission | Read-only (my payouts) | Full admin (calculation, approval, reports) |
| Network | My tree only | Full tree + change parent |
| Content | Read page settings | Edit page settings + blog + public messages |
| 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
+1
View File
@@ -1,3 +1,4 @@
# Docs moved to totalDoc # Docs moved to totalDoc
See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation. See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation.
+6 -3
View File
@@ -138,9 +138,12 @@
<!-- This enables direct communication with CMS (no BFF needed) --> <!-- This enables direct communication with CMS (no BFF needed) -->
<!-- ============================================================ --> <!-- ============================================================ -->
<!-- CMS Protobuf NuGet package (used for both local dev and Docker builds) --> <!-- CMS Protobuf: NuGet in CI/Docker; ProjectReference for local monorepo dev -->
<ItemGroup> <ItemGroup Condition="Exists('../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj')">
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.199" /> <ProjectReference Include="../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj" />
</ItemGroup>
<ItemGroup Condition="!Exists('../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj')">
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.208" />
</ItemGroup> </ItemGroup>
<!-- ============================================================ --> <!-- ============================================================ -->
@@ -8,21 +8,46 @@ namespace BackOffice.Common.BaseComponents;
public partial class DateRangePicker public partial class DateRangePicker
{ {
private DateRange _dateRange = new(); private DateRange _dateRange = new();
private MudDateRangePicker _picker = default!;
private DateTime? _lastSyncedFrom;
private DateTime? _lastSyncedTo;
private MudDateRangePicker _picker;
[Parameter] public string Label { get; set; } = "انتخاب بازه زمانی"; [Parameter] public string Label { get; set; } = "انتخاب بازه زمانی";
[Parameter] public DateTime? DefaultStart { get; set; } [Parameter] public DateTime? DefaultStart { get; set; }
[Parameter] public DateTime? DefaultEnd { get; set; } [Parameter] public DateTime? DefaultEnd { get; set; }
[Parameter] public EventCallback<DateRange> OnChanged { get; set; } [Parameter] public EventCallback<DateRange> OnChanged { get; set; }
[Parameter] public DateTime? From { get; set; }
[Parameter] public EventCallback<DateTime?> FromChanged { get; set; }
[Parameter] public DateTime? To { get; set; }
[Parameter] public EventCallback<DateTime?> ToChanged { get; set; }
protected override void OnInitialized() protected override void OnInitialized()
{ {
base.OnInitialized(); base.OnInitialized();
if (DefaultStart.HasValue) var start = From ?? DefaultStart;
_dateRange.Start = DefaultStart.Value.Date; var end = To ?? DefaultEnd;
ApplyExternalRange(start, end);
if (DefaultEnd.HasValue) _lastSyncedFrom = From;
_dateRange.End = DefaultEnd.Value.Date; _lastSyncedTo = To;
} }
protected override void OnParametersSet()
{
// فقط وقتی والد From/To را عوض کرده (مثلاً پاک کردن فیلتر)، نه هنگام انتخاب داخل پیکر
if (From == _lastSyncedFrom && To == _lastSyncedTo)
return;
_lastSyncedFrom = From;
_lastSyncedTo = To;
ApplyExternalRange(From, To);
}
private void ApplyExternalRange(DateTime? start, DateTime? end)
{
_dateRange = new DateRange(start?.Date, end?.Date);
}
public CultureInfo GetPersianCulture() public CultureInfo GetPersianCulture()
{ {
var culture = new CultureInfo("fa-IR"); var culture = new CultureInfo("fa-IR");
@@ -53,19 +78,35 @@ public partial class DateRangePicker
culture.NumberFormat.NumberNegativePattern = 0; culture.NumberFormat.NumberNegativePattern = 0;
return culture; return culture;
} }
private async Task OnClickOK() private async Task OnClickOK()
{ {
if (_picker.DateRange is not null) var range = _picker.DateRange ?? _dateRange;
{ _dateRange = range ?? new DateRange();
await OnChanged.InvokeAsync(_picker.DateRange); await NotifyRangeChangedAsync(_dateRange);
}
await _picker.CloseAsync(); await _picker.CloseAsync();
} }
private async Task OnClickClear() private async Task OnClickClear()
{ {
_dateRange = new DateRange(); _dateRange = new DateRange();
await OnChanged.InvokeAsync(_dateRange); await NotifyRangeChangedAsync(_dateRange);
await _picker.CloseAsync(); await _picker.CloseAsync();
} }
private async Task NotifyRangeChangedAsync(DateRange range)
{
var start = range.Start?.Date;
var end = range.End?.Date;
_lastSyncedFrom = start;
_lastSyncedTo = end;
if (From != start)
await FromChanged.InvokeAsync(start);
if (To != end)
await ToChanged.InvokeAsync(end);
await OnChanged.InvokeAsync(range);
}
} }
@@ -31,6 +31,11 @@ using CMSMicroservice.Protobuf.Protos.SitePageSettings;
using CMSMicroservice.Protobuf.Protos.UserContract; using CMSMicroservice.Protobuf.Protos.UserContract;
using CMSMicroservice.Protobuf.Protos.UserWallet; using CMSMicroservice.Protobuf.Protos.UserWallet;
using CMSMicroservice.Protobuf.Protos.UserWalletHistory; 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.Common.Utilities;
using BackOffice.Services.DiscountProduct; using BackOffice.Services.DiscountProduct;
using BackOffice.Services.DiscountCategory; using BackOffice.Services.DiscountCategory;
@@ -169,6 +174,13 @@ public static class ConfigureServices
services.AddTransient(sp => new UserWalletContract.UserWalletContractClient(sp.GetRequiredService<CallInvoker>())); services.AddTransient(sp => new UserWalletContract.UserWalletContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new UserWalletHistoryContract.UserWalletHistoryContractClient(sp.GetRequiredService<CallInvoker>())); services.AddTransient(sp => new UserWalletHistoryContract.UserWalletHistoryContractClient(sp.GetRequiredService<CallInvoker>()));
// Admin Report Services
services.AddTransient(sp => new PaymentTransactionContract.PaymentTransactionContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new TransactionsContract.TransactionsContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new UserPackagePurchaseContract.UserPackagePurchaseContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new ClubMembershipCycleContract.ClubMembershipCycleContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new ClubMembershipCycleHistoryContract.ClubMembershipCycleHistoryContractClient(sp.GetRequiredService<CallInvoker>()));
return services; return services;
} }
@@ -0,0 +1,145 @@
using System.Globalization;
using System.Net;
using System.Text;
namespace BackOffice.Common.Utilities;
/// <summary>
/// خروجی سازگار با Excel فارسی:
/// - XML Spreadsheet (.xls) برای ستون‌بندی مطمئن + متن فارسی
/// - CSV با جداکننده ; به‌عنوان جایگزین سبک
/// </summary>
public static class CsvExportHelper
{
/// <summary>جداکننده پیش‌فرض ویندوز فارسی برای CSV.</summary>
public const char Separator = ';';
public static string Escape(string? value)
{
if (string.IsNullOrEmpty(value))
return string.Empty;
// همیشه quote تا Excel فارسی ستون را نشکند
return $"\"{value.Replace("\"", "\"\"")}\"";
}
public static string Cell(object? value) => value switch
{
null => Escape(string.Empty),
string s => Escape(s),
bool b => b ? "1" : "0",
byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal
=> Convert.ToString(value, CultureInfo.InvariantCulture) ?? "0",
IFormattable f => Escape(f.ToString(null, CultureInfo.InvariantCulture) ?? string.Empty),
_ => Escape(Convert.ToString(value, CultureInfo.InvariantCulture))
};
public static string Row(params object?[] cells) =>
string.Join(Separator, cells.Select(Cell));
/// <summary>
/// نام نمایشی مشتری؛ اگر خالی باشد موبایل یا شناسه کاربر.
/// </summary>
public static string CustomerName(string? fullName, string? mobile = null, long? userId = null)
{
if (!string.IsNullOrWhiteSpace(fullName))
return fullName.Trim();
if (!string.IsNullOrWhiteSpace(mobile))
return mobile.Trim();
if (userId is > 0)
return $"کاربر {userId}";
return "—";
}
public static string ToBase64(StringBuilder body)
{
// CRLF صریح — Excel روی ویندوز با \n لینوکس گاهی به‌هم می‌ریزد
var text = body.ToString().Replace("\r\n", "\n").Replace("\r", "\n").Replace("\n", "\r\n");
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(text)).ToArray();
return Convert.ToBase64String(bytes);
}
/// <summary>
/// خروجی SpreadsheetML که Excel به‌صورت ستون‌ستون و با UTF-8 درست باز می‌کند.
/// فایل را با پسوند .xls ذخیره کنید.
/// </summary>
public static string ToExcelXmlBase64(string sheetName, IReadOnlyList<string> headers, IEnumerable<object?[]> rows)
{
var sb = new StringBuilder();
sb.AppendLine(@"<?xml version=""1.0"" encoding=""UTF-8""?>");
sb.AppendLine(@"<?mso-application progid=""Excel.Sheet""?>");
sb.AppendLine(@"<Workbook xmlns=""urn:schemas-microsoft-com:office:spreadsheet""");
sb.AppendLine(@" xmlns:o=""urn:schemas-microsoft-com:office:office""");
sb.AppendLine(@" xmlns:x=""urn:schemas-microsoft-com:office:excel""");
sb.AppendLine(@" xmlns:ss=""urn:schemas-microsoft-com:office:spreadsheet""");
sb.AppendLine(@" xmlns:html=""http://www.w3.org/TR/REC-html40"">");
sb.AppendLine(@"<Styles>");
sb.AppendLine(@"<Style ss:ID=""Header""><Font ss:Bold=""1""/></Style>");
sb.AppendLine(@"<Style ss:ID=""Text""><NumberFormat ss:Format=""@""/></Style>");
sb.AppendLine(@"</Styles>");
sb.Append("<Worksheet ss:Name=\"").Append(XmlEscape(sheetName)).AppendLine("\">");
sb.AppendLine("<Table>");
sb.AppendLine("<Row>");
foreach (var header in headers)
{
sb.Append("<Cell ss:StyleID=\"Header\"><Data ss:Type=\"String\">")
.Append(XmlEscape(header))
.AppendLine("</Data></Cell>");
}
sb.AppendLine("</Row>");
foreach (var row in rows)
{
sb.AppendLine("<Row>");
foreach (var cell in row)
AppendExcelCell(sb, cell);
sb.AppendLine("</Row>");
}
sb.AppendLine("</Table>");
sb.AppendLine("</Worksheet>");
sb.AppendLine("</Workbook>");
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
return Convert.ToBase64String(bytes);
}
private static void AppendExcelCell(StringBuilder sb, object? value)
{
switch (value)
{
case null:
sb.AppendLine(@"<Cell ss:StyleID=""Text""><Data ss:Type=""String""></Data></Cell>");
break;
case string s:
sb.Append(@"<Cell ss:StyleID=""Text""><Data ss:Type=""String"">")
.Append(XmlEscape(s))
.AppendLine("</Data></Cell>");
break;
case bool b:
sb.Append(@"<Cell><Data ss:Type=""Boolean"">")
.Append(b ? "1" : "0")
.AppendLine("</Data></Cell>");
break;
case byte or sbyte or short or ushort or int or uint or long or ulong:
sb.Append(@"<Cell><Data ss:Type=""Number"">")
.Append(Convert.ToString(value, CultureInfo.InvariantCulture))
.AppendLine("</Data></Cell>");
break;
case float or double or decimal:
sb.Append(@"<Cell><Data ss:Type=""Number"">")
.Append(Convert.ToString(value, CultureInfo.InvariantCulture))
.AppendLine("</Data></Cell>");
break;
default:
sb.Append(@"<Cell ss:StyleID=""Text""><Data ss:Type=""String"">")
.Append(XmlEscape(Convert.ToString(value, CultureInfo.InvariantCulture)))
.AppendLine("</Data></Cell>");
break;
}
}
private static string XmlEscape(string? value) =>
WebUtility.HtmlEncode(value ?? string.Empty);
}
@@ -0,0 +1,161 @@
using System.Globalization;
using System.Net;
using System.Text;
using DateTimeConverterCL;
namespace BackOffice.Common.Utilities;
/// <summary>ساخت HTML فاکتور برای چاپ مرورگر (jsHtmlToPdf).</summary>
public static class OrderInvoicePrint
{
public sealed class InvoiceModel
{
public string ShopTitle { get; set; } = "فروشگاه";
public string OrderId { get; set; } = "";
public string? CustomerName { get; set; }
public string? CustomerMobile { get; set; }
public string? CustomerNationalCode { get; set; }
public string? Address { get; set; }
public string? PostalCode { get; set; }
public string? PaymentStatus { get; set; }
public string? PaymentMethod { get; set; }
public string? TransactionId { get; set; }
public string? TrackingCode { get; set; }
public DateTime? Created { get; set; }
public DateTime? PaymentDate { get; set; }
public long Subtotal { get; set; }
public long DiscountTotal { get; set; }
public long CreditUsed { get; set; }
public long GatewayAmount { get; set; }
public long VatAmount { get; set; }
public long TotalAmount { get; set; }
public List<InvoiceLine> Lines { get; set; } = new();
}
public sealed class InvoiceLine
{
public string Title { get; set; } = "";
public int Quantity { get; set; }
public long UnitPrice { get; set; }
public long Discount { get; set; }
public long LineTotal { get; set; }
}
public static string BuildHtml(InvoiceModel model)
{
var sb = new StringBuilder();
sb.Append("<!DOCTYPE html><html lang=\"fa\" dir=\"rtl\"><head><meta charset=\"utf-8\"/>");
sb.Append("<title>").Append(Esc($"فاکتور {model.OrderId}")).Append("</title>");
sb.Append("""
<style>
* { box-sizing: border-box; }
body { font-family: Tahoma, "Segoe UI", sans-serif; color: #111; margin: 24px; font-size: 13px; }
h1 { font-size: 20px; margin: 0 0 4px; }
.muted { color: #555; }
.header { display: flex; justify-content: space-between; align-items: flex-start; border-bottom: 2px solid #222; padding-bottom: 12px; margin-bottom: 16px; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 24px; margin-bottom: 16px; }
.label { color: #666; font-size: 11px; }
.value { font-weight: 600; }
table { width: 100%; border-collapse: collapse; margin: 12px 0 16px; }
th, td { border: 1px solid #ccc; padding: 8px; text-align: right; }
th { background: #f3f3f3; }
.num { text-align: left; font-variant-numeric: tabular-nums; white-space: nowrap; }
.totals { width: 320px; margin-right: auto; }
.totals td { border: none; padding: 4px 0; }
.totals .grand { font-size: 15px; font-weight: 700; border-top: 2px solid #222; padding-top: 8px; }
.footer { margin-top: 28px; font-size: 11px; color: #666; border-top: 1px dashed #bbb; padding-top: 8px; }
@media print {
body { margin: 0; }
.no-print { display: none !important; }
}
</style>
""");
sb.Append("</head><body>");
sb.Append("<div class=\"header\"><div>");
sb.Append("<h1>").Append(Esc(model.ShopTitle)).Append("</h1>");
sb.Append("<div class=\"muted\">فاکتور فروش</div></div><div>");
sb.Append("<div><span class=\"label\">شماره فاکتور: </span><span class=\"value\">").Append(Esc(model.OrderId)).Append("</span></div>");
if (model.Created.HasValue)
sb.Append("<div><span class=\"label\">تاریخ ثبت: </span>").Append(Esc(FormatDate(model.Created.Value))).Append("</div>");
if (model.PaymentDate.HasValue)
sb.Append("<div><span class=\"label\">تاریخ پرداخت: </span>").Append(Esc(FormatDate(model.PaymentDate.Value))).Append("</div>");
sb.Append("</div></div>");
sb.Append("<div class=\"grid\">");
AppendField(sb, "نام مشتری", model.CustomerName);
AppendField(sb, "موبایل", model.CustomerMobile);
AppendField(sb, "کد ملی", model.CustomerNationalCode);
AppendField(sb, "کد پستی", model.PostalCode);
AppendField(sb, "وضعیت پرداخت", model.PaymentStatus);
AppendField(sb, "روش پرداخت", model.PaymentMethod);
AppendField(sb, "کد تراکنش", model.TransactionId);
AppendField(sb, "کد رهگیری", model.TrackingCode);
if (!string.IsNullOrWhiteSpace(model.Address))
{
sb.Append("<div style=\"grid-column:1/-1\"><div class=\"label\">آدرس</div><div class=\"value\">")
.Append(Esc(model.Address)).Append("</div></div>");
}
sb.Append("</div>");
sb.Append("<table><thead><tr>");
sb.Append("<th>#</th><th>کالا</th><th>تعداد</th><th>قیمت واحد</th><th>تخفیف</th><th>مبلغ</th>");
sb.Append("</tr></thead><tbody>");
var i = 1;
foreach (var line in model.Lines)
{
sb.Append("<tr>");
sb.Append("<td class=\"num\">").Append(i++).Append("</td>");
sb.Append("<td>").Append(Esc(line.Title)).Append("</td>");
sb.Append("<td class=\"num\">").Append(line.Quantity).Append("</td>");
sb.Append("<td class=\"num\">").Append(Money(line.UnitPrice)).Append("</td>");
sb.Append("<td class=\"num\">").Append(Money(line.Discount)).Append("</td>");
sb.Append("<td class=\"num\">").Append(Money(line.LineTotal)).Append("</td>");
sb.Append("</tr>");
}
if (model.Lines.Count == 0)
sb.Append("<tr><td colspan=\"6\" class=\"muted\">آیتمی ثبت نشده است</td></tr>");
sb.Append("</tbody></table>");
sb.Append("<table class=\"totals\"><tbody>");
if (model.Subtotal > 0)
AppendTotalRow(sb, "جمع اقلام", model.Subtotal);
if (model.DiscountTotal > 0)
AppendTotalRow(sb, "تخفیف", model.DiscountTotal);
if (model.VatAmount > 0)
AppendTotalRow(sb, "مالیات بر ارزش افزوده", model.VatAmount);
if (model.CreditUsed > 0)
AppendTotalRow(sb, "پرداخت از کیف اعتباری", model.CreditUsed);
if (model.GatewayAmount > 0)
AppendTotalRow(sb, "پرداخت درگاه / نقدی", model.GatewayAmount);
sb.Append("<tr class=\"grand\"><td>مبلغ قابل پرداخت</td><td class=\"num\">")
.Append(Money(model.TotalAmount)).Append(" تومان</td></tr>");
sb.Append("</tbody></table>");
sb.Append("<div class=\"footer\">چاپ‌شده از پنل ادمین — ").Append(Esc(FormatDate(DateTime.Now))).Append("</div>");
sb.Append("</body></html>");
return sb.ToString();
}
private static void AppendField(StringBuilder sb, string label, string? value)
{
if (string.IsNullOrWhiteSpace(value)) return;
sb.Append("<div><div class=\"label\">").Append(Esc(label)).Append("</div><div class=\"value\">")
.Append(Esc(value)).Append("</div></div>");
}
private static void AppendTotalRow(StringBuilder sb, string label, long amount)
{
sb.Append("<tr><td>").Append(Esc(label)).Append("</td><td class=\"num\">")
.Append(Money(amount)).Append(" تومان</td></tr>");
}
private static string Money(long amount) => amount.ToString("N0", CultureInfo.InvariantCulture);
private static string FormatDate(DateTime dt)
{
try { return dt.ToLocalTime().MiladiToJalaliWithTime(); }
catch { return dt.ToLocalTime().ToString("yyyy/MM/dd HH:mm"); }
}
private static string Esc(string? s) => WebUtility.HtmlEncode(s ?? "");
}
@@ -0,0 +1,83 @@
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 => "شارژ کیف اصلی",
TransactionType.ManualCreditWalletCharge => "شارژ دستی کیف اصلی",
_ => type?.ToString() ?? "—"
};
public static string GetPurchaseMethodLabel(int method) => method switch
{
1 => "دایا",
2 => "درگاه",
3 => "دستی",
_ => "نامشخص"
};
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 ? "بله" : "خیر")}";
}
+24 -3
View File
@@ -1,17 +1,34 @@
@page "/club" @page "/club"
@page "/club/members" @page "/club/members"
@page "/club/purchases"
@page "/club/customer-packages"
@page "/club/cycles"
@page "/club/history"
@page "/club/statistics" @page "/club/statistics"
@attribute [Authorize] @attribute [Authorize]
@using BackOffice.Pages.Club.Reports
<div> <div>
<MudText Typo="Typo.h5" Class="mb-3">باشگاه مشتریان</MudText> <MudText Typo="Typo.h5" Class="mb-3">باشگاه مشتریان</MudText>
<MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true" <MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true"
PanelClass="pt-4" @bind-ActivePanelIndex="_activeTab"> PanelClass="pt-4" @bind-ActivePanelIndex="_activeTab">
<MudTabPanel Text="اعضای باشگاه" Icon="@Icons.Material.Filled.Groups"> <MudTabPanel Text="اعضا" Icon="@Icons.Material.Filled.Groups">
<ClubMembers /> <ClubMembers />
</MudTabPanel> </MudTabPanel>
<MudTabPanel Text="آمار باشگاه" Icon="@Icons.Material.Filled.PieChart"> <MudTabPanel Text="گزارش خرید پکیج" Icon="@Icons.Material.Filled.ShoppingBag">
<ClubPackagePurchasesReport />
</MudTabPanel>
<MudTabPanel Text="گزارش مشتریان پکیج" Icon="@Icons.Material.Filled.PeopleAlt">
<CustomerPackagePurchasesReport />
</MudTabPanel>
<MudTabPanel Text="گزارش دوره‌ها" Icon="@Icons.Material.Filled.Loop">
<ClubCyclesReport />
</MudTabPanel>
<MudTabPanel Text="گزارش تاریخچه" Icon="@Icons.Material.Filled.History">
<ClubHistoriesReport />
</MudTabPanel>
<MudTabPanel Text="آمار" Icon="@Icons.Material.Filled.PieChart">
<Statistics /> <Statistics />
</MudTabPanel> </MudTabPanel>
</MudTabs> </MudTabs>
@@ -25,7 +42,11 @@
var uri = Navigation.ToBaseRelativePath(Navigation.Uri).ToLowerInvariant().TrimEnd('/'); var uri = Navigation.ToBaseRelativePath(Navigation.Uri).ToLowerInvariant().TrimEnd('/');
_activeTab = uri switch _activeTab = uri switch
{ {
"club/statistics" => 1, "club/purchases" => 1,
"club/customer-packages" => 2,
"club/cycles" => 3,
"club/history" => 4,
"club/statistics" => 5,
_ => 0 _ => 0
}; };
} }
@@ -33,23 +33,11 @@
</tr> </tr>
<tr> <tr>
<td><strong>پکیج:</strong></td> <td><strong>پکیج:</strong></td>
<td>@Member.PackageName</td> <td>@(string.IsNullOrEmpty(Member.PackageName) ? "—" : Member.PackageName)</td>
</tr>
<tr>
<td><strong>کد فعال‌سازی:</strong></td>
<td dir="ltr">@Member.ActivationCode</td>
</tr> </tr>
<tr> <tr>
<td><strong>تاریخ فعال‌سازی:</strong></td> <td><strong>تاریخ فعال‌سازی:</strong></td>
<td>@Member.ActivatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td> <td>@(Member.ActivatedAt != null ? Member.ActivatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() : "—")</td>
</tr>
<tr>
<td><strong>تاریخ انقضا:</strong></td>
<td>
<MudText Color="@(Member.IsExpired ? Color.Error : Color.Success)">
@Member.ExpiresAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()
</MudText>
</td>
</tr> </tr>
<tr> <tr>
<td><strong>وضعیت:</strong></td> <td><strong>وضعیت:</strong></td>
@@ -61,60 +49,17 @@
</MudChip> </MudChip>
</td> </td>
</tr> </tr>
<tr>
<td><strong>منقضی شده:</strong></td>
<td>
<MudChip T="string"
Color="@(Member.IsExpired ? Color.Warning : Color.Default)"
Size="Size.Small">
@(Member.IsExpired ? "بله" : "خیر")
</MudChip>
</td>
</tr>
</tbody>
</MudSimpleTable>
</MudPaper>
<MudPaper Class="pa-3" Elevation="0">
<MudText Typo="Typo.h6" Color="Color.Primary">تاریخچه</MudText>
<MudDivider Class="my-2" />
<MudSimpleTable Dense="true">
<tbody>
<tr> <tr>
<td><strong>تاریخ ایجاد:</strong></td> <td><strong>تاریخ ایجاد:</strong></td>
<td>@Member.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td> <td>@Member.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td>
</tr> </tr>
</tbody> </tbody>
</MudSimpleTable> </MudSimpleTable>
@if (_historyLoading)
{
<MudProgressLinear Indeterminate="true" Color="Color.Primary" Class="my-2" />
}
else if (_history.Any())
{
<MudTable T="ClubMembershipHistoryModel" Items="_history" Dense="true" Hover="true" Class="mt-2">
<HeaderContent>
<MudTh>عملیات</MudTh>
<MudTh>تاریخ</MudTh>
<MudTh>دلیل</MudTh>
<MudTh>توسط</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="عملیات">
<MudChip T="string" Size="Size.Small" Color="Color.Info">@context.Action</MudChip>
</MudTd>
<MudTd DataLabel="تاریخ">@context.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</MudTd>
<MudTd DataLabel="دلیل">@(string.IsNullOrEmpty(context.Reason) ? "-" : context.Reason)</MudTd>
<MudTd DataLabel="توسط">@(string.IsNullOrEmpty(context.PerformedBy) ? "-" : context.PerformedBy)</MudTd>
</RowTemplate>
</MudTable>
}
else
{
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="mt-2">تاریخچه‌ای یافت نشد.</MudText>
}
</MudPaper> </MudPaper>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text">
تاریخچه تغییرات عضویت در تب «گزارش تاریخچه» باشگاه قابل مشاهده است.
</MudAlert>
</MudStack> </MudStack>
} }
</DialogContent> </DialogContent>
@@ -126,39 +71,5 @@
@code { @code {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } [CascadingParameter] IMudDialogInstance MudDialog { get; set; }
[Parameter] public ClubMembershipModel Member { get; set; } [Parameter] public ClubMembershipModel Member { get; set; }
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubClient { get; set; }
private List<ClubMembershipHistoryModel> _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(); private void Close() => MudDialog.Close();
} }
@@ -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
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
گزارش دوره‌های عضویت باشگاه — هر خرید پکیج یک دور جدید ایجاد می‌کند.
</MudAlert>
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
<Filters>
<MudItem xs="12" sm="6" md="3">
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_userId" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="bool?" Clearable="true" Label="دوره جاری" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_isCurrent">
<MudSelectItem T="bool?" Value="@((bool?)true)">فقط دوره جاری</MudSelectItem>
<MudSelectItem T="bool?" Value="@((bool?)false)">دوره‌های قبلی</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="int?" Clearable="true" Label="وضعیت جادویی" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_magicState">
<MudSelectItem T="int?" Value="0">عادی</MudSelectItem>
<MudSelectItem T="int?" Value="1">در حالت جادویی</MudSelectItem>
<MudSelectItem T="int?" Value="2">جادویی تکمیل‌شده</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه خرید پکیج" />
</MudItem>
</Filters>
<Content>
<MudGrid Class="mb-3" Spacing="2">
<MudItem xs="4">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">کل دوره‌ها</MudText>
<MudText Typo="Typo.h6">@_totalCount.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="4">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">دوره جاری</MudText>
<MudText Typo="Typo.h6">@_currentCount.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="4">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">جادویی فعال</MudText>
<MudText Typo="Typo.h6">@_magicActive.ToString("N0")</MudText>
</MudPaper>
</MudItem>
</MudGrid>
<MudDataGrid @ref="_grid" T="ClubMembershipCycleModel" ServerData="LoadData" ReadOnly="true"
Hover="true" Dense="true" Height="calc(100vh - 420px)">
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<PropertyColumn Property="x => x.UserName" Title="کاربر" />
<PropertyColumn Property="x => x.CycleNumber" Title="شماره دور" />
<PropertyColumn Property="x => x.PackageName" Title="پکیج" />
<TemplateColumn Title="مبلغ (ریال)">
<CellTemplate>@context.Item.PackageAmount.ToString("N0")</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="روش خرید">
<CellTemplate>@ReportLabels.GetPurchaseMethodLabel(context.Item.PurchaseMethod)</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="تاریخ خرید">
<CellTemplate>@context.Item.PackagePurchasedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="وضعیت جادویی">
<CellTemplate>
@ReportLabels.GetMagicStateLabel(null,
context.Item.MagicStartedAt?.ToDateTime(),
context.Item.MagicCompletedAt?.ToDateTime())
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="دوره جاری">
<CellTemplate>
<MudChip T="string" Size="Size.Small" Color="@(context.Item.IsCurrentCycle ? Color.Success : Color.Default)">
@(context.Item.IsCurrentCycle ? "بله" : "خیر")
</MudChip>
</CellTemplate>
</TemplateColumn>
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="ClubMembershipCycleModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد در صفحه" />
</PagerContent>
</MudDataGrid>
</Content>
</BasePageComponent>
@code {
private BasePageComponent? _basePage;
private MudDataGrid<ClubMembershipCycleModel>? _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<GridData<ClubMembershipCycleModel>> LoadData(GridState<ClubMembershipCycleModel> 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<ClubMembershipCycleModel>
{
Items = result?.Models?.ToList() ?? new(),
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
return new GridData<ClubMembershipCycleModel>();
}
}
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();
}
}
@@ -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
<MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true" PanelClass="pt-4">
<MudTabPanel Text="تغییرات عضویت" Icon="@Icons.Material.Filled.History">
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
تاریخچه فعال/غیرفعال شدن عضویت باشگاه.
</MudAlert>
<MudStack Row="true" Spacing="2" Class="mb-3">
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_memUserId" />
<MudSelect T="int?" Clearable="true" Label="عملیات" Variant="Variant.Outlined" Margin="Margin.Dense"
@bind-Value="_memAction" Style="max-width:200px;">
<MudSelectItem T="int?" Value="0">فعال‌سازی</MudSelectItem>
<MudSelectItem T="int?" Value="1">غیرفعال‌سازی</MudSelectItem>
<MudSelectItem T="int?" Value="2">ویرایش</MudSelectItem>
<MudSelectItem T="int?" Value="3">اصلاح دستی</MudSelectItem>
</MudSelect>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ReloadMembership">جستجو</MudButton>
</MudStack>
<MudDataGrid @ref="_memGrid" T="ClubMembershipHistoryModel" ServerData="LoadMembershipHistory" ReadOnly="true"
Hover="true" Dense="true" Height="calc(100vh - 400px)">
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<PropertyColumn Property="x => x.UserName" Title="کاربر" />
<TemplateColumn Title="وضعیت">
<CellTemplate>@ReportLabels.FormatActiveChange(context.Item.OldIsActive, context.Item.NewIsActive)</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="مشارکت اولیه">
<CellTemplate>
@(context.Item.OldInitialContribution != null ? context.Item.OldInitialContribution.Value.ToString("N0") : "—")
→ @(context.Item.NewInitialContribution != null ? context.Item.NewInitialContribution.Value.ToString("N0") : "—")
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="عملیات">
<CellTemplate>@ReportLabels.GetClubMembershipActionLabel(context.Item.Action)</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.PerformedBy" Title="انجام‌دهنده" />
<PropertyColumn Property="x => x.Reason" Title="دلیل" />
<TemplateColumn Title="تاریخ">
<CellTemplate>@context.Item.CreatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
</TemplateColumn>
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="ClubMembershipHistoryModel" PageSizeOptions="@(new int[] { 20, 50, 100 })" />
</PagerContent>
</MudDataGrid>
</MudTabPanel>
<MudTabPanel Text="تغییرات دوره" Icon="@Icons.Material.Filled.Timeline">
<MudAlert Severity="Severity.Warning" Variant="Variant.Text" Dense="true" Class="mb-3">
ردیابی تغییرات دوره از زمان deploy فعال شد. رکوردهای قدیمی‌تر ممکن است خالی باشند.
</MudAlert>
<MudStack Row="true" Spacing="2" Class="mb-3">
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_cycleUserId" />
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ReloadCycle">جستجو</MudButton>
</MudStack>
<MudDataGrid @ref="_cycleGrid" T="ClubMembershipCycleHistoryModel" ServerData="LoadCycleHistory" ReadOnly="true"
Hover="true" Dense="true" Height="calc(100vh - 400px)">
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<PropertyColumn Property="x => x.UserName" Title="کاربر" />
<PropertyColumn Property="x => x.CycleNumber" Title="دور" />
<TemplateColumn Title="دوره جاری">
<CellTemplate>@ReportLabels.FormatBoolChange(context.Item.OldIsCurrentCycle, context.Item.NewIsCurrentCycle)</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="عملیات">
<CellTemplate>@ReportLabels.GetClubCycleActionLabel(context.Item.Action)</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.PerformedBy" Title="انجام‌دهنده" />
<PropertyColumn Property="x => x.Reason" Title="دلیل" />
<TemplateColumn Title="تاریخ">
<CellTemplate>@context.Item.CreatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
</TemplateColumn>
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="ClubMembershipCycleHistoryModel" PageSizeOptions="@(new int[] { 20, 50, 100 })" />
</PagerContent>
</MudDataGrid>
</MudTabPanel>
</MudTabs>
@code {
private MudDataGrid<ClubMembershipHistoryModel>? _memGrid;
private MudDataGrid<ClubMembershipCycleHistoryModel>? _cycleGrid;
private long? _memUserId, _cycleUserId;
private int? _memAction;
private async Task<GridData<ClubMembershipHistoryModel>> LoadMembershipHistory(GridState<ClubMembershipHistoryModel> 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<ClubMembershipHistoryModel>
{
Items = result?.Models?.ToList() ?? new(),
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
return new GridData<ClubMembershipHistoryModel>();
}
}
private async Task<GridData<ClubMembershipCycleHistoryModel>> LoadCycleHistory(GridState<ClubMembershipCycleHistoryModel> 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<ClubMembershipCycleHistoryModel>
{
Items = result?.Models?.ToList() ?? new(),
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
return new GridData<ClubMembershipCycleHistoryModel>();
}
}
private async Task ReloadMembership() { if (_memGrid != null) await _memGrid.ReloadServerData(); }
private async Task ReloadCycle() { if (_cycleGrid != null) await _cycleGrid.ReloadServerData(); }
}
@@ -0,0 +1,131 @@
@using BackOffice.Common.BaseComponents
@using BackOffice.Pages.AutoComplete
@using CMSMicroservice.Protobuf.Protos.UserPackagePurchase
@using Google.Protobuf.WellKnownTypes
@inject UserPackagePurchaseContract.UserPackagePurchaseContractClient PurchaseClient
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
گزارش read-only خریدهای پکیج — فقط مشاهده و فیلتر.
</MudAlert>
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
<Filters>
<MudItem xs="12" sm="6" md="3">
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_userId" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="int?" Clearable="true" Label="روش خرید" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_purchaseMethod">
<MudSelectItem T="int?" Value="1">دایا</MudSelectItem>
<MudSelectItem T="int?" Value="3">دستی</MudSelectItem>
<MudSelectItem T="int?" Value="2">درگاه</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه خرید" />
</MudItem>
</Filters>
<Content>
<MudGrid Class="mb-3" Spacing="2">
<MudItem xs="6" sm="4">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">تعداد</MudText>
<MudText Typo="Typo.h6">@_totalCount.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6" sm="4">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">جمع مبلغ (ریال)</MudText>
<MudText Typo="Typo.h6">@_totalAmount.ToString("N0")</MudText>
</MudPaper>
</MudItem>
</MudGrid>
<MudDataGrid @ref="_grid" T="UserPackagePurchaseModel" ServerData="LoadData" ReadOnly="true"
Hover="true" Dense="true" Height="calc(100vh - 420px)">
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<TemplateColumn Title="کاربر">
<CellTemplate>
<MudStack Spacing="0">
<MudText Typo="Typo.body2">@context.Item.UserName</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">@context.Item.UserMobile</MudText>
</MudStack>
</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.PackageName" Title="پکیج" />
<TemplateColumn Title="روش خرید">
<CellTemplate>@ReportLabels.GetPurchaseMethodLabel(context.Item.PurchaseMethod)</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="مبلغ (ریال)">
<CellTemplate>@context.Item.Amount.ToString("N0")</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="تاریخ خرید">
<CellTemplate>@context.Item.PurchasedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.OrderId" Title="OrderId" />
<PropertyColumn Property="x => x.TransactionId" Title="TransactionId" />
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="UserPackagePurchaseModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد در صفحه" />
</PagerContent>
</MudDataGrid>
</Content>
</BasePageComponent>
@code {
private BasePageComponent? _basePage;
private MudDataGrid<UserPackagePurchaseModel>? _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<GridData<UserPackagePurchaseModel>> LoadData(GridState<UserPackagePurchaseModel> 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<UserPackagePurchaseModel>
{
Items = result?.Models?.ToList() ?? new(),
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
return new GridData<UserPackagePurchaseModel>();
}
}
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();
}
}
@@ -0,0 +1,105 @@
@using CMSMicroservice.Protobuf.Protos.UserPackagePurchase
@using Google.Protobuf.WellKnownTypes
@using DateTimeConverterCL
@inject UserPackagePurchaseContract.UserPackagePurchaseContractClient PurchaseClient
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">
جزئیات خرید پکیج — @UserName
</MudText>
</TitleContent>
<DialogContent>
@if (_loading)
{
<MudProgressLinear Indeterminate="true" Color="Color.Primary" Class="my-4" />
}
else if (_items.Count == 0)
{
<MudAlert Severity="Severity.Info" Dense="true">خریدی برای این کاربر یافت نشد.</MudAlert>
}
else
{
<MudTable Items="_items" Dense="true" Hover="true" Breakpoint="Breakpoint.Sm">
<HeaderContent>
<MudTh>شناسه</MudTh>
<MudTh>پکیج</MudTh>
<MudTh>روش</MudTh>
<MudTh>مبلغ (ریال)</MudTh>
<MudTh>تاریخ خرید</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="شناسه">@context.Id</MudTd>
<MudTd DataLabel="پکیج">@context.PackageName</MudTd>
<MudTd DataLabel="روش">
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined"
Color="@GetMethodColor(context.PurchaseMethod)">
@ReportLabels.GetPurchaseMethodLabel(context.PurchaseMethod)
</MudChip>
</MudTd>
<MudTd DataLabel="مبلغ">@context.Amount.ToString("N0")</MudTd>
<MudTd DataLabel="تاریخ">
@(context.PurchasedAt != null
? context.PurchasedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()
: "-")
</MudTd>
</RowTemplate>
</MudTable>
}
</DialogContent>
<DialogActions>
<MudButton OnClick="Close" Variant="Variant.Text">بستن</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!;
[Parameter] public long UserId { get; set; }
[Parameter] public string UserName { get; set; } = string.Empty;
private bool _loading = true;
private List<UserPackagePurchaseModel> _items = new();
protected override async Task OnInitializedAsync()
{
try
{
var result = await PurchaseClient.GetAllUserPackagePurchaseByFilterAsync(
new GetAllUserPackagePurchaseByFilterRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = 1,
PageSize = 500
},
Filter = new GetAllUserPackagePurchaseByFilterFilter
{
UserId = UserId
}
});
_items = result?.Models?
.OrderBy(x => x.PurchasedAt?.ToDateTime() ?? DateTime.MinValue)
.ToList() ?? new();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری جزئیات: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
private static Color GetMethodColor(int method) => method switch
{
1 => Color.Info, // دایا
2 => Color.Primary, // درگاه
3 => Color.Secondary, // دستی
_ => Color.Default
};
private void Close() => MudDialog.Close();
}
@@ -0,0 +1,282 @@
@using System.Text
@using BackOffice.Common.BaseComponents
@using BackOffice.Common.Utilities
@using BackOffice.Pages.AutoComplete
@using BackOffice.Pages.Club.Reports.Components
@using CMSMicroservice.Protobuf.Protos.UserPackagePurchase
@using Google.Protobuf.WellKnownTypes
@using DateTimeConverterCL
@using Microsoft.JSInterop
@inject UserPackagePurchaseContract.UserPackagePurchaseContractClient PurchaseClient
@inject IDialogService DialogService
@inject IJSRuntime JsRuntime
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
گزارش تجمیعی مشتریان — اولین/آخرین پکیج، تفکیک دایا / دستی / درگاه. خروجی Excel مطابق فیلتر فعال است.
</MudAlert>
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
<Filters>
<MudItem xs="12" sm="6" md="3">
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_userId" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="int?" Clearable="true" Label="روش خرید" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_purchaseMethod">
<MudSelectItem T="int?" Value="1">دایا</MudSelectItem>
<MudSelectItem T="int?" Value="3">دستی</MudSelectItem>
<MudSelectItem T="int?" Value="2">درگاه</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه خرید" />
</MudItem>
</Filters>
<Content>
<MudStack Row="true" Justify="Justify.FlexEnd" Class="mb-3">
<MudButton Variant="Variant.Outlined" Color="Color.Success"
StartIcon="@Icons.Material.Filled.FileDownload"
OnClick="ExportToExcel" Disabled="_exporting">
خروجی Excel
</MudButton>
</MudStack>
<MudDataGrid @ref="_grid" T="CustomerPackagePurchaseRollupModel" ServerData="LoadData" ReadOnly="true"
Hover="true" Dense="true" Height="calc(100vh - 420px)">
<Columns>
<TemplateColumn Title="مشتری">
<CellTemplate>
<MudStack Spacing="0">
<MudText Typo="Typo.body2">@context.Item.UserName</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">@context.Item.UserMobile</MudText>
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="اولین پکیج">
<CellTemplate>
<MudStack Spacing="0">
<MudText Typo="Typo.body2">@context.Item.FirstPackageName</MudText>
<MudText Typo="Typo.caption">@context.Item.FirstAmount.ToString("N0") ریال</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">
@(context.Item.FirstPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-")
</MudText>
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="آخرین پکیج">
<CellTemplate>
<MudStack Spacing="0">
<MudText Typo="Typo.body2">@context.Item.LastPackageName</MudText>
<MudText Typo="Typo.caption">@context.Item.LastAmount.ToString("N0") ریال</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">
@(context.Item.LastPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-")
</MudText>
<MudChip T="string" Size="Size.Small" Class="mt-1" Variant="Variant.Filled"
Color="@GetMethodColor(context.Item.LastPurchaseMethod)">
@ReportLabels.GetPurchaseMethodLabel(context.Item.LastPurchaseMethod)
</MudChip>
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="تعداد / جمع">
<CellTemplate>
<MudStack Spacing="0">
<MudText>@context.Item.PurchaseCount خرید</MudText>
<MudText Typo="Typo.caption">@context.Item.TotalAmount.ToString("N0") ریال</MudText>
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="دایا">
<CellTemplate>
<MudText Typo="Typo.caption">@context.Item.DayaCount عدد</MudText>
<MudText Typo="Typo.caption">@context.Item.DayaAmount.ToString("N0")</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="دستی">
<CellTemplate>
<MudText Typo="Typo.caption">@context.Item.ManualCount عدد</MudText>
<MudText Typo="Typo.caption">@context.Item.ManualAmount.ToString("N0")</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="درگاه">
<CellTemplate>
<MudText Typo="Typo.caption">@context.Item.GatewayCount عدد</MudText>
<MudText Typo="Typo.caption">@context.Item.GatewayAmount.ToString("N0")</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="عملیات">
<CellTemplate>
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
OnClick="() => OpenDetails(context.Item)">
جزئیات
</MudButton>
</CellTemplate>
</TemplateColumn>
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="CustomerPackagePurchaseRollupModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد در صفحه" />
</PagerContent>
</MudDataGrid>
</Content>
</BasePageComponent>
@code {
private BasePageComponent? _basePage;
private MudDataGrid<CustomerPackagePurchaseRollupModel>? _grid;
private long? _userId;
private int? _purchaseMethod;
private DateTime? _fromDate, _toDate;
private bool _exporting;
private GetCustomerPackagePurchaseRollupFilter BuildFilter()
{
var filter = new GetCustomerPackagePurchaseRollupFilter();
if (_userId is > 0)
filter.UserId = _userId;
if (_purchaseMethod.HasValue)
filter.PurchaseMethod = _purchaseMethod.Value;
if (_fromDate.HasValue)
filter.PurchasedFrom = Timestamp.FromDateTime(DateTime.SpecifyKind(_fromDate.Value.Date, DateTimeKind.Utc));
if (_toDate.HasValue)
filter.PurchasedTo = Timestamp.FromDateTime(DateTime.SpecifyKind(_toDate.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc));
return filter;
}
private async Task<GridData<CustomerPackagePurchaseRollupModel>> LoadData(GridState<CustomerPackagePurchaseRollupModel> state)
{
try
{
var result = await PurchaseClient.GetCustomerPackagePurchaseRollupAsync(
new GetCustomerPackagePurchaseRollupRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = state.Page + 1,
PageSize = state.PageSize
},
Filter = BuildFilter()
});
return new GridData<CustomerPackagePurchaseRollupModel>
{
Items = result?.Models?.ToList() ?? new(),
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
return new GridData<CustomerPackagePurchaseRollupModel>();
}
}
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();
}
private async Task OpenDetails(CustomerPackagePurchaseRollupModel row)
{
var parameters = new DialogParameters
{
{ nameof(CustomerPackagePurchaseDetailsDialog.UserId), row.UserId },
{ nameof(CustomerPackagePurchaseDetailsDialog.UserName), row.UserName }
};
var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true, CloseButton = true };
await DialogService.ShowAsync<CustomerPackagePurchaseDetailsDialog>("جزئیات خرید پکیج", parameters, options);
}
private async Task ExportToExcel()
{
_exporting = true;
try
{
// PageSize=0 → همهٔ ردیف‌های مطابق فیلتر، مستقل از صفحه‌بندی گرید
var result = await PurchaseClient.GetCustomerPackagePurchaseRollupAsync(
new GetCustomerPackagePurchaseRollupRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = 1,
PageSize = 0
},
Filter = BuildFilter()
});
var all = result?.Models?.ToList() ?? new();
if (all.Count == 0)
{
Snackbar.Add("داده‌ای برای خروجی با فیلتر فعلی وجود ندارد.", Severity.Info);
return;
}
var headers = new[]
{
"شناسه کاربر", "نام مشتری", "موبایل",
"اولین پکیج", "مبلغ اولین", "تاریخ اولین",
"آخرین پکیج", "مبلغ آخرین", "تاریخ آخرین", "روش آخرین",
"تعداد خرید", "جمع مبلغ",
"تعداد دایا", "مبلغ دایا",
"تعداد دستی", "مبلغ دستی",
"تعداد درگاه", "مبلغ درگاه"
};
var rows = all.Select(o => new object?[]
{
o.UserId,
o.UserName,
o.UserMobile,
o.FirstPackageName,
o.FirstAmount,
o.FirstPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-",
o.LastPackageName,
o.LastAmount,
o.LastPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-",
ReportLabels.GetPurchaseMethodLabel(o.LastPurchaseMethod),
o.PurchaseCount,
o.TotalAmount,
o.DayaCount,
o.DayaAmount,
o.ManualCount,
o.ManualAmount,
o.GatewayCount,
o.GatewayAmount
});
var filename = $"customer-packages-{DateTime.Now:yyyyMMddHHmmss}.xls";
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
CsvExportHelper.ToExcelXmlBase64("مشتریان پکیج", headers, rows));
Snackbar.Add($"خروجی Excel آماده شد ({all.Count} مشتری).", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
}
finally
{
_exporting = false;
}
}
private static Color GetMethodColor(int method) => method switch
{
1 => Color.Info,
2 => Color.Primary,
3 => Color.Secondary,
_ => Color.Default
};
}
@@ -55,7 +55,7 @@
Margin="Margin.Dense"> Margin="Margin.Dense">
<MudSelectItem T="int?" Value="@(null as int?)">همه وضعیت‌ها</MudSelectItem> <MudSelectItem T="int?" Value="@(null as int?)">همه وضعیت‌ها</MudSelectItem>
<MudSelectItem T="int?" Value="0">در انتظار واریز به کیف پول</MudSelectItem> <MudSelectItem T="int?" Value="0">در انتظار واریز به کیف پول</MudSelectItem>
<MudSelectItem T="int?" Value="1">واریز شده به پاداش تیمی</MudSelectItem> <MudSelectItem T="int?" Value="1">واریز شده به پاداش های دریافتی</MudSelectItem>
<MudSelectItem T="int?" Value="2">درخواست برداشت ثبت شده</MudSelectItem> <MudSelectItem T="int?" Value="2">درخواست برداشت ثبت شده</MudSelectItem>
<MudSelectItem T="int?" Value="3">برداشت انجام شده</MudSelectItem> <MudSelectItem T="int?" Value="3">برداشت انجام شده</MudSelectItem>
<MudSelectItem T="int?" Value="4">خطا در پرداخت بانکی</MudSelectItem> <MudSelectItem T="int?" Value="4">خطا در پرداخت بانکی</MudSelectItem>
@@ -26,6 +26,7 @@
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Processing">در حال آماده‌سازی</MudSelectItem> <MudSelectItem T="OrderStatus" Value="@OrderStatus.Processing">در حال آماده‌سازی</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem> <MudSelectItem T="OrderStatus" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem> <MudSelectItem T="OrderStatus" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.ReadyForOfficePickup">آماده تحویل در دفتر</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem> <MudSelectItem T="OrderStatus" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem> <MudSelectItem T="OrderStatus" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
</MudSelect> </MudSelect>
@@ -55,6 +56,15 @@
</MudItem> </MudItem>
} }
@if (Model.Status == OrderStatus.ReadyForOfficePickup)
{
<MudItem xs="12">
<MudAlert Severity="Severity.Info">
<MudText>سفارش برای تحویل حضوری در دفتر آماده است.</MudText>
</MudAlert>
</MudItem>
}
@if (Model.Status == OrderStatus.Shipped) @if (Model.Status == OrderStatus.Shipped)
{ {
<MudItem xs="12"> <MudItem xs="12">
@@ -126,8 +136,9 @@
OrderStatus.Processing => Color.Primary, OrderStatus.Processing => Color.Primary,
OrderStatus.Shipped => Color.Secondary, OrderStatus.Shipped => Color.Secondary,
OrderStatus.Delivered => Color.Success, OrderStatus.Delivered => Color.Success,
OrderStatus.Cancelled => Color.Error, OrderStatus.ReadyForOfficePickup => Color.Primary,
OrderStatus.Returned => Color.Dark, OrderStatus.Cancelled => Color.Dark,
OrderStatus.Returned => Color.Error,
_ => Color.Default _ => Color.Default
}; };
} }
@@ -141,6 +152,7 @@
OrderStatus.Processing => "در حال آماده‌سازی", OrderStatus.Processing => "در حال آماده‌سازی",
OrderStatus.Shipped => "ارسال شده", OrderStatus.Shipped => "ارسال شده",
OrderStatus.Delivered => "تحویل داده شده", OrderStatus.Delivered => "تحویل داده شده",
OrderStatus.ReadyForOfficePickup => "آماده تحویل در دفتر",
OrderStatus.Cancelled => "لغو شده", OrderStatus.Cancelled => "لغو شده",
OrderStatus.Returned => "مرجوع شده", OrderStatus.Returned => "مرجوع شده",
_ => "نامشخص" _ => "نامشخص"
@@ -1,36 +1,72 @@
@using BackOffice.Services.DiscountOrder @using BackOffice.Services.DiscountOrder
@using BackOffice.Common.BaseComponents
<MudDialog> <MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.ShoppingCart" Class="ml-2" />
جزئیات سفارش #@Order.OrderNumber
</MudText>
</TitleContent>
<DialogContent> <DialogContent>
<MudGrid> <MudStack Spacing="2">
<!-- اطلاعات کاربر --> <MudText Typo="Typo.h6">جزئیات سفارش شماره @(string.IsNullOrWhiteSpace(Order.OrderNumber) ? Order.Id.ToString() : Order.OrderNumber)</MudText>
<MudItem xs="12"> <MudText Typo="Typo.body2">
<MudPaper Class="pa-4" Elevation="1"> کاربر:
<MudText Typo="Typo.h6" GutterBottom="true">اطلاعات سفارش</MudText> @if (!string.IsNullOrWhiteSpace(Order.UserFullName))
<MudGrid> {
<MudItem xs="6"> @Order.UserFullName
<MudText Typo="Typo.body2" Color="Color.Secondary">شماره سفارش:</MudText> }
<MudText Typo="Typo.body1"><strong>@Order.OrderNumber</strong></MudText> else
</MudItem> {
<MudItem xs="6"> <text>#@Order.UserId</text>
<MudText Typo="Typo.body2" Color="Color.Secondary">شناسه کاربر:</MudText> }
<MudText Typo="Typo.body1">@Order.UserId</MudText> @if (!string.IsNullOrWhiteSpace(DisplayMobile))
</MudItem> {
</MudGrid> <text> (@DisplayMobile)</text>
</MudPaper> }
</MudItem> </MudText>
<!-- Timeline وضعیت --> <MudPaper Class="pa-3">
<MudItem xs="12"> <MudStack Spacing="2">
<MudPaper Class="pa-4" Elevation="1"> <MudText Typo="Typo.subtitle2">خلاصه سفارش</MudText>
<MudText Typo="Typo.subtitle2" GutterBottom="true">Timeline وضعیت</MudText> <MudDivider />
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center" Justify="Justify.Center"> <MudText Typo="Typo.body2">مبلغ کل: @Order.TotalPrice.ToString("N0") تومان</MudText>
<MudText Typo="Typo.body2" Color="Color.Success">
از کیف پول اعتباری: @Order.DiscountBalanceUsed.ToString("N0") تومان
</MudText>
<MudText Typo="Typo.body2">
مبلغ پرداختی از درگاه: @Order.GatewayAmount.ToString("N0") تومان
</MudText>
@if (!string.IsNullOrEmpty(Order.TransactionId))
{
<MudText Typo="Typo.body2">شناسه پرداخت: @Order.TransactionId</MudText>
}
@if (Order.Created.HasValue)
{
<MudText Typo="Typo.body2">
تاریخ ثبت: @Order.Created.Value.MiladiToJalaliWithTime()
</MudText>
}
<MudText Typo="Typo.body2">
وضعیت پرداخت:
<MudChip T="string"
Color="@GetPaymentStatusColor(Order.PaymentStatusValue)"
Size="Size.Small"
Icon="@GetPaymentStatusIcon(Order.PaymentStatusValue)">
@GetPaymentStatusText(Order.PaymentStatusValue)
</MudChip>
</MudText>
@if (Order.VatAmount > 0)
{
<MudDivider Class="my-2" />
<MudText Typo="Typo.subtitle2">جزئیات مالیات بر ارزش افزوده</MudText>
<MudText Typo="Typo.body2">
مالیات: @Order.VatAmount.ToString("N0") تومان
</MudText>
<MudText Typo="Typo.body2">
مبلغ نهایی (شامل مالیات): @Order.TotalPrice.ToString("N0") تومان
</MudText>
}
<MudDivider Class="my-2" />
<MudText Typo="Typo.subtitle2">Timeline وضعیت</MudText>
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center">
@foreach (var step in _timelineSteps) @foreach (var step in _timelineSteps)
{ {
<MudStack Spacing="0" AlignItems="AlignItems.Center"> <MudStack Spacing="0" AlignItems="AlignItems.Center">
@@ -41,218 +77,155 @@
</MudStack> </MudStack>
} }
</MudStack> </MudStack>
</MudPaper>
</MudItem>
<!-- وضعیت سفارش --> <MudDivider Class="my-2" />
<MudItem xs="12" sm="6"> <MudText Typo="Typo.body2">
<MudPaper Class="pa-4" Elevation="1"> وضعیت ارسال:
<MudText Typo="Typo.h6" GutterBottom="true">وضعیت سفارش</MudText> <MudSelect T="OrderStatus"
<MudChip T="string" Color="@GetStatusColor(Order.Status)" Size="Size.Large"> Label="وضعیت ارسال"
@GetStatusText(Order.Status) Dense="true"
</MudChip> Variant="Variant.Outlined"
@if (Order.Created.HasValue) @bind-Value="_newStatus">
{ <MudSelectItem T="OrderStatus" Value="@OrderStatus.Pending">در انتظار پرداخت</MudSelectItem>
<MudText Typo="Typo.body2" Class="mt-2"> <MudSelectItem T="OrderStatus" Value="@OrderStatus.Paid">پرداخت شده</MudSelectItem>
تاریخ ثبت: @Order.Created.Value.MiladiToJalaliWithTime() <MudSelectItem T="OrderStatus" Value="@OrderStatus.Processing">در حال آماده‌سازی</MudSelectItem>
</MudText> <MudSelectItem T="OrderStatus" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
} <MudSelectItem T="OrderStatus" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
</MudPaper> <MudSelectItem T="OrderStatus" Value="@OrderStatus.ReadyForOfficePickup">آماده تحویل در دفتر</MudSelectItem>
</MudItem> <MudSelectItem T="OrderStatus" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
<!-- وضعیت پرداخت --> </MudSelect>
<MudItem xs="12" sm="6"> </MudText>
<MudPaper Class="pa-4" Elevation="1"> <MudTextField T="string"
<MudText Typo="Typo.h6" GutterBottom="true">وضعیت پرداخت</MudText> Label="کد رهگیری"
@if (Order.PaymentCompleted) Variant="Variant.Outlined"
{ Dense="true"
<MudChip T="string" Color="Color.Success" Size="Size.Large" Icon="@Icons.Material.Filled.CheckCircle"> @bind-Value="_trackingCode" />
پرداخت شده <MudTextField T="string"
</MudChip> Label="یادداشت ادمین"
@if (!string.IsNullOrEmpty(Order.TransactionId)) Variant="Variant.Outlined"
{ Dense="true"
<MudText Typo="Typo.body2" Class="mt-2"> Lines="3"
کد تراکنش: @Order.TransactionId HelperText="دلیل تغییر وضعیت یا توضیحات اضافی"
</MudText> @bind-Value="_adminNotes" />
}
}
else
{
<MudChip T="string" Color="Color.Warning" Size="Size.Large" Icon="@Icons.Material.Filled.Schedule">
در انتظار پرداخت
</MudChip>
}
</MudPaper>
</MudItem>
<!-- آدرس ارسال -->
@if (Order.Address != null)
{
<MudItem xs="12">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" GutterBottom="true">
<MudIcon Icon="@Icons.Material.Filled.LocationOn" Size="Size.Small" />
آدرس ارسال
</MudText>
<MudText Typo="Typo.body1"><strong>@Order.Address.Title</strong></MudText>
<MudText Typo="Typo.body2">@Order.Address.Address</MudText>
<MudText Typo="Typo.body2">کد پستی: @Order.Address.PostalCode</MudText>
@if (!string.IsNullOrEmpty(Order.Address.Phone))
{
<MudText Typo="Typo.body2">تلفن: @Order.Address.Phone</MudText>
}
</MudPaper>
</MudItem>
}
<!-- آیتم‌های سفارش -->
<MudItem xs="12">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" GutterBottom="true">آیتم‌های سفارش</MudText>
<MudTable Items="@Order.Items" Hover="true" Dense="true" Elevation="0">
<HeaderContent>
<MudTh>محصول</MudTh>
<MudTh>تعداد</MudTh>
<MudTh>قیمت واحد</MudTh>
<MudTh>تخفیف</MudTh>
<MudTh>قیمت نهایی</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
@if (!string.IsNullOrWhiteSpace(context.ThumbnailPath))
{
<MudImage Src="@context.ThumbnailPath" Width="40" Height="40"
ObjectFit="ObjectFit.Cover" Class="rounded" />
}
<MudText>@context.ProductTitle</MudText>
</MudStack>
</MudTd>
<MudTd>@context.Count</MudTd>
<MudTd>@context.UnitPrice.ToString("N0") تومان</MudTd>
<MudTd>
<MudChip T="string" Size="Size.Small" Color="Color.Success">@context.MaxDiscountPercent%</MudChip>
</MudTd>
<MudTd><strong>@context.FinalPrice.ToString("N0") تومان</strong></MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
</MudItem>
<!-- خلاصه مالی -->
<MudItem xs="12">
<MudPaper Class="pa-4" Elevation="2">
<MudText Typo="Typo.h6" GutterBottom="true">خلاصه مالی</MudText>
<MudGrid>
<MudItem xs="6">
<MudText Color="Color.Secondary">مبلغ کل:</MudText>
</MudItem>
<MudItem xs="6" Class="text-left">
<MudText>@Order.TotalPrice.ToString("N0") تومان</MudText>
</MudItem>
<MudItem xs="6">
<MudText Color="Color.Success">از کیف پول اعتباری:</MudText>
</MudItem>
<MudItem xs="6" Class="text-left">
<MudText Color="Color.Success">@Order.DiscountBalanceUsed.ToString("N0") تومان</MudText>
</MudItem>
<MudItem xs="12"><MudDivider /></MudItem>
<MudItem xs="6">
<MudText Typo="Typo.h6">مبلغ پرداختی از درگاه:</MudText>
</MudItem>
<MudItem xs="6" Class="text-left">
<MudText Typo="Typo.h6" Color="Color.Primary">
<strong>@Order.GatewayAmount.ToString("N0") تومان</strong>
</MudText>
</MudItem>
</MudGrid>
</MudPaper>
</MudItem>
<!-- ویرایش وضعیت ارسال -->
<MudItem xs="12">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.subtitle2" GutterBottom="true">مدیریت وضعیت ارسال</MudText>
<MudDivider Class="mb-3" />
<MudGrid>
<MudItem xs="12" sm="6">
<MudSelect T="OrderStatus"
Label="وضعیت ارسال"
Dense="true"
Variant="Variant.Outlined"
@bind-Value="_newStatus">
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Pending">در انتظار پرداخت</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Paid">پرداخت شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Processing">در حال آماده‌سازی</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6">
<MudTextField T="string"
Label="کد رهگیری"
Variant="Variant.Outlined"
Margin="Margin.Dense"
@bind-Value="_trackingCode" />
</MudItem>
<MudItem xs="12">
<MudTextField T="string"
Label="یادداشت ادمین"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Lines="3"
HelperText="دلیل تغییر وضعیت یا توضیحات اضافی"
@bind-Value="_adminNotes" />
</MudItem>
</MudGrid>
@if (_newStatus == OrderStatus.Cancelled || _newStatus == OrderStatus.Returned) @if (_newStatus == OrderStatus.Cancelled || _newStatus == OrderStatus.Returned)
{ {
<MudAlert Severity="Severity.Warning" Class="mt-2"> <MudAlert Severity="Severity.Warning" Dense="true">
توجه: در صورت لغو یا مرجوعی سفارش، موجودی محصولات به انبار بازگردانده می‌شود. توجه: در صورت لغو یا مرجوعی سفارش، موجودی محصولات به انبار بازگردانده می‌شود.
</MudAlert> </MudAlert>
} }
@if (_newStatus == OrderStatus.ReadyForOfficePickup)
{
<MudAlert Severity="Severity.Info" Dense="true">
سفارش برای تحویل حضوری در دفتر آماده است.
</MudAlert>
}
@if (_newStatus == OrderStatus.Shipped) @if (_newStatus == OrderStatus.Shipped)
{ {
<MudAlert Severity="Severity.Info" Class="mt-2"> <MudAlert Severity="Severity.Info" Dense="true">
پس از تغییر وضعیت به "ارسال شده"، اطلاع‌رسانی به کاربر ارسال خواهد شد. پس از تغییر وضعیت به «ارسال شده»، اطلاع‌رسانی به کاربر ارسال خواهد شد.
</MudAlert> </MudAlert>
} }
</MudPaper> </MudStack>
</MudItem> </MudPaper>
<!-- یادداشت‌ها --> <MudPaper Class="pa-3">
@if (!string.IsNullOrEmpty(Order.Notes) || !string.IsNullOrEmpty(Order.AdminNotes)) <MudStack Spacing="1">
<MudText Typo="Typo.subtitle2">آدرس تحویل</MudText>
<MudDivider />
@if (Order.Address != null && !string.IsNullOrWhiteSpace(Order.Address.Address))
{
@if (!string.IsNullOrWhiteSpace(Order.Address.Title))
{
<MudText Typo="Typo.body2"><strong>@Order.Address.Title</strong></MudText>
}
<MudText Typo="Typo.body2">@Order.Address.Address</MudText>
@if (!string.IsNullOrWhiteSpace(Order.Address.PostalCode))
{
<MudText Typo="Typo.body2">کد پستی: @Order.Address.PostalCode</MudText>
}
@if (!string.IsNullOrWhiteSpace(DisplayMobile))
{
<MudText Typo="Typo.body2">تلفن سفارش‌دهنده: @DisplayMobile</MudText>
}
}
else
{
<MudText Typo="Typo.caption">آدرسی ثبت نشده است.</MudText>
}
</MudStack>
</MudPaper>
<MudPaper Class="pa-3">
<MudStack Spacing="1">
<MudText Typo="Typo.subtitle2">اقلام فاکتور</MudText>
<MudDivider />
@if (Order.Items.Count > 0)
{
<MudTable Items="Order.Items" Dense="true">
<HeaderContent>
<MudTh>تصویر</MudTh>
<MudTh>محصول</MudTh>
<MudTh>قیمت واحد</MudTh>
<MudTh>تعداد</MudTh>
<MudTh>تخفیف</MudTh>
<MudTh>قیمت نهایی</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
@if (!string.IsNullOrWhiteSpace(context.ThumbnailPath))
{
<Image Src="@context.ThumbnailPath" Height="50" Width="50" />
}
</MudTd>
<MudTd>@context.ProductTitle</MudTd>
<MudTd>@context.UnitPrice.ToString("N0")</MudTd>
<MudTd>@context.Count</MudTd>
<MudTd>
@if (context.MaxDiscountPercent > 0)
{
<MudChip T="string" Size="Size.Small" Color="Color.Success">@context.MaxDiscountPercent%</MudChip>
}
else if (context.DiscountAmount > 0)
{
@context.DiscountAmount.ToString("N0")
}
else
{
<text>-</text>
}
</MudTd>
<MudTd>@context.FinalPrice.ToString("N0")</MudTd>
</RowTemplate>
</MudTable>
}
else
{
<MudText Typo="Typo.caption">آیتمی برای این سفارش ثبت نشده است.</MudText>
}
</MudStack>
</MudPaper>
@if (!string.IsNullOrEmpty(Order.Notes))
{ {
<MudItem xs="12"> <MudPaper Class="pa-3">
<MudPaper Class="pa-4" Elevation="1"> <MudStack Spacing="1">
@if (!string.IsNullOrEmpty(Order.Notes)) <MudText Typo="Typo.subtitle2">یادداشت کاربر</MudText>
{ <MudDivider />
<MudText Typo="Typo.h6" GutterBottom="true"> <MudText Typo="Typo.body2">@Order.Notes</MudText>
<MudIcon Icon="@Icons.Material.Filled.Comment" Size="Size.Small" /> </MudStack>
یادداشت کاربر </MudPaper>
</MudText>
<MudText Typo="Typo.body1" Class="mb-4">@Order.Notes</MudText>
}
@if (!string.IsNullOrEmpty(Order.AdminNotes))
{
<MudText Typo="Typo.h6" GutterBottom="true">
<MudIcon Icon="@Icons.Material.Filled.Note" Size="Size.Small" />
یادداشت ادمین
</MudText>
<MudText Typo="Typo.body1">@Order.AdminNotes</MudText>
}
</MudPaper>
</MudItem>
} }
</MudGrid> </MudStack>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<MudButton Variant="Variant.Outlined" Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.Print"
OnClick="PrintInvoiceAsync">
چاپ فاکتور
</MudButton>
<MudSpacer />
<MudButton Variant="Variant.Outlined" Color="Color.Default" OnClick="Close"> <MudButton Variant="Variant.Outlined" Color="Color.Default" OnClick="Close">
بستن بستن
</MudButton> </MudButton>
@@ -1,5 +1,7 @@
using BackOffice.Common.Utilities;
using BackOffice.Services.DiscountOrder; using BackOffice.Services.DiscountOrder;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor; using MudBlazor;
namespace BackOffice.Pages.DiscountShop.Components; namespace BackOffice.Pages.DiscountShop.Components;
@@ -8,6 +10,7 @@ public partial class OrderDetailsDialog
{ {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!; [CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!;
[Inject] public IDiscountOrderService OrderService { get; set; } = default!; [Inject] public IDiscountOrderService OrderService { get; set; } = default!;
[Inject] private IJSRuntime JsRuntime { get; set; } = default!;
[Parameter] public DiscountOrderDetailsDto Order { get; set; } = null!; [Parameter] public DiscountOrderDetailsDto Order { get; set; } = null!;
@@ -17,6 +20,9 @@ public partial class OrderDetailsDialog
private bool _isSaving; private bool _isSaving;
private List<TimelineStep> _timelineSteps = new(); private List<TimelineStep> _timelineSteps = new();
private string? DisplayMobile =>
!string.IsNullOrWhiteSpace(Order.UserMobile) ? Order.UserMobile : Order.Address?.Phone;
protected override void OnInitialized() protected override void OnInitialized()
{ {
_newStatus = Order.Status; _newStatus = Order.Status;
@@ -127,34 +133,69 @@ public partial class OrderDetailsDialog
private void Close() => MudDialog.Close(); private void Close() => MudDialog.Close();
private Color GetStatusColor(OrderStatus status) private Color GetPaymentStatusColor(int status) => status switch
{ {
return status switch 0 => Color.Warning,
{ 1 => Color.Success,
OrderStatus.Pending => Color.Warning, 2 => Color.Error,
OrderStatus.Paid => Color.Info, 3 => Color.Info,
OrderStatus.Processing => Color.Primary, _ => Color.Default
OrderStatus.Shipped => Color.Secondary, };
OrderStatus.Delivered => Color.Success,
OrderStatus.Cancelled => Color.Error,
OrderStatus.Returned => Color.Dark,
_ => Color.Default
};
}
private string GetStatusText(OrderStatus status) private string GetPaymentStatusText(int status) => status switch
{ {
return status switch 0 => "در انتظار پرداخت",
1 => "پرداخت شده",
2 => "ناموفق",
3 => "بازپرداخت شده",
_ => "نامشخص"
};
private string GetPaymentStatusIcon(int status) => status switch
{
1 => Icons.Material.Filled.CheckCircle,
2 => Icons.Material.Filled.Cancel,
3 => Icons.Material.Filled.Replay,
_ => Icons.Material.Filled.Schedule
};
private async Task PrintInvoiceAsync()
{
var lines = Order.Items.Select(i => new OrderInvoicePrint.InvoiceLine
{ {
OrderStatus.Pending => "در انتظار پرداخت", Title = i.ProductTitle,
OrderStatus.Paid => "پرداخت شده", Quantity = i.Count,
OrderStatus.Processing => "در حال آماده‌سازی", UnitPrice = i.UnitPrice,
OrderStatus.Shipped => "ارسال شده", Discount = i.DiscountAmount,
OrderStatus.Delivered => "تحویل داده شده", LineTotal = i.FinalPrice > 0 ? i.FinalPrice : i.UnitPrice * i.Count
OrderStatus.Cancelled => "لغو شده", }).ToList();
OrderStatus.Returned => "مرجوع شده",
_ => "نامشخص" var mobile = !string.IsNullOrWhiteSpace(Order.UserMobile)
}; ? Order.UserMobile
: Order.Address?.Phone;
var html = OrderInvoicePrint.BuildHtml(new OrderInvoicePrint.InvoiceModel
{
ShopTitle = "فروشگاه اعتباری — کارا بازار سلامت",
OrderId = string.IsNullOrWhiteSpace(Order.OrderNumber) ? Order.Id.ToString() : Order.OrderNumber,
CustomerName = Order.UserFullName,
CustomerMobile = mobile,
Address = Order.Address?.Address,
PostalCode = Order.Address?.PostalCode,
PaymentStatus = GetPaymentStatusText(Order.PaymentStatusValue),
TransactionId = Order.TransactionId,
TrackingCode = Order.TrackingCode,
Created = Order.Created,
Subtotal = lines.Sum(l => l.UnitPrice * l.Quantity),
DiscountTotal = lines.Sum(l => l.Discount),
CreditUsed = Order.DiscountBalanceUsed,
GatewayAmount = Order.GatewayAmount,
VatAmount = Order.VatAmount,
TotalAmount = Order.TotalPrice,
Lines = lines
});
await JsRuntime.InvokeVoidAsync("jsHtmlToPdf", html, $"فاکتور سفارش {Order.Id}");
} }
private enum StepState private enum StepState
@@ -49,6 +49,8 @@
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem> <MudSelectItem T="OrderStatus?" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem> <MudSelectItem T="OrderStatus?" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem> <MudSelectItem T="OrderStatus?" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.ReadyForOfficePickup">آماده تحویل در دفتر</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
</MudSelect> </MudSelect>
</MudItem> </MudItem>
</Filters> </Filters>
@@ -1,5 +1,6 @@
using System.Text; using System.Text;
using BackOffice.Common.BaseComponents; using BackOffice.Common.BaseComponents;
using BackOffice.Common.Utilities;
using BackOffice.Services.DiscountOrder; using BackOffice.Services.DiscountOrder;
using BackOffice.Pages.DiscountShop.Components; using BackOffice.Pages.DiscountShop.Components;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@@ -74,6 +75,15 @@ public partial class DiscountOrdersMainPage
return; return;
} }
// تکمیل فیلدهای فاکتور از ردیف لیست (جزئیات API نام/موبایل/VAT ندارد)
var listRow = _orders?.FirstOrDefault(o => o.Id == orderId);
if (listRow != null)
{
order.UserFullName = listRow.UserFullName;
order.UserMobile = listRow.UserMobile;
order.VatAmount = listRow.VatAmount;
}
var parameters = new DialogParameters<OrderDetailsDialog> { { x => x.Order, order } }; var parameters = new DialogParameters<OrderDetailsDialog> { { x => x.Order, order } };
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large, FullWidth = true }; var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large, FullWidth = true };
var dialog = await DialogService.ShowAsync<OrderDetailsDialog>("جزئیات سفارش", parameters, options); var dialog = await DialogService.ShowAsync<OrderDetailsDialog>("جزئیات سفارش", parameters, options);
@@ -178,8 +188,9 @@ public partial class DiscountOrdersMainPage
OrderStatus.Processing => Color.Primary, OrderStatus.Processing => Color.Primary,
OrderStatus.Shipped => Color.Secondary, OrderStatus.Shipped => Color.Secondary,
OrderStatus.Delivered => Color.Success, OrderStatus.Delivered => Color.Success,
OrderStatus.Cancelled => Color.Error, OrderStatus.Cancelled => Color.Dark,
OrderStatus.Returned => Color.Dark, OrderStatus.ReadyForOfficePickup => Color.Primary,
OrderStatus.Returned => Color.Error,
_ => Color.Default _ => Color.Default
}; };
} }
@@ -194,6 +205,7 @@ public partial class DiscountOrdersMainPage
OrderStatus.Shipped => "ارسال شده", OrderStatus.Shipped => "ارسال شده",
OrderStatus.Delivered => "تحویل داده شده", OrderStatus.Delivered => "تحویل داده شده",
OrderStatus.Cancelled => "لغو شده", OrderStatus.Cancelled => "لغو شده",
OrderStatus.ReadyForOfficePickup => "آماده تحویل در دفتر",
OrderStatus.Returned => "مرجوع شده", OrderStatus.Returned => "مرجوع شده",
_ => "نامشخص" _ => "نامشخص"
}; };
@@ -233,44 +245,36 @@ public partial class DiscountOrdersMainPage
try try
{ {
var sb = new StringBuilder(); var headers = new[]
sb.AppendLine("شناسه,نام کاربر,موبایل,مبلغ کل,کیف پول اعتباری,پرداخت درگاه,تعداد آیتم,وضعیت پرداخت,وضعیت ارسال,تاریخ پرداخت,تاریخ ثبت,آدرس");
foreach (var o in _orders)
{ {
sb.AppendLine(string.Join(",", "شناسه", "نام مشتری", "موبایل", "مبلغ کل", "کیف پول اعتباری", "پرداخت درگاه",
o.Id, "تعداد آیتم", "وضعیت پرداخت", "وضعیت ارسال", "تاریخ پرداخت", "تاریخ ثبت", "آدرس"
EscapeCsv(o.UserFullName), };
EscapeCsv(o.UserMobile),
o.TotalPrice.ToString("N0"),
o.DiscountBalanceUsed.ToString("N0"),
o.GatewayAmount.ToString("N0"),
o.ItemsCount,
EscapeCsv(GetPaymentStatusText(o.PaymentStatusValue)),
EscapeCsv(GetStatusText(o.Status)),
o.PaymentDate?.MiladiToJalaliWithTime() ?? "-",
o.Created?.MiladiToJalaliWithTime() ?? "-",
EscapeCsv(o.ShippingAddress ?? "-")));
}
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray(); var rows = _orders.Select(o => new object?[]
var base64 = Convert.ToBase64String(bytes); {
var filename = $"discount-orders-{DateTime.Now:yyyyMMddHHmmss}.csv"; o.Id,
CsvExportHelper.CustomerName(o.UserFullName, o.UserMobile, o.UserId),
o.UserMobile ?? "",
o.TotalPrice,
o.DiscountBalanceUsed,
o.GatewayAmount,
o.ItemsCount,
GetPaymentStatusText(o.PaymentStatusValue),
GetStatusText(o.Status),
o.PaymentDate?.MiladiToJalaliWithTime() ?? "-",
o.Created?.MiladiToJalaliWithTime() ?? "-",
o.ShippingAddress ?? "-"
});
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64); var filename = $"discount-orders-{DateTime.Now:yyyyMMddHHmmss}.xls";
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success); await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
CsvExportHelper.ToExcelXmlBase64("سفارش‌های اعتباری", headers, rows));
Snackbar.Add("خروجی Excel آماده شد.", Severity.Success);
} }
catch (Exception ex) catch (Exception ex)
{ {
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error); Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
} }
} }
private static string EscapeCsv(string value)
{
if (string.IsNullOrEmpty(value)) return "";
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
return $"\"{value.Replace("\"", "\"\"")}\"";
return value;
}
} }
@@ -48,6 +48,7 @@
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Processing">در حال پردازش</MudSelectItem> <MudSelectItem T="OrderStatus?" Value="OrderStatus.Processing">در حال پردازش</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Shipped">ارسال شده</MudSelectItem> <MudSelectItem T="OrderStatus?" Value="OrderStatus.Shipped">ارسال شده</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Delivered">تحویل شده</MudSelectItem> <MudSelectItem T="OrderStatus?" Value="OrderStatus.Delivered">تحویل شده</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="OrderStatus.ReadyForOfficePickup">آماده تحویل در دفتر</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Cancelled">لغو شده</MudSelectItem> <MudSelectItem T="OrderStatus?" Value="OrderStatus.Cancelled">لغو شده</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Returned">مرجوع شده</MudSelectItem> <MudSelectItem T="OrderStatus?" Value="OrderStatus.Returned">مرجوع شده</MudSelectItem>
</MudSelect> </MudSelect>
@@ -498,7 +499,8 @@
OrderStatus.Processing => Color.Info, OrderStatus.Processing => Color.Info,
OrderStatus.Shipped => Color.Info, OrderStatus.Shipped => Color.Info,
OrderStatus.Delivered => Color.Success, OrderStatus.Delivered => Color.Success,
OrderStatus.Cancelled => Color.Error, OrderStatus.ReadyForOfficePickup => Color.Primary,
OrderStatus.Cancelled => Color.Dark,
OrderStatus.Returned => Color.Error, OrderStatus.Returned => Color.Error,
_ => Color.Default _ => Color.Default
}; };
@@ -513,6 +515,7 @@
OrderStatus.Processing => "در حال پردازش", OrderStatus.Processing => "در حال پردازش",
OrderStatus.Shipped => "ارسال شده", OrderStatus.Shipped => "ارسال شده",
OrderStatus.Delivered => "تحویل شده", OrderStatus.Delivered => "تحویل شده",
OrderStatus.ReadyForOfficePickup => "آماده تحویل در دفتر",
OrderStatus.Cancelled => "لغو شده", OrderStatus.Cancelled => "لغو شده",
OrderStatus.Returned => "مرجوع شده", OrderStatus.Returned => "مرجوع شده",
_ => "نامشخص" _ => "نامشخص"
@@ -527,28 +530,31 @@
return; return;
} }
var sb = new StringBuilder(); var headers = new[]
sb.AppendLine("شناسه,نام کاربر,موبایل,تاریخ ثبت,تعداد آیتم,مبلغ نهایی,تخفیف,وضعیت");
foreach (var o in _orders.Where(o => o.Created.HasValue).OrderBy(o => o.Created))
{ {
sb.AppendLine(string.Join(",", "شناسه", "نام مشتری", "موبایل", "تاریخ ثبت", "تعداد آیتم", "مبلغ نهایی", "تخفیف", "وضعیت"
};
var rows = _orders
.OrderBy(o => o.Created ?? o.PaymentDate ?? DateTime.MaxValue)
.Select(o => new object?[]
{
o.Id, o.Id,
EscapeCsv(o.UserFullName), CsvExportHelper.CustomerName(o.UserFullName, o.UserMobile, o.UserId),
EscapeCsv(o.UserMobile), o.UserMobile ?? "",
o.Created?.MiladiToJalaliWithTime() ?? "-", o.Created?.MiladiToJalaliWithTime()
?? o.PaymentDate?.MiladiToJalaliWithTime()
?? "-",
o.ItemsCount, o.ItemsCount,
o.GatewayAmount, o.GatewayAmount,
o.DiscountBalanceUsed, o.DiscountBalanceUsed,
GetStatusText(o.Status))); GetStatusText(o.Status)
} });
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray(); var filename = $"discount-sales-{DateTime.Now:yyyyMMddHHmmss}.xls";
var base64 = Convert.ToBase64String(bytes); await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
var filename = $"discount-sales-{DateTime.Now:yyyyMMddHHmmss}.csv"; CsvExportHelper.ToExcelXmlBase64("گزارش فروش اعتباری", headers, rows));
Snackbar.Add("خروجی Excel آماده شد.", Severity.Success);
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
} }
private async Task ExportToPdf() private async Task ExportToPdf()
@@ -572,12 +578,12 @@
sb.AppendLine(); sb.AppendLine();
sb.AppendLine("جزئیات سفارش‌ها:"); sb.AppendLine("جزئیات سفارش‌ها:");
sb.AppendLine("شناسه | نام کاربر | تاریخ | تعداد آیتم | مبلغ نهایی | تخفیف | وضعیت"); sb.AppendLine("شناسه | نام مشتری | تاریخ | تعداد آیتم | مبلغ نهایی | تخفیف | وضعیت");
foreach (var o in _orders.Where(o => o.Created.HasValue).OrderBy(o => o.Created)) foreach (var o in _orders.Where(o => o.Created.HasValue).OrderBy(o => o.Created))
{ {
sb.AppendLine( sb.AppendLine(
$"{o.Id} | {o.UserFullName} | {o.Created?.MiladiToJalaliWithTime() ?? "-"} | {o.ItemsCount} | {o.GatewayAmount:N0} | {o.DiscountBalanceUsed:N0} | {GetStatusText(o.Status)}"); $"{o.Id} | {CsvExportHelper.CustomerName(o.UserFullName, o.UserMobile, o.UserId)} | {o.Created?.MiladiToJalaliWithTime() ?? "-"} | {o.ItemsCount} | {o.GatewayAmount:N0} | {o.DiscountBalanceUsed:N0} | {GetStatusText(o.Status)}");
} }
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray(); var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
@@ -587,14 +593,4 @@
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64); await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
Snackbar.Add("خروجی PDF (نسخه متنی) آماده شد.", Severity.Success); Snackbar.Add("خروجی PDF (نسخه متنی) آماده شد.", Severity.Success);
} }
private static string EscapeCsv(string value)
{
if (string.IsNullOrEmpty(value))
return "";
var needsQuotes = value.Contains(',') || value.Contains('"') || value.Contains('\n');
var escaped = value.Replace("\"", "\"\"");
return needsQuotes ? $"\"{escaped}\"" : escaped;
}
} }
@@ -0,0 +1,106 @@
@using BackOffice.Pages.AutoComplete
@using CMSMicroservice.Protobuf.Protos.UserWallet
@inject UserWalletContract.UserWalletContractClient WalletClient
<MudDialog>
<DialogContent>
<MudStack Spacing="3">
<MudAlert Severity="Severity.Warning" Dense="true" Variant="Variant.Outlined">
مبلغ مستقیماً به کیف پول اصلی (Balance) کاربر اضافه می‌شود و در دفتر مالی با نوع «شارژ دستی کیف اصلی» ثبت می‌گردد.
</MudAlert>
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_userId" />
<MudNumericField T="long" @bind-Value="_amount"
Label="مبلغ (تومان)"
Variant="Variant.Outlined"
Min="10000"
HelperText="حداقل ۱۰,۰۰۰ تومان" />
<MudTextField T="string" @bind-Value="_note"
Label="توضیحات ادمین (اختیاری)"
Variant="Variant.Outlined"
Lines="2" />
</MudStack>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel" Disabled="_isSubmitting">انصراف</MudButton>
<MudButton Color="Color.Primary" Variant="Variant.Filled"
OnClick="Submit" Disabled="_isSubmitting || !_userId.HasValue || _amount < 10000">
@if (_isSubmitting)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
}
ثبت شارژ
</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!;
private long? _userId;
private long _amount = 10000;
private string? _note;
private bool _isSubmitting;
private void Cancel() => MudDialog.Cancel();
private async Task Submit()
{
if (!_userId.HasValue || _userId.Value <= 0)
{
Snackbar.Add("لطفاً کاربر را انتخاب کنید", Severity.Warning);
return;
}
if (_amount < 10000)
{
Snackbar.Add("حداقل مبلغ شارژ ۱۰,۰۰۰ تومان است", Severity.Warning);
return;
}
bool? confirm = await DialogService.ShowMessageBox(
"تأیید شارژ دستی",
$"آیا از شارژ {_amount:N0} تومان به کیف پول اصلی کاربر #{_userId.Value} مطمئن هستید؟",
yesText: "بله، شارژ شود", cancelText: "لغو");
if (confirm != true)
return;
_isSubmitting = true;
try
{
var request = new AdminManualCreditChargeRequest
{
UserId = _userId.Value,
Amount = _amount
};
if (!string.IsNullOrWhiteSpace(_note))
request.Note = _note.Trim();
var response = await WalletClient.AdminManualCreditChargeAsync(request);
if (response.Success)
{
Snackbar.Add(
$"شارژ موفق — تراکنش #{response.TransactionId}، موجودی جدید: {response.NewBalance:N0}",
Severity.Success);
MudDialog.Close(DialogResult.Ok(true));
}
else
{
Snackbar.Add(response.Message ?? "شارژ ناموفق بود", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_isSubmitting = false;
}
}
}
@@ -43,7 +43,7 @@
@bind-Value="_selectedType"> @bind-Value="_selectedType">
<MudSelectItem T="int" Value="1">واریز نقدی</MudSelectItem> <MudSelectItem T="int" Value="1">واریز نقدی</MudSelectItem>
<MudSelectItem T="int" Value="2">شارژ کیف پول اعتباری</MudSelectItem> <MudSelectItem T="int" Value="2">شارژ کیف پول اعتباری</MudSelectItem>
<MudSelectItem T="int" Value="3">شارژ پاداش تیمی</MudSelectItem> <MudSelectItem T="int" Value="3">شارژ پاداش های دریافتی</MudSelectItem>
<MudSelectItem T="int" Value="4">تسویه حساب</MudSelectItem> <MudSelectItem T="int" Value="4">تسویه حساب</MudSelectItem>
<MudSelectItem T="int" Value="5">اصلاح خطا</MudSelectItem> <MudSelectItem T="int" Value="5">اصلاح خطا</MudSelectItem>
<MudSelectItem T="int" Value="6">بازپرداخت</MudSelectItem> <MudSelectItem T="int" Value="6">بازپرداخت</MudSelectItem>
@@ -0,0 +1,247 @@
@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
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
<Filters>
<MudItem xs="12">
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true">
گزارش read-only دفتر تراکنش‌های مالی (جدول Transactions). برای جزئیات درگاه پرداخت به «گزارش تراکنش درگاه» مراجعه کنید.
</MudAlert>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudTextField T="long?" Label="شناسه" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_id" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudTextField T="string" Label="RefId" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_refId" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudTextField T="string" Label="توضیحات" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_description" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="PaymentStatus?" Clearable="true" Label="وضعیت پرداخت" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_paymentStatus">
<MudSelectItem T="PaymentStatus?" Value="PaymentStatus.Success">موفق</MudSelectItem>
<MudSelectItem T="PaymentStatus?" Value="PaymentStatus.Pending">در انتظار</MudSelectItem>
<MudSelectItem T="PaymentStatus?" Value="PaymentStatus.Reject">رد شده</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="TransactionType?" Clearable="true" Label="نوع تراکنش" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_type">
<MudSelectItem T="TransactionType?" Value="TransactionType.Buy">خرید</MudSelectItem>
<MudSelectItem T="TransactionType?" Value="TransactionType.DepositIpg">واریز درگاه</MudSelectItem>
<MudSelectItem T="TransactionType?" Value="TransactionType.DepositExternal1">واریز خارجی</MudSelectItem>
<MudSelectItem T="TransactionType?" Value="TransactionType.Withdraw">برداشت</MudSelectItem>
<MudSelectItem T="TransactionType?" Value="TransactionType.NetworkCommission">کمیسیون شبکه</MudSelectItem>
<MudSelectItem T="TransactionType?" Value="TransactionType.ClubActivation">فعال‌سازی باشگاه</MudSelectItem>
<MudSelectItem T="TransactionType?" Value="TransactionType.DiscountWalletCharge">شارژ کیف تخفیف</MudSelectItem>
<MudSelectItem T="TransactionType?" Value="TransactionType.DiscountShopPurchase">خرید فروشگاه تخفیف</MudSelectItem>
<MudSelectItem T="TransactionType?" Value="TransactionType.MagicWalletDeposit">واریز کیف جادویی</MudSelectItem>
<MudSelectItem T="TransactionType?" Value="TransactionType.MagicWalletBonus">بونوس کیف جادویی</MudSelectItem>
<MudSelectItem T="TransactionType?" Value="TransactionType.CreditWalletCharge">شارژ کیف اصلی</MudSelectItem>
<MudSelectItem T="TransactionType?" Value="TransactionType.ManualCreditWalletCharge">شارژ دستی کیف اصلی</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<DateRangePicker @bind-From="_createdFrom" @bind-To="_createdTo" Label="بازه ثبت" />
</MudItem>
</Filters>
<Content>
<MudGrid Class="mb-3" Spacing="2">
<MudItem xs="6" sm="3">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">تعداد</MudText>
<MudText Typo="Typo.h6">@_summaryTotalCount.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6" sm="3">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">جمع مبلغ (تومان)</MudText>
<MudText Typo="Typo.h6">@_summaryTotalAmount.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6" sm="2">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">موفق</MudText>
<MudText Typo="Typo.h6" Color="Color.Success">@_summarySuccess.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6" sm="2">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">در انتظار</MudText>
<MudText Typo="Typo.h6" Color="Color.Warning">@_summaryPending.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6" sm="2">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">رد شده</MudText>
<MudText Typo="Typo.h6" Color="Color.Error">@_summaryReject.ToString("N0")</MudText>
</MudPaper>
</MudItem>
</MudGrid>
<MudDataGrid @ref="_dataGrid" T="GetAllTransactionsByFilterResponseModel"
ServerData="LoadData" ReadOnly="true"
Hover="true" Dense="true" Height="calc(100vh - 400px)">
<ToolBarContent>
<MudText Typo="Typo.subtitle1">دفتر تراکنش‌های مالی</MudText>
<MudSpacer />
<MudButton Variant="Variant.Outlined" Color="Color.Info" Size="Size.Small"
StartIcon="@Icons.Material.Filled.FileDownload" OnClick="ExportCsv">CSV</MudButton>
</ToolBarContent>
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<TemplateColumn Title="نوع">
<CellTemplate>
<MudChip T="string" Size="Size.Small" Variant="Variant.Text">
@ReportLabels.GetTransactionTypeLabel(context.Item.HasType ? context.Item.Type : null)
</MudChip>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="مبلغ (تومان)">
<CellTemplate>@context.Item.Amount.ToString("N0")</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.Description" Title="توضیحات" />
<TemplateColumn Title="وضعیت">
<CellTemplate>
<MudChip T="string" Size="Size.Small" Color="@ReportLabels.GetTransactionPaymentStatusColor(context.Item.HasPaymentStatus ? context.Item.PaymentStatus : null)">
@ReportLabels.GetTransactionPaymentStatusLabel(context.Item.HasPaymentStatus ? context.Item.PaymentStatus : null)
</MudChip>
</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.RefId" Title="RefId" />
<TemplateColumn Title="تاریخ پرداخت">
<CellTemplate>
@(context.Item.PaymentDate != null
? context.Item.PaymentDate.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()
: "—")
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="تاریخ ثبت">
<CellTemplate>@context.Item.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
</TemplateColumn>
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">
با فیلتر فعلی موردی یافت نشد — بازه تاریخ یا فیلترها را تغییر دهید.
</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="GetAllTransactionsByFilterResponseModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد در صفحه" />
</PagerContent>
</MudDataGrid>
</Content>
</BasePageComponent>
@code {
private BasePageComponent? _basePage;
private MudDataGrid<GetAllTransactionsByFilterResponseModel>? _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<GridData<GetAllTransactionsByFilterResponseModel>> LoadData(GridState<GetAllTransactionsByFilterResponseModel> 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<GetAllTransactionsByFilterResponseModel>
{
Items = result?.Models?.ToList() ?? new(),
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
return new GridData<GetAllTransactionsByFilterResponseModel>();
}
}
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); }
}
}
@@ -0,0 +1,149 @@
@page "/payment/manual-credit-charges"
@attribute [Authorize(Roles = "Administrator")]
@using BackOffice.Common.BaseComponents
@using BackOffice.Pages.AutoComplete
@using BackOffice.Pages.Payment.Components
@using CMSMicroservice.Protobuf.Protos
@using CMSMicroservice.Protobuf.Protos.UserWallet
@using DateTimeConverterCL
@using Google.Protobuf.WellKnownTypes
@inject UserWalletContract.UserWalletContractClient WalletClient
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
<Filters>
<MudItem xs="12">
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true">
شارژ دستی کیف پول اصلی (Balance) — بدون درگاه. جدا از «پرداخت‌های دستی» عضویت باشگاه.
</MudAlert>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<UserAutoComplete Label="جستجوی کاربر" @bind-SelectedUserId="_userIdFilter" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudTextField T="long?" Label="شناسه تراکنش" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_transactionIdFilter" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudTextField T="string" Label="RefId" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_refIdFilter" />
</MudItem>
<MudItem xs="12" sm="6" md="4">
<DateRangePicker @bind-From="_createdFrom" @bind-To="_createdTo" Label="بازه ثبت" />
</MudItem>
</Filters>
<Content>
<MudDataGrid @ref="_dataGrid" T="ManualCreditChargeModel"
ServerData="LoadData"
Hover="true" Dense="true"
Height="calc(100vh - 240px)">
<ToolBarContent>
<MudText Typo="Typo.subtitle1">شارژ دستی کیف پول اصلی</MudText>
<MudSpacer />
<MudButton Variant="Variant.Filled" Color="Color.Primary" Size="Size.Small"
StartIcon="@Icons.Material.Filled.Add" OnClick="OpenCreateDialog">
شارژ دستی جدید
</MudButton>
</ToolBarContent>
<Columns>
<PropertyColumn Property="x => x.TransactionId" Title="شناسه تراکنش" />
<PropertyColumn Property="x => x.UserId" Title="شناسه کاربر" />
<PropertyColumn Property="x => x.UserName" Title="نام کاربر" />
<PropertyColumn Property="x => x.Amount" Title="مبلغ">
<CellTemplate>
@context.Item.Amount.ToString("N0")
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.NewBalance" Title="موجودی بعد از شارژ">
<CellTemplate>
@context.Item.NewBalance.ToString("N0")
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.RefId" Title="RefId" />
<PropertyColumn Property="x => x.Description" Title="توضیحات" />
<TemplateColumn Title="تاریخ">
<CellTemplate>
@(context.Item.Created?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-")
</CellTemplate>
</TemplateColumn>
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="ManualCreditChargeModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد در صفحه" />
</PagerContent>
</MudDataGrid>
</Content>
</BasePageComponent>
@code {
private BasePageComponent? _basePage;
private MudDataGrid<ManualCreditChargeModel>? _dataGrid;
private long? _userIdFilter;
private long? _transactionIdFilter;
private string? _refIdFilter;
private DateTime? _createdFrom;
private DateTime? _createdTo;
private async Task<GridData<ManualCreditChargeModel>> LoadData(GridState<ManualCreditChargeModel> state)
{
var request = new GetManualCreditChargesRequest
{
PaginationState = new PaginationState
{
PageNumber = state.Page + 1,
PageSize = state.PageSize
},
Filter = new GetManualCreditChargesFilter()
};
if (_userIdFilter.HasValue)
request.Filter.UserId = _userIdFilter.Value;
if (_transactionIdFilter.HasValue)
request.Filter.TransactionId = _transactionIdFilter.Value;
if (!string.IsNullOrWhiteSpace(_refIdFilter))
request.Filter.RefId = _refIdFilter.Trim();
if (_createdFrom.HasValue)
request.Filter.CreatedFrom = Timestamp.FromDateTime(DateTime.SpecifyKind(_createdFrom.Value, DateTimeKind.Utc));
if (_createdTo.HasValue)
request.Filter.CreatedTo = Timestamp.FromDateTime(DateTime.SpecifyKind(_createdTo.Value, DateTimeKind.Utc));
var response = await WalletClient.GetManualCreditChargesAsync(request);
return new GridData<ManualCreditChargeModel>
{
Items = response.Models.ToList(),
TotalItems = (int)(response.MetaData?.TotalCount ?? 0)
};
}
private async Task OnFilterSubmit()
{
if (_dataGrid != null)
await _dataGrid.ReloadServerData();
}
private async Task OnFilterCleared()
{
_userIdFilter = null;
_transactionIdFilter = null;
_refIdFilter = null;
_createdFrom = null;
_createdTo = null;
if (_dataGrid != null)
await _dataGrid.ReloadServerData();
}
private async Task OpenCreateDialog()
{
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true };
var dialog = DialogService.Show<ManualCreditChargeDialog>("شارژ دستی کیف پول اصلی", options);
var result = await dialog.Result;
if (result is { Canceled: false } && _dataGrid != null)
await _dataGrid.ReloadServerData();
}
}
@@ -33,7 +33,7 @@
Margin="Margin.Dense" @bind-Value="_typeFilter"> Margin="Margin.Dense" @bind-Value="_typeFilter">
<MudSelectItem T="int?" Value="@(1)">واریز نقدی</MudSelectItem> <MudSelectItem T="int?" Value="@(1)">واریز نقدی</MudSelectItem>
<MudSelectItem T="int?" Value="@(2)">شارژ کیف پول اعتباری</MudSelectItem> <MudSelectItem T="int?" Value="@(2)">شارژ کیف پول اعتباری</MudSelectItem>
<MudSelectItem T="int?" Value="@(3)">شارژ پاداش تیمی</MudSelectItem> <MudSelectItem T="int?" Value="@(3)">شارژ پاداش های دریافتی</MudSelectItem>
<MudSelectItem T="int?" Value="@(4)">تسویه حساب</MudSelectItem> <MudSelectItem T="int?" Value="@(4)">تسویه حساب</MudSelectItem>
<MudSelectItem T="int?" Value="@(5)">اصلاح خطا</MudSelectItem> <MudSelectItem T="int?" Value="@(5)">اصلاح خطا</MudSelectItem>
<MudSelectItem T="int?" Value="@(6)">بازگشت وجه</MudSelectItem> <MudSelectItem T="int?" Value="@(6)">بازگشت وجه</MudSelectItem>
@@ -226,7 +226,7 @@
{ {
1 => "واریز نقدی", 1 => "واریز نقدی",
2 => "شارژ کیف پول اعتباری", 2 => "شارژ کیف پول اعتباری",
3 => "شارژ پاداش تیمی", 3 => "شارژ پاداش های دریافتی",
4 => "تسویه حساب", 4 => "تسویه حساب",
5 => "اصلاح خطا", 5 => "اصلاح خطا",
6 => "بازگشت وجه", 6 => "بازگشت وجه",
+172 -51
View File
@@ -1,78 +1,115 @@
@page "/payment/transactions" @page "/payment/transactions"
@using BackOffice.Common.BaseComponents @using BackOffice.Common.BaseComponents
@using BackOffice.Pages.AutoComplete
@using CMSMicroservice.Protobuf.Protos.PaymentTransaction
@using Google.Protobuf.WellKnownTypes
@inject PaymentTransactionContract.PaymentTransactionContractClient PaymentTxClient
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared"> <BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
<Filters> <Filters>
<MudItem xs="12" sm="6" md="4"> <MudItem xs="12">
<BackOffice.Common.BaseComponents.DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه تاریخی" /> <MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true">
گزارش read-only تراکنش‌های درگاه پرداخت. برای ثبت پرداخت دستی به «پرداخت‌های دستی» مراجعه کنید.
</MudAlert>
</MudItem> </MudItem>
<MudItem xs="12" sm="6" md="4"> <MudItem xs="12" sm="6" md="3">
<MudSelect T="int?" Clearable="true" Label="وضعیت پرداخت" Variant="Variant.Outlined" <UserAutoComplete Label="کاربر" @bind-SelectedUserId="_userId" />
Margin="Margin.Dense" @bind-Value="_statusFilter"> </MudItem>
<MudSelectItem T="int?" Value="@(0)">در انتظار</MudSelectItem> <MudItem xs="12" sm="6" md="3">
<MudSelectItem T="int?" Value="@(1)">موفق</MudSelectItem> <MudTextField T="string" Label="موبایل" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_mobile" />
<MudSelectItem T="int?" Value="@(2)">ناموفق</MudSelectItem> </MudItem>
<MudSelectItem T="int?" Value="@(3)">بازگشت وجه</MudSelectItem> <MudItem xs="12" sm="6" md="3">
<MudTextField T="string" Label="Authority" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_authority" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudTextField T="string" Label="RefId" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_refId" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudTextField T="string" Label="OrderId" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_orderId" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudTextField T="string" Label="درگاه" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_gateway" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="bool?" Clearable="true" Label="وضعیت پرداخت" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_paymentStatus">
<MudSelectItem T="bool?" Value="@((bool?)true)">موفق</MudSelectItem>
<MudSelectItem T="bool?" Value="@((bool?)false)">ناموفق / در انتظار</MudSelectItem>
</MudSelect> </MudSelect>
</MudItem> </MudItem>
<MudItem xs="12" sm="6" md="4"> <MudItem xs="12" sm="6" md="4">
<MudSelect T="int?" Clearable="true" Label="نوع تراکنش" Variant="Variant.Outlined" <DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه تاریخ" />
Margin="Margin.Dense" @bind-Value="_typeFilter">
<MudSelectItem T="int?" Value="@(0)">نامشخص</MudSelectItem>
<MudSelectItem T="int?" Value="@(1)">خرید پکیج</MudSelectItem>
<MudSelectItem T="int?" Value="@(2)">پرداخت دستی</MudSelectItem>
<MudSelectItem T="int?" Value="@(3)">شارژ کیف‌پول</MudSelectItem>
<MudSelectItem T="int?" Value="@(4)">بازگشت وجه</MudSelectItem>
</MudSelect>
</MudItem> </MudItem>
</Filters> </Filters>
<Content> <Content>
<MudDataGrid @ref="_dataGrid" T="TransactionModel" <MudGrid Class="mb-3" Spacing="2">
ServerData="LoadData" <MudItem xs="6" sm="3">
Hover="true" Dense="true" <MudPaper Class="pa-3" Outlined="true">
Height="calc(100vh - 240px)"> <MudText Typo="Typo.caption" Color="Color.Secondary">تعداد</MudText>
<MudText Typo="Typo.h6">@_summaryTotalCount.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6" sm="3">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">جمع مبلغ (تومان)</MudText>
<MudText Typo="Typo.h6">@_summaryTotalAmount.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6" sm="3">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">موفق</MudText>
<MudText Typo="Typo.h6" Color="Color.Success">@_summarySuccessful.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6" sm="3">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">در انتظار</MudText>
<MudText Typo="Typo.h6" Color="Color.Warning">@_summaryPending.ToString("N0")</MudText>
</MudPaper>
</MudItem>
</MudGrid>
<MudDataGrid @ref="_dataGrid" T="PaymentTransactionModel"
ServerData="LoadData" ReadOnly="true"
Hover="true" Dense="true" Height="calc(100vh - 380px)">
<ToolBarContent>
<MudText Typo="Typo.subtitle1">گزارش تراکنش درگاه</MudText>
<MudSpacer />
<MudButton Variant="Variant.Outlined" Color="Color.Info" Size="Size.Small"
StartIcon="@Icons.Material.Filled.FileDownload" OnClick="ExportCsv">CSV</MudButton>
</ToolBarContent>
<Columns> <Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" /> <PropertyColumn Property="x => x.Id" Title="شناسه" />
<PropertyColumn Property="x => x.RefId" Title="شماره مرجع" /> <PropertyColumn Property="x => x.UserId" Title="کاربر" />
<PropertyColumn Property="x => x.Amount" Title="مبلغ"> <PropertyColumn Property="x => x.Mobile" Title="موبایل" />
<CellTemplate> <PropertyColumn Property="x => x.GatewayProvider" Title="درگاه" />
@context.Item.Amount.ToString("N0") <TemplateColumn Title="مبلغ (تومان)">
</CellTemplate> <CellTemplate>@context.Item.Amount.ToString("N0")</CellTemplate>
</PropertyColumn> </TemplateColumn>
<PropertyColumn Property="x => x.Authority" Title="Authority" />
<PropertyColumn Property="x => x.RefId" Title="RefId" />
<PropertyColumn Property="x => x.CardPan" Title="کارت" />
<TemplateColumn Title="وضعیت"> <TemplateColumn Title="وضعیت">
<CellTemplate> <CellTemplate>
<MudChip T="string" <MudChip T="string" Size="Size.Small" Color="@(context.Item.PaymentStatus ? Color.Success : Color.Warning)">
Size="Size.Small" @ReportLabels.GetPaymentStatusLabel(context.Item.PaymentStatus)
Color="@GetStatusColor(context.Item.PaymentStatus)">
@GetStatusText(context.Item.PaymentStatus)
</MudChip> </MudChip>
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
<TemplateColumn Title="تاریخ پرداخت"> <PropertyColumn Property="x => x.VerificationStatusCode" Title="کد Verify" />
<CellTemplate> <PropertyColumn Property="x => x.OrderId" Title="OrderId" />
@context.Item.PaymentDate.ToLocalTime().MiladiToJalaliWithTime() <TemplateColumn Title="تاریخ">
</CellTemplate> <CellTemplate>@context.Item.CreatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="نوع">
<CellTemplate>
@GetTypeText(context.Item.Type)
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="عملیات">
<CellTemplate>
<MudTooltip Text="جزئیات">
<MudIconButton Icon="@Icons.Material.Filled.Info"
Size="Size.Small"
OnClick="@(() => OpenDetails(context.Item))" />
</MudTooltip>
</CellTemplate>
</TemplateColumn> </TemplateColumn>
</Columns> </Columns>
<NoRecordsContent> <NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert> <MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">
با فیلتر فعلی موردی یافت نشد — بازه تاریخ یا کاربر را تغییر دهید.
</MudAlert>
</NoRecordsContent> </NoRecordsContent>
<PagerContent> <PagerContent>
<MudDataGridPager T="TransactionModel" PageSizeOptions="@(new int[] { 20, 50, 100 })" <MudDataGridPager T="PaymentTransactionModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}" InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد در صفحه" /> RowsPerPageString="تعداد در صفحه" />
</PagerContent> </PagerContent>
@@ -80,3 +117,87 @@
</Content> </Content>
</BasePageComponent> </BasePageComponent>
@code {
private BasePageComponent? _basePage;
private MudDataGrid<PaymentTransactionModel>? _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<GridData<PaymentTransactionModel>> LoadData(GridState<PaymentTransactionModel> 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<PaymentTransactionModel>
{
Items = result?.Models?.ToList() ?? new(),
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
return new GridData<PaymentTransactionModel>();
}
}
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); }
}
}
@@ -1,95 +0,0 @@
using BackOffice.Common.BaseComponents;
using MudBlazor;
namespace BackOffice.Pages.Payment;
public partial class Transactions
{
private BasePageComponent? _basePage;
private MudDataGrid<TransactionModel>? _dataGrid;
private DateTime? _fromDate;
private DateTime? _toDate;
private int? _statusFilter;
private int? _typeFilter;
private async Task<GridData<TransactionModel>> LoadData(GridState<TransactionModel> state)
{
// TODO: Connect to BackOffice.BFF Transactions when API is ready
await Task.CompletedTask;
return new GridData<TransactionModel>
{
Items = Array.Empty<TransactionModel>(),
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; }
}
}
@@ -27,6 +27,8 @@
<MudSelectItem T="int" Value="2">تحویل پست</MudSelectItem> <MudSelectItem T="int" Value="2">تحویل پست</MudSelectItem>
<MudSelectItem T="int" Value="3">تحویل به مشتری</MudSelectItem> <MudSelectItem T="int" Value="3">تحویل به مشتری</MudSelectItem>
<MudSelectItem T="int" Value="4">مرجوع شده</MudSelectItem> <MudSelectItem T="int" Value="4">مرجوع شده</MudSelectItem>
<MudSelectItem T="int" Value="5">لغو شده</MudSelectItem>
<MudSelectItem T="int" Value="6">آماده تحویل در دفتر</MudSelectItem>
</MudSelect> </MudSelect>
</MudItem> </MudItem>
@@ -56,6 +58,24 @@
</MudItem> </MudItem>
} }
@if (_newStatus == 5)
{
<MudItem xs="12">
<MudAlert Severity="Severity.Warning">
توجه: وضعیت «لغو شده» یعنی ارسال این سفارش متوقف شده است.
</MudAlert>
</MudItem>
}
@if (_newStatus == 6)
{
<MudItem xs="12">
<MudAlert Severity="Severity.Info">
سفارش برای تحویل حضوری در دفتر آماده است.
</MudAlert>
</MudItem>
}
@if (_newStatus == 2) @if (_newStatus == 2)
{ {
<MudItem xs="12"> <MudItem xs="12">
@@ -37,6 +37,8 @@ public partial class ChangeOrderStatusDialog
2 => "تحویل پست", 2 => "تحویل پست",
3 => "تحویل به مشتری", 3 => "تحویل به مشتری",
4 => "مرجوع شده", 4 => "مرجوع شده",
5 => "لغو شده",
6 => "آماده تحویل در دفتر",
_ => "بدون ارسال / نامشخص" _ => "بدون ارسال / نامشخص"
}; };
} }
@@ -49,6 +51,8 @@ public partial class ChangeOrderStatusDialog
2 => Color.Info, 2 => Color.Info,
3 => Color.Success, 3 => Color.Success,
4 => Color.Error, 4 => Color.Error,
5 => Color.Dark,
6 => Color.Primary,
_ => Color.Default _ => Color.Default
}; };
} }
@@ -95,6 +95,8 @@
<MudSelectItem T="int" Value="2">تحویل پست</MudSelectItem> <MudSelectItem T="int" Value="2">تحویل پست</MudSelectItem>
<MudSelectItem T="int" Value="3">تحویل به مشتری</MudSelectItem> <MudSelectItem T="int" Value="3">تحویل به مشتری</MudSelectItem>
<MudSelectItem T="int" Value="4">مرجوع شده</MudSelectItem> <MudSelectItem T="int" Value="4">مرجوع شده</MudSelectItem>
<MudSelectItem T="int" Value="5">لغو شده</MudSelectItem>
<MudSelectItem T="int" Value="6">آماده تحویل در دفتر</MudSelectItem>
</MudSelect> </MudSelect>
</MudText> </MudText>
<MudTextField T="string" <MudTextField T="string"
@@ -123,6 +125,14 @@
{ {
<MudText Typo="Typo.caption">آدرسی ثبت نشده است.</MudText> <MudText Typo="Typo.caption">آدرسی ثبت نشده است.</MudText>
} }
@if (!string.IsNullOrWhiteSpace(_model.PostalCode))
{
<MudText Typo="Typo.body2">کد پستی: @_model.PostalCode</MudText>
}
@if (!string.IsNullOrWhiteSpace(_model.UserMobile))
{
<MudText Typo="Typo.body2">تلفن سفارش‌دهنده: @_model.UserMobile</MudText>
}
</MudStack> </MudStack>
</MudPaper> </MudPaper>
@@ -162,6 +172,13 @@
</MudStack> </MudStack>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<MudButton Variant="Variant.Outlined" Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.Print"
OnClick="PrintInvoiceAsync"
Disabled="_model is null || _isLoading">
چاپ فاکتور
</MudButton>
<MudSpacer />
<MudButton Variant="Variant.Outlined" Color="Color.Default" OnClick="Close"> <MudButton Variant="Variant.Outlined" Color="Color.Default" OnClick="Close">
بستن بستن
</MudButton> </MudButton>
@@ -1,6 +1,8 @@
using BackOffice.Common.Utilities;
using CMSMicroservice.Protobuf.Protos; using CMSMicroservice.Protobuf.Protos;
using CMSMicroservice.Protobuf.Protos.UserOrder; using CMSMicroservice.Protobuf.Protos.UserOrder;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor; using MudBlazor;
namespace BackOffice.Pages.UserOrder.Components; namespace BackOffice.Pages.UserOrder.Components;
@@ -9,6 +11,7 @@ public partial class UserOrderDetailsDialog
{ {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!; [CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!;
[Inject] public UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } = default!; [Inject] public UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } = default!;
[Inject] private IJSRuntime JsRuntime { get; set; } = default!;
[Parameter] public long OrderId { get; set; } [Parameter] public long OrderId { get; set; }
@@ -56,6 +59,8 @@ public partial class UserOrderDetailsDialog
2 => Color.Info, // InTransit 2 => Color.Info, // InTransit
3 => Color.Success, // Delivered 3 => Color.Success, // Delivered
4 => Color.Error, // Returned 4 => Color.Error, // Returned
5 => Color.Dark, // Cancelled
6 => Color.Primary, // ReadyForOfficePickup
_ => Color.Default // None / Unknown _ => Color.Default // None / Unknown
}; };
} }
@@ -68,6 +73,8 @@ public partial class UserOrderDetailsDialog
2 => "تحویل پست", 2 => "تحویل پست",
3 => "تحویل به مشتری", 3 => "تحویل به مشتری",
4 => "مرجوع شده", 4 => "مرجوع شده",
5 => "لغو شده",
6 => "آماده تحویل در دفتر",
_ => "بدون ارسال / نامشخص" _ => "بدون ارسال / نامشخص"
}; };
} }
@@ -178,5 +185,61 @@ public partial class UserOrderDetailsDialog
MudDialog.Close(DialogResult.Ok(true)); MudDialog.Close(DialogResult.Ok(true));
} }
private async Task PrintInvoiceAsync()
{
if (_model is null) return;
var vat = _model.VatInfo;
var lines = _model.FactorDetails.Select(f =>
{
var unit = f.UnitPrice ?? 0;
var count = f.Count ?? 0;
var discountUnit = f.UnitDiscountPrice ?? 0;
var lineDiscount = Math.Max(0, (unit - discountUnit) * count);
var lineTotal = discountUnit > 0 ? discountUnit * count : unit * count;
return new OrderInvoicePrint.InvoiceLine
{
Title = f.ProductTitle ?? "",
Quantity = count,
UnitPrice = unit,
Discount = lineDiscount,
LineTotal = lineTotal
};
}).ToList();
var subtotal = lines.Sum(l => l.UnitPrice * l.Quantity);
var discountTotal = lines.Sum(l => l.Discount);
var total = vat is { TotalAmount: > 0 } ? vat.TotalAmount : _model.Amount;
var html = OrderInvoicePrint.BuildHtml(new OrderInvoicePrint.InvoiceModel
{
ShopTitle = "فروشگاه عادی — کارا بازار سلامت",
OrderId = _model.Id.ToString(),
CustomerName = _model.UserFullName,
CustomerMobile = _model.UserMobile,
CustomerNationalCode = _model.UserNationalCode,
Address = _model.UserAddressText,
PostalCode = _model.PostalCode,
PaymentStatus = _model.PaymentStatus switch
{
PaymentStatus.Success => "پرداخت شده",
PaymentStatus.Pending => "در انتظار پرداخت",
_ => "پرداخت نشده"
},
PaymentMethod = GetPaymentMethodText(_model.PaymentMethod.GetHashCode()),
TransactionId = _model.TransactionId?.ToString(),
TrackingCode = _trackingCode ?? _model.TrackingCode,
Created = null,
PaymentDate = _model.PaymentDate?.ToDateTime(),
Subtotal = subtotal,
DiscountTotal = discountTotal,
VatAmount = vat?.VatAmount ?? 0,
TotalAmount = total,
Lines = lines
});
await JsRuntime.InvokeVoidAsync("jsHtmlToPdf", html, $"فاکتور سفارش {_model.Id}");
}
private void Close() => MudDialog.Close(); private void Close() => MudDialog.Close();
} }
@@ -46,6 +46,7 @@
<MudSelectItem T="int?" Value="@(3)">تحویل به مشتری</MudSelectItem> <MudSelectItem T="int?" Value="@(3)">تحویل به مشتری</MudSelectItem>
<MudSelectItem T="int?" Value="@(4)">مرجوع شده</MudSelectItem> <MudSelectItem T="int?" Value="@(4)">مرجوع شده</MudSelectItem>
<MudSelectItem T="int?" Value="@(5)">لغو شده</MudSelectItem> <MudSelectItem T="int?" Value="@(5)">لغو شده</MudSelectItem>
<MudSelectItem T="int?" Value="@(6)">آماده تحویل در دفتر</MudSelectItem>
</MudSelect> </MudSelect>
</MudItem> </MudItem>
<MudItem xs="12" sm="6" md="3"> <MudItem xs="12" sm="6" md="3">
@@ -1,4 +1,5 @@
using System.Text; using System.Text;
using BackOffice.Common.Utilities;
using CMSMicroservice.Protobuf.Protos; using CMSMicroservice.Protobuf.Protos;
using CMSMicroservice.Protobuf.Protos.UserOrder; using CMSMicroservice.Protobuf.Protos.UserOrder;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@@ -215,7 +216,8 @@ public partial class OrderSalesReports
2 => Color.Info, 2 => Color.Info,
3 => Color.Success, 3 => Color.Success,
4 => Color.Error, 4 => Color.Error,
5 => Color.Error, 5 => Color.Dark,
6 => Color.Primary,
_ => Color.Default _ => Color.Default
}; };
} }
@@ -229,6 +231,7 @@ public partial class OrderSalesReports
3 => "تحویل به مشتری", 3 => "تحویل به مشتری",
4 => "مرجوع شده", 4 => "مرجوع شده",
5 => "لغو شده", 5 => "لغو شده",
6 => "آماده تحویل در دفتر",
_ => "بدون ارسال / نامشخص" _ => "بدون ارسال / نامشخص"
}; };
} }
@@ -241,27 +244,26 @@ public partial class OrderSalesReports
return; return;
} }
var sb = new StringBuilder(); var headers = new[]
sb.AppendLine("شناسه,کاربر,نام کاربر,مبلغ,وضعیت پرداخت,وضعیت ارسال,تاریخ پرداخت");
foreach (var o in _orders)
{ {
sb.AppendLine(string.Join(",", "شناسه", "کاربر", "نام مشتری", "مبلغ", "وضعیت پرداخت", "وضعیت ارسال", "تاریخ پرداخت"
o.Id, };
o.UserId,
EscapeCsv(o.UserFullName),
o.Amount,
EscapeCsv(o.PaymentStatus == PaymentStatus.Success ? "پرداخت شده" : "پرداخت نشده"),
EscapeCsv(GetDeliveryStatusText(o.DeliveryStatus.GetHashCode())),
o.PaymentDate != null ? o.PaymentDate.ToDateTime().MiladiToJalaliWithTime() : "-"));
}
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray(); var rows = _orders.Select(o => new object?[]
var base64 = Convert.ToBase64String(bytes); {
var filename = $"store-sales-{DateTime.Now:yyyyMMddHHmmss}.csv"; o.Id,
o.UserId,
CsvExportHelper.CustomerName(o.UserFullName, userId: o.UserId),
o.Amount,
o.PaymentStatus == PaymentStatus.Success ? "پرداخت شده" : "پرداخت نشده",
GetDeliveryStatusText(o.DeliveryStatus.GetHashCode()),
o.PaymentDate != null ? o.PaymentDate.ToDateTime().MiladiToJalaliWithTime() : "-"
});
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64); var filename = $"store-sales-{DateTime.Now:yyyyMMddHHmmss}.xls";
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success); await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
CsvExportHelper.ToExcelXmlBase64("گزارش فروش", headers, rows));
Snackbar.Add("خروجی Excel آماده شد.", Severity.Success);
} }
protected async Task ExportToPdf() protected async Task ExportToPdf()
@@ -290,7 +292,7 @@ public partial class OrderSalesReports
foreach (var o in _orders) foreach (var o in _orders)
{ {
sb.AppendLine( sb.AppendLine(
$"{o.Id} | {o.UserFullName} | {o.Amount:N0} | {(o.PaymentStatus == PaymentStatus.Success ? "پرداخت شده" : "پرداخت نشده")} | {GetDeliveryStatusText(o.DeliveryStatus.GetHashCode())} | {(o.PaymentDate != null ? o.PaymentDate.ToDateTime().MiladiToJalaliWithTime() : "-")}"); $"{o.Id} | {CsvExportHelper.CustomerName(o.UserFullName, userId: o.UserId)} | {o.Amount:N0} | {(o.PaymentStatus == PaymentStatus.Success ? "پرداخت شده" : "پرداخت نشده")} | {GetDeliveryStatusText(o.DeliveryStatus.GetHashCode())} | {(o.PaymentDate != null ? o.PaymentDate.ToDateTime().MiladiToJalaliWithTime() : "-")}");
} }
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray(); var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
@@ -349,12 +351,4 @@ public partial class OrderSalesReports
} }
}; };
} }
private static string EscapeCsv(string value)
{
if (string.IsNullOrEmpty(value)) return "";
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
return $"\"{value.Replace("\"", "\"\"")}\"";
return value;
}
} }
@@ -56,6 +56,7 @@
<MudSelectItem T="int?" Value="@(3)">تحویل به مشتری</MudSelectItem> <MudSelectItem T="int?" Value="@(3)">تحویل به مشتری</MudSelectItem>
<MudSelectItem T="int?" Value="@(4)">مرجوع شده</MudSelectItem> <MudSelectItem T="int?" Value="@(4)">مرجوع شده</MudSelectItem>
<MudSelectItem T="int?" Value="@(5)">لغو شده</MudSelectItem> <MudSelectItem T="int?" Value="@(5)">لغو شده</MudSelectItem>
<MudSelectItem T="int?" Value="@(6)">آماده تحویل در دفتر</MudSelectItem>
</MudSelect> </MudSelect>
</MudItem> </MudItem>
</Filters> </Filters>
@@ -1,8 +1,9 @@
using System.Text.Json; using System.Text;
using System.Text; using System.Text.Json;
using BackOffice.Common.BaseComponents;
using BackOffice.Common.Utilities;
using CMSMicroservice.Protobuf.Protos; using CMSMicroservice.Protobuf.Protos;
using CMSMicroservice.Protobuf.Protos.UserOrder; using CMSMicroservice.Protobuf.Protos.UserOrder;
using BackOffice.Common.BaseComponents;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using MudBlazor; using MudBlazor;
@@ -213,6 +214,8 @@ public partial class UserOrderMainPage
2 => Color.Info, // InTransit 2 => Color.Info, // InTransit
3 => Color.Success, // Delivered 3 => Color.Success, // Delivered
4 => Color.Error, // Returned 4 => Color.Error, // Returned
5 => Color.Dark, // Cancelled
6 => Color.Primary, // ReadyForOfficePickup
_ => Color.Default // None / Unknown _ => Color.Default // None / Unknown
}; };
} }
@@ -225,6 +228,8 @@ public partial class UserOrderMainPage
2 => "تحویل پست", 2 => "تحویل پست",
3 => "تحویل به مشتری", 3 => "تحویل به مشتری",
4 => "مرجوع شده", 4 => "مرجوع شده",
5 => "لغو شده",
6 => "آماده تحویل در دفتر",
_ => "بدون ارسال / نامشخص" _ => "بدون ارسال / نامشخص"
}; };
} }
@@ -238,6 +243,15 @@ public partial class UserOrderMainPage
}; };
} }
// Domain PaymentStatus: Success=0, Reject=1, Pending=2
private static string GetPaymentStatusText(int status) => status switch
{
0 => "پرداخت شده",
1 => "پرداخت ناموفق",
2 => "در انتظار پرداخت",
_ => "نامشخص"
};
private async Task OpenChangeStatus(DataModel model) private async Task OpenChangeStatus(DataModel model)
{ {
var parameters = new DialogParameters var parameters = new DialogParameters
@@ -315,40 +329,31 @@ public partial class UserOrderMainPage
return; return;
} }
var sb = new StringBuilder(); var headers = new[]
sb.AppendLine("شناسه,کاربر,نام کاربر,مبلغ,وضعیت پرداخت,وضعیت ارسال,روش پرداخت,تاریخ پرداخت");
foreach (var o in result.Models)
{ {
sb.AppendLine(string.Join(",", "شناسه", "کاربر", "نام مشتری", "مبلغ", "وضعیت پرداخت", "وضعیت ارسال", "روش پرداخت", "تاریخ پرداخت"
o.Id, };
o.UserId,
EscapeCsv(o.UserFullName),
o.Amount,
EscapeCsv(o.PaymentStatus.ToString()),
EscapeCsv(GetDeliveryStatusText(o.DeliveryStatus.GetHashCode())),
EscapeCsv(GetPaymentMethodText(o.PaymentMethod.GetHashCode())),
o.PaymentDate?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-"));
}
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray(); var rows = result.Models.Select(o => new object?[]
var base64 = Convert.ToBase64String(bytes); {
var filename = $"user-orders-{DateTime.Now:yyyyMMddHHmmss}.csv"; o.Id,
o.UserId,
CsvExportHelper.CustomerName(o.UserFullName, userId: o.UserId),
o.Amount,
GetPaymentStatusText(o.PaymentStatus.GetHashCode()),
GetDeliveryStatusText(o.DeliveryStatus.GetHashCode()),
GetPaymentMethodText(o.PaymentMethod.GetHashCode()),
o.PaymentDate?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-"
});
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64); var filename = $"user-orders-{DateTime.Now:yyyyMMddHHmmss}.xls";
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success); await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
CsvExportHelper.ToExcelXmlBase64("سفارش‌ها", headers, rows));
Snackbar.Add("خروجی Excel آماده شد.", Severity.Success);
} }
catch (Exception ex) catch (Exception ex)
{ {
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error); Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
} }
} }
private static string EscapeCsv(string value)
{
if (string.IsNullOrEmpty(value)) return "";
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
return $"\"{value.Replace("\"", "\"\"")}\"";
return value;
}
} }
@@ -43,7 +43,7 @@
<MudText Color="Color.Primary"><strong>@context.Item.Balance.ToString("N0")</strong> تومان</MudText> <MudText Color="Color.Primary"><strong>@context.Item.Balance.ToString("N0")</strong> تومان</MudText>
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
<TemplateColumn Title="پاداش تیمی"> <TemplateColumn Title="پاداش های دریافتی">
<CellTemplate> <CellTemplate>
<MudText Color="Color.Success">@context.Item.NetworkBalance.ToString("N0") تومان</MudText> <MudText Color="Color.Success">@context.Item.NetworkBalance.ToString("N0") تومان</MudText>
</CellTemplate> </CellTemplate>
@@ -115,16 +115,26 @@
<MudText Color="Color.Primary">@context.Item.CurrentBalance.ToString("N0")</MudText> <MudText Color="Color.Primary">@context.Item.CurrentBalance.ToString("N0")</MudText>
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
<TemplateColumn Title="تغییر پاداش تیمی"> <TemplateColumn Title="تغییر پاداش های دریافتی">
<CellTemplate> <CellTemplate>
<MudText>@context.Item.ChangeNerworkValue.ToString("N0")</MudText> <MudText>@context.Item.ChangeNerworkValue.ToString("N0")</MudText>
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
<TemplateColumn Title="پاداش تیمی فعلی"> <TemplateColumn Title="پاداش های دریافتی فعلی">
<CellTemplate> <CellTemplate>
<MudText Color="Color.Success">@context.Item.CurrentNetworkBalance.ToString("N0")</MudText> <MudText Color="Color.Success">@context.Item.CurrentNetworkBalance.ToString("N0")</MudText>
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
<TemplateColumn Title="تغییر موجودی اعتباری">
<CellTemplate>
<MudText>@context.Item.ChangeDiscountValue.ToString("N0")</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="موجودی اعتباری">
<CellTemplate>
<MudText Color="Color.Tertiary">@context.Item.CurrentDiscountBalance.ToString("N0")</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="شناسه مرجع"> <TemplateColumn Title="شناسه مرجع">
<CellTemplate> <CellTemplate>
@(context.Item.RefrenceId?.ToString() ?? "—") @(context.Item.RefrenceId?.ToString() ?? "—")
@@ -102,6 +102,7 @@ public class DiscountOrderService : IDiscountOrderService
DiscountBalanceUsed = response.DiscountBalanceUsed, DiscountBalanceUsed = response.DiscountBalanceUsed,
GatewayAmount = response.GatewayAmount, GatewayAmount = response.GatewayAmount,
PaymentCompleted = response.PaymentCompleted, PaymentCompleted = response.PaymentCompleted,
PaymentStatusValue = (int)response.PaymentStatus,
TransactionId = response.TransactionId, TransactionId = response.TransactionId,
Status = MapFromProtoStatus(response.DeliveryStatus, response.PaymentCompleted), Status = MapFromProtoStatus(response.DeliveryStatus, response.PaymentCompleted),
TrackingCode = response.TrackingCode, TrackingCode = response.TrackingCode,
@@ -200,12 +201,13 @@ public class DiscountOrderService : IDiscountOrderService
return status switch return status switch
{ {
OrderStatus.Pending => ProtoDeliveryStatus.DeliveryPending, OrderStatus.Pending => ProtoDeliveryStatus.DeliveryPending,
OrderStatus.Paid => ProtoDeliveryStatus.DeliveryPending, // Paid but not processed yet OrderStatus.Paid => ProtoDeliveryStatus.DeliveryPending,
OrderStatus.Processing => ProtoDeliveryStatus.DeliveryProcessing, OrderStatus.Processing => ProtoDeliveryStatus.DeliveryProcessing,
OrderStatus.Shipped => ProtoDeliveryStatus.DeliveryShipped, OrderStatus.Shipped => ProtoDeliveryStatus.DeliveryShipped,
OrderStatus.Delivered => ProtoDeliveryStatus.DeliveryDelivered, OrderStatus.Delivered => ProtoDeliveryStatus.DeliveryDelivered,
OrderStatus.Cancelled => ProtoDeliveryStatus.DeliveryCancelled, OrderStatus.Cancelled => ProtoDeliveryStatus.DeliveryCancelled,
OrderStatus.Returned => ProtoDeliveryStatus.DeliveryCancelled, // Treat as cancelled OrderStatus.ReadyForOfficePickup => ProtoDeliveryStatus.DeliveryReadyForOffice,
OrderStatus.Returned => ProtoDeliveryStatus.DeliveryReturned,
_ => ProtoDeliveryStatus.DeliveryPending _ => ProtoDeliveryStatus.DeliveryPending
}; };
} }
@@ -221,6 +223,8 @@ public class DiscountOrderService : IDiscountOrderService
ProtoDeliveryStatus.DeliveryShipped => OrderStatus.Shipped, ProtoDeliveryStatus.DeliveryShipped => OrderStatus.Shipped,
ProtoDeliveryStatus.DeliveryDelivered => OrderStatus.Delivered, ProtoDeliveryStatus.DeliveryDelivered => OrderStatus.Delivered,
ProtoDeliveryStatus.DeliveryCancelled => OrderStatus.Cancelled, ProtoDeliveryStatus.DeliveryCancelled => OrderStatus.Cancelled,
ProtoDeliveryStatus.DeliveryReadyForOffice => OrderStatus.ReadyForOfficePickup,
ProtoDeliveryStatus.DeliveryReturned => OrderStatus.Returned,
_ => OrderStatus.Pending _ => OrderStatus.Pending
}; };
} }
@@ -235,6 +239,8 @@ public class DiscountOrderService : IDiscountOrderService
ProtoDeliveryStatus.DeliveryShipped => OrderStatus.Shipped, ProtoDeliveryStatus.DeliveryShipped => OrderStatus.Shipped,
ProtoDeliveryStatus.DeliveryDelivered => OrderStatus.Delivered, ProtoDeliveryStatus.DeliveryDelivered => OrderStatus.Delivered,
ProtoDeliveryStatus.DeliveryCancelled => OrderStatus.Cancelled, ProtoDeliveryStatus.DeliveryCancelled => OrderStatus.Cancelled,
ProtoDeliveryStatus.DeliveryReadyForOffice => OrderStatus.ReadyForOfficePickup,
ProtoDeliveryStatus.DeliveryReturned => OrderStatus.Returned,
_ => OrderStatus.Pending _ => OrderStatus.Pending
}; };
} }
@@ -27,13 +27,14 @@ public class OrderFilterDto
// For backward compatibility with UI - maps to proto DeliveryStatus // For backward compatibility with UI - maps to proto DeliveryStatus
public enum OrderStatus public enum OrderStatus
{ {
Pending = 0, // DELIVERY_PENDING Pending = 0, // DELIVERY_PENDING
Paid = 1, // Not in proto - handle in service Paid = 1, // Not in proto - handle in service
Processing = 2, // DELIVERY_PROCESSING Processing = 2, // DELIVERY_PROCESSING
Shipped = 3, // DELIVERY_SHIPPED Shipped = 3, // DELIVERY_SHIPPED
Delivered = 4, // DELIVERY_DELIVERED Delivered = 4, // DELIVERY_DELIVERED
Cancelled = 5, // DELIVERY_CANCELLED Cancelled = 5, // DELIVERY_CANCELLED
Returned = 6 // Not in proto - handle in service Returned = 6, // DELIVERY_RETURNED
ReadyForOfficePickup = 7 // DELIVERY_READY_FOR_OFFICE
} }
public class DiscountOrderDto public class DiscountOrderDto
@@ -62,11 +63,17 @@ public class DiscountOrderDetailsDto
public long Id { get; set; } public long Id { get; set; }
public long UserId { get; set; } public long UserId { get; set; }
public string OrderNumber { get; set; } = string.Empty; public string OrderNumber { get; set; } = string.Empty;
/// <summary>از ردیف لیست پر می‌شود (GetOrderById این فیلدها را ندارد)</summary>
public string UserFullName { get; set; } = string.Empty;
public string UserMobile { get; set; } = string.Empty;
public AddressInfoDto? Address { get; set; } public AddressInfoDto? Address { get; set; }
public long TotalPrice { get; set; } public long TotalPrice { get; set; }
public long DiscountBalanceUsed { get; set; } public long DiscountBalanceUsed { get; set; }
public long GatewayAmount { get; set; } public long GatewayAmount { get; set; }
public long VatAmount { get; set; }
public bool PaymentCompleted { get; set; } public bool PaymentCompleted { get; set; }
/// <summary>مقدار proto PaymentStatus: 0 Pending, 1 Completed, 2 Failed, 3 Refunded</summary>
public int PaymentStatusValue { get; set; }
public string? TransactionId { get; set; } public string? TransactionId { get; set; }
public OrderStatus Status { get; set; } public OrderStatus Status { get; set; }
public string? TrackingCode { get; set; } public string? TrackingCode { get; set; }
@@ -96,6 +96,7 @@
// Payment // Payment
"payment" => "پرداخت", "payment" => "پرداخت",
"transactions" => "تراکنش‌ها", "transactions" => "تراکنش‌ها",
"ledger-transactions" => "دفتر تراکنش‌های مالی",
"manual-payments" => "پرداخت دستی", "manual-payments" => "پرداخت دستی",
// Network // Network
+24 -1
View File
@@ -75,7 +75,12 @@
<MudNavLink Match="NavLinkMatch.Prefix" <MudNavLink Match="NavLinkMatch.Prefix"
Href="/club/members" Href="/club/members"
Icon="@Icons.Material.Filled.Groups"> Icon="@Icons.Material.Filled.Groups">
اعضا و آمار اعضا و گزارش‌ها
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/club/customer-packages"
Icon="@Icons.Material.Filled.PeopleAlt">
گزارش مشتریان پکیج
</MudNavLink> </MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix" <MudNavLink Match="NavLinkMatch.Prefix"
Href="/club/features" Href="/club/features"
@@ -200,6 +205,24 @@
</MudNavLink> </MudNavLink>
} }
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/payment/manual-credit-charges"
Icon="@Icons.Material.Filled.AddCard">
شارژ دستی کیف پول
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/payment/transactions"
Icon="@Icons.Material.Filled.ReceiptLong">
گزارش تراکنش درگاه
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/payment/ledger-transactions"
Icon="@Icons.Material.Filled.AccountBalance">
دفتر تراکنش‌های مالی
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix" <MudNavLink Match="NavLinkMatch.Prefix"
Href="/wallets" Href="/wallets"
Icon="@Icons.Material.Filled.AccountBalanceWallet"> Icon="@Icons.Material.Filled.AccountBalanceWallet">