Compare commits

...

34 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
masoodafar-web 933cdb1094 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
Deploy blog slug UI fix to production.
2026-06-08 02:06:52 +03:30
masoodafar-web 4596296d97 fix(blog): add optional slug field with auto-generate hint
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
BackOffice no longer requires manual slug; CMS generates it when empty.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 02:06:31 +03:30
masoodafar-web f2316eafe4 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 19m22s
2026-05-18 01:57:57 +03:30
masoodafar-web 9c6149bbb9 fix(ci): add --no-restore to publish step; suppress pre-existing MUD0002/CA2022/BL0005 warnings
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 17m42s
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 01:19:32 +03:30
masoodafar-web 8afc684098 feat(wallets): show DiscountBalance column in wallet management page; bump proto to 0.0.199
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 01:14:07 +03:30
masoodafar-web c17ad68c30 feat(commission/payouts): add bulk wallet credit button for pending payouts
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 6m1s
- UserPayouts.razor: add 'واریز به کیف پول' button in toolbar, enabled only when a week is selected; shows loading spinner while processing
- UserPayouts.razor.cs: implement BulkCreditWallets — confirms with dialog, calls BulkCreditPayoutsAsync, shows credited count + amount in snackbar
- BackOffice.csproj: bump Foursat.CMSMicroservice.Protobuf to 0.0.198

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 00:36:30 +03:30
masoodafar-web 9378b7474c fix: remove hardcoded GW_URL from staging manifest
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m27s
GW_URL in the K8s manifest was injected into all appsettings*.json files
by docker-entrypoint.sh, overriding even appsettings.Production.json.
Now only BLAZOR_ENVIRONMENT is set; GwUrl comes from the correct
appsettings.{Environment}.json per environment.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-14 01:35:20 +03:30
masoodafar-web 68d7647cb1 Bump Foursat.CMSMicroservice.Protobuf to 0.0.197 for proto parity with CMS.
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 5m43s
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 22:24:13 +03:30
masoodafar-web c4712a00ac Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 8m43s
2026-05-04 23:54:56 +03:30
masoodafar-web 3b4562665d fix(commission/dashboard): improve feedback on weekly calculation trigger success and error handling
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 7m40s
2026-05-04 18:44:40 +03:30
masoodafar-web 52c8965704 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 5m44s
2026-05-01 00:15:07 +03:30
masoodafar-web 7fb2062721 feat(dashboard): enhance dashboard layout and loading indicators, update week selector and package display
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 6m30s
feat(network): add activation type and package name display in network tree viewer
style(admin-org-chart): refine styles for node cards and add new activation/renewal badges
fix(admin-org-chart): adjust node dimensions for better visibility and add tooltip enhancements
2026-05-01 00:06:53 +03:30
masoodafar-web 797bfef964 fix(commission/dashboard): adjust week selector anchor and transform origins for better alignment
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 7m4s
2026-04-29 00:35:27 +03:30
52 changed files with 3426 additions and 873 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
See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation.
-2
View File
@@ -27,8 +27,6 @@ spec:
env:
- name: BLAZOR_ENVIRONMENT
value: "Staging"
- name: GW_URL
value: "https://cms.kbs2.ir"
resources:
requests:
memory: "128Mi"
+8 -3
View File
@@ -9,6 +9,8 @@
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<!-- Suppress pre-existing warnings that do not affect runtime correctness -->
<NoWarn>MUD0002;CA2022;BL0005</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -136,9 +138,12 @@
<!-- This enables direct communication with CMS (no BFF needed) -->
<!-- ============================================================ -->
<!-- CMS Protobuf NuGet package (used for both local dev and Docker builds) -->
<ItemGroup>
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.193" />
<!-- CMS Protobuf: NuGet in CI/Docker; ProjectReference for local monorepo dev -->
<ItemGroup Condition="Exists('../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj')">
<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>
<!-- ============================================================ -->
@@ -8,21 +8,46 @@ namespace BackOffice.Common.BaseComponents;
public partial class DateRangePicker
{
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 DateTime? DefaultStart { get; set; }
[Parameter] public DateTime? DefaultEnd { 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()
{
base.OnInitialized();
if (DefaultStart.HasValue)
_dateRange.Start = DefaultStart.Value.Date;
if (DefaultEnd.HasValue)
_dateRange.End = DefaultEnd.Value.Date;
var start = From ?? DefaultStart;
var end = To ?? DefaultEnd;
ApplyExternalRange(start, end);
_lastSyncedFrom = From;
_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()
{
var culture = new CultureInfo("fa-IR");
@@ -53,19 +78,35 @@ public partial class DateRangePicker
culture.NumberFormat.NumberNegativePattern = 0;
return culture;
}
private async Task OnClickOK()
{
if (_picker.DateRange is not null)
{
await OnChanged.InvokeAsync(_picker.DateRange);
}
var range = _picker.DateRange ?? _dateRange;
_dateRange = range ?? new DateRange();
await NotifyRangeChangedAsync(_dateRange);
await _picker.CloseAsync();
}
private async Task OnClickClear()
{
_dateRange = new DateRange();
await OnChanged.InvokeAsync(_dateRange);
await NotifyRangeChangedAsync(_dateRange);
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.UserWallet;
using CMSMicroservice.Protobuf.Protos.UserWalletHistory;
using CMSMicroservice.Protobuf.Protos.PaymentTransaction;
using CMSMicroservice.Protobuf.Protos.Transactions;
using CMSMicroservice.Protobuf.Protos.UserPackagePurchase;
using CMSMicroservice.Protobuf.Protos.ClubMembershipCycle;
using CMSMicroservice.Protobuf.Protos.ClubMembershipCycleHistory;
using BackOffice.Common.Utilities;
using BackOffice.Services.DiscountProduct;
using BackOffice.Services.DiscountCategory;
@@ -169,6 +174,13 @@ public static class ConfigureServices
services.AddTransient(sp => new UserWalletContract.UserWalletContractClient(sp.GetRequiredService<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;
}
@@ -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 ? "بله" : "خیر")}";
}
@@ -12,6 +12,11 @@
Required="true"
Variant="Variant.Outlined" />
<MudTextField @bind-Value="Model.Slug"
Label="نشانی URL (slug)"
Variant="Variant.Outlined"
HelperText="اختیاری — خالی بگذارید تا خودکار از عنوان ساخته شود. فقط حروف کوچک انگلیسی، عدد و خط تیره." />
<MudTextField @bind-Value="Model.Summary"
Label="خلاصه"
Variant="Variant.Outlined"
+24 -3
View File
@@ -1,17 +1,34 @@
@page "/club"
@page "/club/members"
@page "/club/purchases"
@page "/club/customer-packages"
@page "/club/cycles"
@page "/club/history"
@page "/club/statistics"
@attribute [Authorize]
@using BackOffice.Pages.Club.Reports
<div>
<MudText Typo="Typo.h5" Class="mb-3">باشگاه مشتریان</MudText>
<MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true"
PanelClass="pt-4" @bind-ActivePanelIndex="_activeTab">
<MudTabPanel Text="اعضای باشگاه" Icon="@Icons.Material.Filled.Groups">
<MudTabPanel Text="اعضا" Icon="@Icons.Material.Filled.Groups">
<ClubMembers />
</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 />
</MudTabPanel>
</MudTabs>
@@ -25,7 +42,11 @@
var uri = Navigation.ToBaseRelativePath(Navigation.Uri).ToLowerInvariant().TrimEnd('/');
_activeTab = uri switch
{
"club/statistics" => 1,
"club/purchases" => 1,
"club/customer-packages" => 2,
"club/cycles" => 3,
"club/history" => 4,
"club/statistics" => 5,
_ => 0
};
}
@@ -33,23 +33,11 @@
</tr>
<tr>
<td><strong>پکیج:</strong></td>
<td>@Member.PackageName</td>
</tr>
<tr>
<td><strong>کد فعال‌سازی:</strong></td>
<td dir="ltr">@Member.ActivationCode</td>
<td>@(string.IsNullOrEmpty(Member.PackageName) ? "—" : Member.PackageName)</td>
</tr>
<tr>
<td><strong>تاریخ فعال‌سازی:</strong></td>
<td>@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>
<td>@(Member.ActivatedAt != null ? Member.ActivatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() : "—")</td>
</tr>
<tr>
<td><strong>وضعیت:</strong></td>
@@ -61,60 +49,17 @@
</MudChip>
</td>
</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>
<td><strong>تاریخ ایجاد:</strong></td>
<td>@Member.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td>
</tr>
</tbody>
</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>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text">
تاریخچه تغییرات عضویت در تب «گزارش تاریخچه» باشگاه قابل مشاهده است.
</MudAlert>
</MudStack>
}
</DialogContent>
@@ -126,39 +71,5 @@
@code {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; }
[Parameter] public ClubMembershipModel Member { get; set; }
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubClient { get; set; }
private List<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();
}
@@ -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
};
}
+249 -203
View File
@@ -5,121 +5,59 @@
@using MudBlazor
@using BackOffice.Pages.AutoComplete
<div>
<MudText Typo="Typo.h4" Class="mb-4">داشبورد کمیسیون</MudText>
<MudStack Spacing="4">
@* ── G11: راهنمای کمیسیون (Q28) ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
هر پکیج Pool پورسانت مستقل دارد. مبالغ ActivationFee به Pool پکیج مربوطه واریز می‌شود.
</MudAlert>
@if (_isLoading)
{
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
}
else
{
<!-- Pool Summary Section -->
<MudGrid>
<MudItem xs="12" sm="6" md="3">
<MudCard Elevation="2">
<MudCardContent>
<MudText Typo="Typo.h6" Color="Color.Primary">استخر هفتگی</MudText>
<MudText Typo="Typo.h4">@(_poolData?.TotalPoolAmount.ToString("N0") ?? "0") تومان</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">@_currentWeekDisplayText</MudText>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudCard Elevation="2">
<MudCardContent>
<MudText Typo="Typo.h6" Color="Color.Success">تعداد تعادلها</MudText>
<MudText Typo="Typo.h4">@(_poolData?.TotalBalances.ToString("N0") ?? "0")</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">مجموع تعادل های فعال</MudText>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudCard Elevation="2">
<MudCardContent>
<MudText Typo="Typo.h6" Color="Color.Warning">ارزش هر تعادل</MudText>
<MudText Typo="Typo.h4">@(_poolData?.ValuePerBalance.ToString("N0") ?? "0") تومان</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">قیمت واحد تعادل</MudText>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudCard Elevation="2">
<MudCardContent>
<MudText Typo="Typo.h6" Color="@(_poolData?.IsCalculated == true ? Color.Success : Color.Error)">
وضعیت محاسبه
</MudText>
<MudText Typo="Typo.h4">
@(_poolData?.IsCalculated == true ? "محاسبه شده ✓" : "محاسبه نشده ✗")
</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">
@if (_poolData?.CalculatedAt != null)
{
var calculatedDate = _poolData.CalculatedAt.ToDateTime().ToLocalTime();
var persianDate = PersianDateTime.ConvertToPersianDateTime(calculatedDate);
@($"در تاریخ {persianDate}")
}
else
{
@("در انتظار محاسبه")
}
</MudText>
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
<!-- Week Selector -->
<MudPaper Class="pa-4 mt-4" Elevation="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3" Wrap="Wrap.Wrap">
<MudText Typo="Typo.h6">انتخاب هفته:</MudText>
@* ── عنوان صفحه ── *@
<div>
<MudText Typo="Typo.h5" Class="mb-1">داشبورد کمیسیون</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">
مبلغ پورسانت هر هفته بین اعضاء تقسیم می‌شود. هر پکیج استخر مستقل دارد.
</MudText>
</div>
@* ── فیلتر (اول از همه) ── *@
<MudPaper Elevation="1" Class="pa-4">
<MudGrid AlignItems="AlignItems.End" Spacing="3">
<MudItem xs="12" sm="5">
@if (_allWeeks.Any())
{
<MudSelect T="long"
Value="_currentWeekDefinitionId"
Label="شماره هفته"
Variant="Variant.Outlined"
Margin="Margin.Dense"
AnchorOrigin="Origin.BottomCenter"
Style="min-width: 400px;"
ValueChanged="OnWeekChanged">
Value="_currentWeekDefinitionId"
Label="هفته"
Variant="Variant.Outlined"
Margin="Margin.Dense"
AnchorOrigin="Origin.BottomLeft"
TransformOrigin="Origin.TopLeft"
AdornmentIcon="@Icons.Material.Filled.CalendarToday"
Adornment="Adornment.Start"
ValueChanged="OnWeekChanged">
@if (_availableWeeks?.CurrentWeek != null)
{
<MudSelectItem T="long" Value="@_availableWeeks.CurrentWeek.WeekDefinitionId">
<MudText>
<MudIcon Icon="@Icons.Material.Filled.Today" Size="Size.Small" Class="ml-2" />
@_availableWeeks.CurrentWeek.DisplayText
</MudText>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.Today" Size="Size.Small" Color="Color.Primary" />
<span>@_availableWeeks.CurrentWeek.DisplayText</span>
<MudChip T="string" Size="Size.Small" Color="Color.Primary" Variant="Variant.Text">جاری</MudChip>
</MudStack>
</MudSelectItem>
<MudDivider />
}
@if (_availableWeeks?.CalculatedWeeks?.Any() == true)
{
<MudListSubheader>هفته‌های محاسبه شده</MudListSubheader>
<MudListSubheader>تسویه‌شده</MudListSubheader>
@foreach (var week in _availableWeeks.CalculatedWeeks)
{
<MudSelectItem T="long" Value="@week.WeekDefinitionId">
<MudText>
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" Class="ml-2" />
@week.DisplayText
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Size="Size.Small" Color="Color.Success" />
<span>@week.DisplayText</span>
@if (week.TotalPoolAmount > 0)
{
<MudChip T="string" Size="Size.Small" Color="Color.Success" Class="mr-2">
@week.TotalPoolAmount.ToString("N0") تومان
</MudChip>
<MudText Typo="Typo.caption" Color="Color.Secondary">(@week.TotalPoolAmount.ToString("N0") T)</MudText>
}
</MudText>
</MudStack>
</MudSelectItem>
}
<MudDivider />
@@ -127,14 +65,14 @@
@if (_availableWeeks?.PendingWeeks?.Any() == true)
{
<MudListSubheader>هفته‌های محاسبه نشده</MudListSubheader>
<MudListSubheader>در انتظار تسویه</MudListSubheader>
@foreach (var week in _availableWeeks.PendingWeeks)
{
<MudSelectItem T="long" Value="@week.WeekDefinitionId">
<MudText>
<MudIcon Icon="@Icons.Material.Filled.AccessTime" Color="Color.Warning" Size="Size.Small" Class="ml-2" />
@week.DisplayText
</MudText>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.AccessTime" Size="Size.Small" Color="Color.Warning" />
<span>@week.DisplayText</span>
</MudStack>
</MudSelectItem>
}
<MudDivider />
@@ -146,10 +84,10 @@
@foreach (var week in _availableWeeks.FutureWeeks)
{
<MudSelectItem T="long" Value="@week.WeekDefinitionId">
<MudText>
<MudIcon Icon="@Icons.Material.Filled.CalendarMonth" Color="Color.Info" Size="Size.Small" Class="ml-2" />
@week.DisplayText
</MudText>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.CalendarMonth" Size="Size.Small" Color="Color.Tertiary" />
<span>@week.DisplayText</span>
</MudStack>
</MudSelectItem>
}
}
@@ -159,114 +97,222 @@
{
<MudText Color="Color.Warning">لیست هفته‌ها بارگذاری نشده</MudText>
}
</MudItem>
<MudItem xs="12" sm="5">
<PackageSelect Label="پکیج"
SelectedPackageId="_selectedPackageId"
SelectedPackageIdChanged="OnPackageChanged" />
</MudItem>
<MudItem xs="12" sm="2">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="LoadPoolData"
StartIcon="@Icons.Material.Filled.Refresh">
بارگذاری
Color="Color.Primary"
FullWidth="true"
OnClick="LoadPoolData"
StartIcon="@Icons.Material.Filled.Search"
Disabled="_isLoading">
نمایش
</MudButton>
</MudStack>
</MudPaper>
</MudItem>
</MudGrid>
</MudPaper>
<!-- Pool Details -->
@if (_poolData != null)
{
<MudPaper Class="pa-4 mt-4" Elevation="2">
<MudText Typo="Typo.h6" Class="mb-3">جزئیات استخر</MudText>
<MudSimpleTable Hover="true" Dense="true">
<tbody>
<tr>
<td><strong>شناسه استخر:</strong></td>
<td>@_poolData.Id</td>
</tr>
<tr>
<td><strong>پکیج:</strong></td>
<td>
@if (!string.IsNullOrEmpty(_poolData.PackageTitle))
{
<MudChip T="string" Size="Size.Small" Color="Color.Primary">@_poolData.PackageTitle</MudChip>
}
else
{
<MudText Color="Color.Secondary">-</MudText>
}
</td>
</tr>
<tr>
<td><strong>شماره هفته:</strong></td>
<td>@_currentWeekDisplayName</td>
</tr>
<tr>
<td><strong>مجموع استخر:</strong></td>
<td>@_poolData.TotalPoolAmount.ToString("N0") تومان</td>
</tr>
<tr>
<td><strong>تعداد تعادل:</strong></td>
<td>@_poolData.TotalBalances</td>
</tr>
<tr>
<td><strong>ارزش هر تعادل:</strong></td>
<td>@_poolData.ValuePerBalance.ToString("N0") تومان</td>
</tr>
<tr>
<td><strong>وضعیت:</strong></td>
<td>
<MudChip T="string" Color="@(_poolData.IsCalculated ? Color.Success : Color.Error)" Size="Size.Small">
@(_poolData.IsCalculated ? "محاسبه شده" : "محاسبه نشده")
</MudChip>
</td>
</tr>
<tr>
<td><strong>تاریخ محاسبه:</strong></td>
<td>
@if (_poolData.CalculatedAt != null)
{
@_poolData.CalculatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()
}
else
{
<MudText Color="Color.Secondary">-</MudText>
}
</td>
</tr>
<tr>
<td><strong>تاریخ ایجاد:</strong></td>
<td>@_poolData.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td>
</tr>
</tbody>
</MudSimpleTable>
</MudPaper>
}
<!-- Quick Actions -->
<MudPaper Class="pa-4 mt-4" Elevation="2">
<MudText Typo="Typo.h6" Class="mb-3">عملیات سریع</MudText>
<MudStack Row="true" Spacing="2">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Payments"
Href="/commission/payouts">
مشاهده پرداخت‌ها
</MudButton>
<MudButton Variant="Variant.Filled"
Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.AccountBalance"
Href="/commission/withdrawals">
درخواست‌های برداشت
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Info"
StartIcon="@Icons.Material.Filled.Calculate"
Disabled="@(_poolData?.IsCalculated == true)"
OnClick="TriggerCalculation">
اجرای محاسبه دستی
</MudButton>
@* ── لودینگ ── *@
@if (_isLoading)
{
<MudPaper Elevation="0" Class="pa-6 d-flex justify-center align-center">
<MudStack AlignItems="AlignItems.Center" Spacing="2">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Medium" />
<MudText Typo="Typo.body2" Color="Color.Secondary">در حال بارگذاری اطلاعات استخر…</MudText>
</MudStack>
</MudPaper>
}
</div>
else
{
@* ── نوار وضعیت هفته / پکیج ── *@
<MudPaper Elevation="0" Class="pa-3 mud-background-gray rounded">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Wrap="Wrap.Wrap">
<MudIcon Icon="@Icons.Material.Filled.DateRange" Color="Color.Default" Size="Size.Small" />
<MudText Typo="Typo.body1">@_currentWeekDisplayText</MudText>
@if (!string.IsNullOrEmpty(_poolData?.PackageTitle))
{
<MudDivider Vertical="true" FlexItem="true" />
<MudChip T="string" Size="Size.Small" Color="Color.Primary" Icon="@Icons.Material.Filled.WorkspacePremium">
@_poolData.PackageTitle
</MudChip>
}
<MudSpacer />
@if (_poolData != null && _poolData.Id > 0)
{
@if (_poolData.IsCalculated)
{
<MudChip T="string" Color="Color.Success" Size="Size.Small"
Icon="@Icons.Material.Filled.CheckCircle" Variant="Variant.Filled">
تسویه انجام شده
</MudChip>
}
else
{
<MudChip T="string" Color="Color.Warning" Size="Size.Small"
Icon="@Icons.Material.Filled.Pending" Variant="Variant.Filled">
در انتظار تسویه
</MudChip>
}
}
</MudStack>
</MudPaper>
@* ── ۳ کارت اصلی ── *@
<MudGrid Spacing="3">
@* کارت ۱: کل مبلغ استخر *@
<MudItem xs="12" sm="4">
<MudPaper Elevation="2" Class="pa-4" Style="border-top: 4px solid var(--mud-palette-primary);">
<MudStack Spacing="1">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Color="Color.Primary" Size="Size.Small" />
<MudText Typo="Typo.caption" Color="Color.Secondary">کل مبلغ استخر پورسانت</MudText>
</MudStack>
<MudText Typo="Typo.h5" Style="font-weight:700; direction:ltr; text-align:right;">
@((_poolData?.TotalPoolAmount ?? 0).ToString("N0"))
</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">تومان — جمع کارمزد فعال‌سازی‌های این هفته</MudText>
</MudStack>
</MudPaper>
</MudItem>
@* کارت ۲: تعداد واحدهای پورسانت *@
<MudItem xs="12" sm="4">
<MudPaper Elevation="2" Class="pa-4" Style="border-top: 4px solid var(--mud-palette-success);">
<MudStack Spacing="1">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.People" Color="Color.Success" Size="Size.Small" />
<MudText Typo="Typo.caption" Color="Color.Secondary">واحدهای پورسانت (بالانس)</MudText>
<MudTooltip Text="تعداد واحدهایی که پورسانت به آن‌ها تعلق می‌گیرد. هر عضو بر اساس شبکه‌اش چند واحد دریافت می‌کند.">
<MudIcon Icon="@Icons.Material.Filled.HelpOutline" Size="Size.Small" Color="Color.Tertiary" />
</MudTooltip>
</MudStack>
<MudText Typo="Typo.h5" Style="font-weight:700; direction:ltr; text-align:right;">
@((_poolData?.TotalBalances ?? 0).ToString("N0"))
</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">واحد — مجموع همه اعضاء فعال</MudText>
</MudStack>
</MudPaper>
</MudItem>
@* کارت ۳: سهم هر واحد *@
<MudItem xs="12" sm="4">
<MudPaper Elevation="2" Class="pa-4" Style="border-top: 4px solid var(--mud-palette-warning);">
<MudStack Spacing="1">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.LocalAtm" Color="Color.Warning" Size="Size.Small" />
<MudText Typo="Typo.caption" Color="Color.Secondary">سهم پورسانت هر واحد</MudText>
</MudStack>
<MudText Typo="Typo.h5" Style="font-weight:700; direction:ltr; text-align:right;">
@((_poolData?.ValuePerBalance ?? 0).ToString("N0"))
</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">
تومان — @((_poolData?.IsCalculated == true) ? "قطعی شده" : "قطعی نشده")
</MudText>
</MudStack>
</MudPaper>
</MudItem>
</MudGrid>
@* ── اعتبارسنجی مبلغ استخر ── *@
@if (_poolData != null && _poolData.Id > 0)
{
@if (!_poolData.ValidationIsValid)
{
<MudAlert Severity="Severity.Error" Variant="Variant.Filled" Icon="@Icons.Material.Filled.ReportProblem" NoIcon="false">
<MudText Typo="Typo.subtitle2" Class="mb-2">مبلغ استخر با تعداد فعال‌سازی‌های دیتابیس مطابقت ندارد</MudText>
<MudGrid Spacing="2">
<MudItem xs="12" sm="4">
<MudText Typo="Typo.caption">مبلغ ثبت‌شده</MudText>
<MudText Typo="Typo.body2"><strong>@_poolData.TotalPoolAmount.ToString("N0") تومان</strong></MudText>
</MudItem>
<MudItem xs="12" sm="4">
<MudText Typo="Typo.caption">مبلغ انتظاری (@_poolData.ValidationActivationCount فعال‌سازی)</MudText>
<MudText Typo="Typo.body2"><strong>@_poolData.ValidationExpectedAmount.ToString("N0") تومان</strong></MudText>
</MudItem>
<MudItem xs="12" sm="4">
<MudText Typo="Typo.caption">اختلاف</MudText>
<MudText Typo="Typo.body2" Style="color:var(--mud-palette-warning-lighten);">
<strong>@_poolData.ValidationDiscrepancy.ToString("N0") تومان</strong>
</MudText>
</MudItem>
</MudGrid>
</MudAlert>
}
else if (_poolData.ValidationActivationCount > 0)
{
<MudAlert Severity="Severity.Success" Variant="Variant.Text" Dense="true" Icon="@Icons.Material.Filled.VerifiedUser">
مبلغ استخر صحیح است — با @_poolData.ValidationActivationCount فعال‌سازی در این هفته مطابقت دارد.
</MudAlert>
}
}
@* ── جزئیات تکمیلی (بدون تکرار اطلاعات کارت‌ها) ── *@
@if (_poolData != null && _poolData.Id > 0)
{
<MudExpansionPanels Elevation="1">
<MudExpansionPanel Text="جزئیات بیشتر" Icon="@Icons.Material.Filled.ExpandMore">
<MudSimpleTable Dense="true" Hover="true" Style="max-width:540px;">
<tbody>
<tr>
<td style="width:200px;"><MudText Typo="Typo.body2" Color="Color.Secondary">شناسه استخر</MudText></td>
<td><MudText Typo="Typo.body2">#@_poolData.Id</MudText></td>
</tr>
<tr>
<td><MudText Typo="Typo.body2" Color="Color.Secondary">تاریخ ایجاد استخر</MudText></td>
<td><MudText Typo="Typo.body2">@_poolData.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</MudText></td>
</tr>
@if (_poolData.CalculatedAt != null)
{
<tr>
<td><MudText Typo="Typo.body2" Color="Color.Secondary">تاریخ تسویه</MudText></td>
<td><MudText Typo="Typo.body2">@_poolData.CalculatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</MudText></td>
</tr>
}
</tbody>
</MudSimpleTable>
</MudExpansionPanel>
</MudExpansionPanels>
}
@* ── دکمه‌های عملیاتی ── *@
<MudPaper Elevation="0" Class="pa-3 mud-background-gray rounded">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Wrap="Wrap.Wrap">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Payments"
Href="/commission/payouts"
Size="Size.Medium">
پرداخت‌های این هفته
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Default"
StartIcon="@Icons.Material.Filled.AccountBalance"
Href="/commission/withdrawals"
Size="Size.Medium">
درخواست‌های برداشت
</MudButton>
<MudSpacer />
@if (_poolData?.IsCalculated != true)
{
<MudButton Variant="Variant.Filled"
Color="Color.Warning"
StartIcon="@Icons.Material.Filled.Calculate"
OnClick="TriggerCalculation"
Size="Size.Medium">
اجرای تسویه دستی
</MudButton>
}
</MudStack>
</MudPaper>
}
</MudStack>
@@ -160,8 +160,17 @@ public partial class Dashboard
req.PackageId = _selectedPackageId;
await CommissionContract.TriggerWeeklyCalculationAsync(req);
Snackbar.Add("محاسبه با موفقیت آغاز شد. این عملیات ممکن است چند دقیقه طول بکشد.", Severity.Info);
var response = await CommissionContract.TriggerWeeklyCalculationAsync(req);
if (response.Success)
{
Snackbar.Add("محاسبه با موفقیت انجام شد.", Severity.Success);
}
else
{
Snackbar.Add($"خطا در محاسبه: {response.Message}", Severity.Error);
return;
}
// Reload data after a delay
await Task.Delay(2000);
@@ -49,6 +49,21 @@
StartIcon="@Icons.Material.Filled.Search">
جستجو
</MudButton>
<MudButton Variant="Variant.Filled"
Color="Color.Success"
StartIcon="@Icons.Material.Filled.AccountBalanceWallet"
Disabled="@(!_filterWeekDefinitionId.HasValue || _isCreditingWallets)"
OnClick="BulkCreditWallets">
@if (_isCreditingWallets)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="ms-n1 me-2" />
<span>در حال واریز...</span>
}
else
{
<span>واریز به کیف پول</span>
}
</MudButton>
</MudStack>
</ToolBarContent>
<Columns>
@@ -18,6 +18,7 @@ public partial class UserPayouts
private long? _filterWeekDefinitionId;
private int? _filterStatus;
private long? _filterPackageId;
private bool _isCreditingWallets;
private async Task<GridData<UserCommissionPayoutModel>> ServerReload(GridState<UserCommissionPayoutModel> state)
{
@@ -147,6 +148,50 @@ public partial class UserPayouts
}
}
private async Task BulkCreditWallets()
{
if (!_filterWeekDefinitionId.HasValue)
{
Snackbar.Add("لطفاً ابتدا یک هفته انتخاب کنید", Severity.Warning);
return;
}
var confirmed = await DialogService.ShowMessageBox(
"واریز دسته‌جمعی به کیف پول",
$"آیا از واریز کمیسیون همه پرداخت‌های «در انتظار» هفته انتخاب‌شده به کیف پول کاربران مطمئن هستید؟ این عملیات برگشت‌پذیر نیست.",
yesText: "بله، واریز کن", cancelText: "لغو");
if (confirmed != true)
return;
_isCreditingWallets = true;
StateHasChanged();
try
{
var result = await CommissionContract.BulkCreditPayoutsAsync(new BulkCreditPayoutsRequest
{
WeekDefinitionId = _filterWeekDefinitionId.Value
});
if (result.CreditedCount == 0)
Snackbar.Add("هیچ پرداخت در انتظاری برای این هفته یافت نشد", Severity.Info);
else
Snackbar.Add(result.Message, Severity.Success);
await ApplyFilter();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در واریز: {ex.Message}", Severity.Error);
}
finally
{
_isCreditingWallets = false;
StateHasChanged();
}
}
private async Task RejectWithdrawal(UserCommissionPayoutModel payout)
{
var reason = await DialogService.ShowMessageBox(
@@ -55,7 +55,7 @@
Margin="Margin.Dense">
<MudSelectItem T="int?" Value="@(null as int?)">همه وضعیت‌ها</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="3">برداشت انجام شده</MudSelectItem>
<MudSelectItem T="int?" Value="4">خطا در پرداخت بانکی</MudSelectItem>
@@ -26,6 +26,7 @@
<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.ReadyForOfficePickup">آماده تحویل در دفتر</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
</MudSelect>
@@ -55,6 +56,15 @@
</MudItem>
}
@if (Model.Status == OrderStatus.ReadyForOfficePickup)
{
<MudItem xs="12">
<MudAlert Severity="Severity.Info">
<MudText>سفارش برای تحویل حضوری در دفتر آماده است.</MudText>
</MudAlert>
</MudItem>
}
@if (Model.Status == OrderStatus.Shipped)
{
<MudItem xs="12">
@@ -126,8 +136,9 @@
OrderStatus.Processing => Color.Primary,
OrderStatus.Shipped => Color.Secondary,
OrderStatus.Delivered => Color.Success,
OrderStatus.Cancelled => Color.Error,
OrderStatus.Returned => Color.Dark,
OrderStatus.ReadyForOfficePickup => Color.Primary,
OrderStatus.Cancelled => Color.Dark,
OrderStatus.Returned => Color.Error,
_ => Color.Default
};
}
@@ -141,6 +152,7 @@
OrderStatus.Processing => "در حال آماده‌سازی",
OrderStatus.Shipped => "ارسال شده",
OrderStatus.Delivered => "تحویل داده شده",
OrderStatus.ReadyForOfficePickup => "آماده تحویل در دفتر",
OrderStatus.Cancelled => "لغو شده",
OrderStatus.Returned => "مرجوع شده",
_ => "نامشخص"
@@ -1,36 +1,72 @@
@using BackOffice.Services.DiscountOrder
@using BackOffice.Common.BaseComponents
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.ShoppingCart" Class="ml-2" />
جزئیات سفارش #@Order.OrderNumber
</MudText>
</TitleContent>
<DialogContent>
<MudGrid>
<!-- اطلاعات کاربر -->
<MudItem xs="12">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" GutterBottom="true">اطلاعات سفارش</MudText>
<MudGrid>
<MudItem xs="6">
<MudText Typo="Typo.body2" Color="Color.Secondary">شماره سفارش:</MudText>
<MudText Typo="Typo.body1"><strong>@Order.OrderNumber</strong></MudText>
</MudItem>
<MudItem xs="6">
<MudText Typo="Typo.body2" Color="Color.Secondary">شناسه کاربر:</MudText>
<MudText Typo="Typo.body1">@Order.UserId</MudText>
</MudItem>
</MudGrid>
</MudPaper>
</MudItem>
<MudStack Spacing="2">
<MudText Typo="Typo.h6">جزئیات سفارش شماره @(string.IsNullOrWhiteSpace(Order.OrderNumber) ? Order.Id.ToString() : Order.OrderNumber)</MudText>
<MudText Typo="Typo.body2">
کاربر:
@if (!string.IsNullOrWhiteSpace(Order.UserFullName))
{
@Order.UserFullName
}
else
{
<text>#@Order.UserId</text>
}
@if (!string.IsNullOrWhiteSpace(DisplayMobile))
{
<text> (@DisplayMobile)</text>
}
</MudText>
<!-- Timeline وضعیت -->
<MudItem xs="12">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.subtitle2" GutterBottom="true">Timeline وضعیت</MudText>
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center" Justify="Justify.Center">
<MudPaper Class="pa-3">
<MudStack Spacing="2">
<MudText Typo="Typo.subtitle2">خلاصه سفارش</MudText>
<MudDivider />
<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)
{
<MudStack Spacing="0" AlignItems="AlignItems.Center">
@@ -41,218 +77,155 @@
</MudStack>
}
</MudStack>
</MudPaper>
</MudItem>
<!-- وضعیت سفارش -->
<MudItem xs="12" sm="6">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" GutterBottom="true">وضعیت سفارش</MudText>
<MudChip T="string" Color="@GetStatusColor(Order.Status)" Size="Size.Large">
@GetStatusText(Order.Status)
</MudChip>
@if (Order.Created.HasValue)
{
<MudText Typo="Typo.body2" Class="mt-2">
تاریخ ثبت: @Order.Created.Value.MiladiToJalaliWithTime()
</MudText>
}
</MudPaper>
</MudItem>
<!-- وضعیت پرداخت -->
<MudItem xs="12" sm="6">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" GutterBottom="true">وضعیت پرداخت</MudText>
@if (Order.PaymentCompleted)
{
<MudChip T="string" Color="Color.Success" Size="Size.Large" Icon="@Icons.Material.Filled.CheckCircle">
پرداخت شده
</MudChip>
@if (!string.IsNullOrEmpty(Order.TransactionId))
{
<MudText Typo="Typo.body2" Class="mt-2">
کد تراکنش: @Order.TransactionId
</MudText>
}
}
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>
<MudDivider Class="my-2" />
<MudText Typo="Typo.body2">
وضعیت ارسال:
<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.ReadyForOfficePickup">آماده تحویل در دفتر</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
</MudSelect>
</MudText>
<MudTextField T="string"
Label="کد رهگیری"
Variant="Variant.Outlined"
Dense="true"
@bind-Value="_trackingCode" />
<MudTextField T="string"
Label="یادداشت ادمین"
Variant="Variant.Outlined"
Dense="true"
Lines="3"
HelperText="دلیل تغییر وضعیت یا توضیحات اضافی"
@bind-Value="_adminNotes" />
@if (_newStatus == OrderStatus.Cancelled || _newStatus == OrderStatus.Returned)
{
<MudAlert Severity="Severity.Warning" Class="mt-2">
<MudAlert Severity="Severity.Warning" Dense="true">
توجه: در صورت لغو یا مرجوعی سفارش، موجودی محصولات به انبار بازگردانده می‌شود.
</MudAlert>
}
@if (_newStatus == OrderStatus.ReadyForOfficePickup)
{
<MudAlert Severity="Severity.Info" Dense="true">
سفارش برای تحویل حضوری در دفتر آماده است.
</MudAlert>
}
@if (_newStatus == OrderStatus.Shipped)
{
<MudAlert Severity="Severity.Info" Class="mt-2">
پس از تغییر وضعیت به "ارسال شده"، اطلاع‌رسانی به کاربر ارسال خواهد شد.
<MudAlert Severity="Severity.Info" Dense="true">
پس از تغییر وضعیت به «ارسال شده»، اطلاع‌رسانی به کاربر ارسال خواهد شد.
</MudAlert>
}
</MudPaper>
</MudItem>
</MudStack>
</MudPaper>
<!-- یادداشت‌ها -->
@if (!string.IsNullOrEmpty(Order.Notes) || !string.IsNullOrEmpty(Order.AdminNotes))
<MudPaper Class="pa-3">
<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-4" Elevation="1">
@if (!string.IsNullOrEmpty(Order.Notes))
{
<MudText Typo="Typo.h6" GutterBottom="true">
<MudIcon Icon="@Icons.Material.Filled.Comment" Size="Size.Small" />
یادداشت کاربر
</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>
<MudPaper Class="pa-3">
<MudStack Spacing="1">
<MudText Typo="Typo.subtitle2">یادداشت کاربر</MudText>
<MudDivider />
<MudText Typo="Typo.body2">@Order.Notes</MudText>
</MudStack>
</MudPaper>
}
</MudGrid>
</MudStack>
</DialogContent>
<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>
@@ -1,5 +1,7 @@
using BackOffice.Common.Utilities;
using BackOffice.Services.DiscountOrder;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor;
namespace BackOffice.Pages.DiscountShop.Components;
@@ -8,6 +10,7 @@ public partial class OrderDetailsDialog
{
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!;
[Inject] public IDiscountOrderService OrderService { get; set; } = default!;
[Inject] private IJSRuntime JsRuntime { get; set; } = default!;
[Parameter] public DiscountOrderDetailsDto Order { get; set; } = null!;
@@ -17,6 +20,9 @@ public partial class OrderDetailsDialog
private bool _isSaving;
private List<TimelineStep> _timelineSteps = new();
private string? DisplayMobile =>
!string.IsNullOrWhiteSpace(Order.UserMobile) ? Order.UserMobile : Order.Address?.Phone;
protected override void OnInitialized()
{
_newStatus = Order.Status;
@@ -127,34 +133,69 @@ public partial class OrderDetailsDialog
private void Close() => MudDialog.Close();
private Color GetStatusColor(OrderStatus status)
private Color GetPaymentStatusColor(int status) => status switch
{
return status switch
{
OrderStatus.Pending => Color.Warning,
OrderStatus.Paid => Color.Info,
OrderStatus.Processing => Color.Primary,
OrderStatus.Shipped => Color.Secondary,
OrderStatus.Delivered => Color.Success,
OrderStatus.Cancelled => Color.Error,
OrderStatus.Returned => Color.Dark,
_ => Color.Default
};
}
0 => Color.Warning,
1 => Color.Success,
2 => Color.Error,
3 => Color.Info,
_ => 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 => "در انتظار پرداخت",
OrderStatus.Paid => "پرداخت شده",
OrderStatus.Processing => "در حال آماده‌سازی",
OrderStatus.Shipped => "ارسال شده",
OrderStatus.Delivered => "تحویل داده شده",
OrderStatus.Cancelled => "لغو شده",
OrderStatus.Returned => "مرجوع شده",
_ => "نامشخص"
};
Title = i.ProductTitle,
Quantity = i.Count,
UnitPrice = i.UnitPrice,
Discount = i.DiscountAmount,
LineTotal = i.FinalPrice > 0 ? i.FinalPrice : i.UnitPrice * i.Count
}).ToList();
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
@@ -49,6 +49,8 @@
<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.ReadyForOfficePickup">آماده تحویل در دفتر</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
</MudSelect>
</MudItem>
</Filters>
@@ -1,5 +1,6 @@
using System.Text;
using BackOffice.Common.BaseComponents;
using BackOffice.Common.Utilities;
using BackOffice.Services.DiscountOrder;
using BackOffice.Pages.DiscountShop.Components;
using Microsoft.AspNetCore.Components;
@@ -74,6 +75,15 @@ public partial class DiscountOrdersMainPage
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 options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large, FullWidth = true };
var dialog = await DialogService.ShowAsync<OrderDetailsDialog>("جزئیات سفارش", parameters, options);
@@ -178,8 +188,9 @@ public partial class DiscountOrdersMainPage
OrderStatus.Processing => Color.Primary,
OrderStatus.Shipped => Color.Secondary,
OrderStatus.Delivered => Color.Success,
OrderStatus.Cancelled => Color.Error,
OrderStatus.Returned => Color.Dark,
OrderStatus.Cancelled => Color.Dark,
OrderStatus.ReadyForOfficePickup => Color.Primary,
OrderStatus.Returned => Color.Error,
_ => Color.Default
};
}
@@ -194,6 +205,7 @@ public partial class DiscountOrdersMainPage
OrderStatus.Shipped => "ارسال شده",
OrderStatus.Delivered => "تحویل داده شده",
OrderStatus.Cancelled => "لغو شده",
OrderStatus.ReadyForOfficePickup => "آماده تحویل در دفتر",
OrderStatus.Returned => "مرجوع شده",
_ => "نامشخص"
};
@@ -233,44 +245,36 @@ public partial class DiscountOrdersMainPage
try
{
var sb = new StringBuilder();
sb.AppendLine("شناسه,نام کاربر,موبایل,مبلغ کل,کیف پول اعتباری,پرداخت درگاه,تعداد آیتم,وضعیت پرداخت,وضعیت ارسال,تاریخ پرداخت,تاریخ ثبت,آدرس");
foreach (var o in _orders)
var headers = new[]
{
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 base64 = Convert.ToBase64String(bytes);
var filename = $"discount-orders-{DateTime.Now:yyyyMMddHHmmss}.csv";
var rows = _orders.Select(o => new object?[]
{
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);
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
var filename = $"discount-orders-{DateTime.Now:yyyyMMddHHmmss}.xls";
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
CsvExportHelper.ToExcelXmlBase64("سفارش‌های اعتباری", headers, rows));
Snackbar.Add("خروجی Excel آماده شد.", Severity.Success);
}
catch (Exception ex)
{
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.Shipped">ارسال شده</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.Returned">مرجوع شده</MudSelectItem>
</MudSelect>
@@ -498,7 +499,8 @@
OrderStatus.Processing => Color.Info,
OrderStatus.Shipped => Color.Info,
OrderStatus.Delivered => Color.Success,
OrderStatus.Cancelled => Color.Error,
OrderStatus.ReadyForOfficePickup => Color.Primary,
OrderStatus.Cancelled => Color.Dark,
OrderStatus.Returned => Color.Error,
_ => Color.Default
};
@@ -513,6 +515,7 @@
OrderStatus.Processing => "در حال پردازش",
OrderStatus.Shipped => "ارسال شده",
OrderStatus.Delivered => "تحویل شده",
OrderStatus.ReadyForOfficePickup => "آماده تحویل در دفتر",
OrderStatus.Cancelled => "لغو شده",
OrderStatus.Returned => "مرجوع شده",
_ => "نامشخص"
@@ -527,28 +530,31 @@
return;
}
var sb = new StringBuilder();
sb.AppendLine("شناسه,نام کاربر,موبایل,تاریخ ثبت,تعداد آیتم,مبلغ نهایی,تخفیف,وضعیت");
foreach (var o in _orders.Where(o => o.Created.HasValue).OrderBy(o => o.Created))
var headers = new[]
{
sb.AppendLine(string.Join(",",
"شناسه", "نام مشتری", "موبایل", "تاریخ ثبت", "تعداد آیتم", "مبلغ نهایی", "تخفیف", "وضعیت"
};
var rows = _orders
.OrderBy(o => o.Created ?? o.PaymentDate ?? DateTime.MaxValue)
.Select(o => new object?[]
{
o.Id,
EscapeCsv(o.UserFullName),
EscapeCsv(o.UserMobile),
o.Created?.MiladiToJalaliWithTime() ?? "-",
CsvExportHelper.CustomerName(o.UserFullName, o.UserMobile, o.UserId),
o.UserMobile ?? "",
o.Created?.MiladiToJalaliWithTime()
?? o.PaymentDate?.MiladiToJalaliWithTime()
?? "-",
o.ItemsCount,
o.GatewayAmount,
o.DiscountBalanceUsed,
GetStatusText(o.Status)));
}
GetStatusText(o.Status)
});
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
var base64 = Convert.ToBase64String(bytes);
var filename = $"discount-sales-{DateTime.Now:yyyyMMddHHmmss}.csv";
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
var filename = $"discount-sales-{DateTime.Now:yyyyMMddHHmmss}.xls";
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
CsvExportHelper.ToExcelXmlBase64("گزارش فروش اعتباری", headers, rows));
Snackbar.Add("خروجی Excel آماده شد.", Severity.Success);
}
private async Task ExportToPdf()
@@ -572,12 +578,12 @@
sb.AppendLine();
sb.AppendLine("جزئیات سفارش‌ها:");
sb.AppendLine("شناسه | نام کاربر | تاریخ | تعداد آیتم | مبلغ نهایی | تخفیف | وضعیت");
sb.AppendLine("شناسه | نام مشتری | تاریخ | تعداد آیتم | مبلغ نهایی | تخفیف | وضعیت");
foreach (var o in _orders.Where(o => o.Created.HasValue).OrderBy(o => o.Created))
{
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();
@@ -587,14 +593,4 @@
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
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;
}
}
@@ -169,6 +169,42 @@
</CellTemplate>
</PropertyColumn>
<TemplateColumn Title="نوع فعالسازی" Sortable="false">
<CellTemplate>
@if (context.Item.IsNewActivation == null)
{
<span style="color: #bbb;">-</span>
}
else if (context.Item.IsNewActivation == true)
{
<MudChip T="string" Size="Size.Small" Color="Color.Success" Icon="@Icons.Material.Filled.FiberNew">
عضو جدید
</MudChip>
}
else
{
<MudChip T="string" Size="Size.Small" Color="Color.Secondary" Icon="@Icons.Material.Filled.Refresh">
خرید مجدد
</MudChip>
}
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="پکیج" Sortable="false">
<CellTemplate>
@if (!string.IsNullOrEmpty(context.Item.PackageName))
{
<MudChip T="string" Size="Size.Small" Color="Color.Dark">
@context.Item.PackageName
</MudChip>
}
else
{
<span style="color: #bbb;">-</span>
}
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="عملیات" Sortable="false">
<CellTemplate>
<MudButtonGroup Size="Size.Small" Variant="Variant.Text">
@@ -337,7 +373,9 @@
isClubActive = n.IsClubActive,
isActivatedInTargetWeek = n.IsActivatedInTargetWeek,
activationWeekDefinitionId = n.ActivationWeekDefinitionId,
clubActivatedAt = n.ClubActivatedAt?.ToDateTime().ToLocalTime().MiladiToJalali() ?? ""
clubActivatedAt = n.ClubActivatedAt?.ToDateTime().ToLocalTime().MiladiToJalali() ?? "",
isNewActivation = n.IsNewActivation,
packageName = n.PackageName ?? ""
}).ToArray();
var options = new { filterWeek = _activationWeekFilter };
@@ -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">
<MudSelectItem T="int" Value="1">واریز نقدی</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="5">اصلاح خطا</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">
<MudSelectItem T="int?" Value="@(1)">واریز نقدی</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="@(5)">اصلاح خطا</MudSelectItem>
<MudSelectItem T="int?" Value="@(6)">بازگشت وجه</MudSelectItem>
@@ -226,7 +226,7 @@
{
1 => "واریز نقدی",
2 => "شارژ کیف پول اعتباری",
3 => "شارژ پاداش تیمی",
3 => "شارژ پاداش های دریافتی",
4 => "تسویه حساب",
5 => "اصلاح خطا",
6 => "بازگشت وجه",
+172 -51
View File
@@ -1,78 +1,115 @@
@page "/payment/transactions"
@using BackOffice.Common.BaseComponents
@using BackOffice.Pages.AutoComplete
@using CMSMicroservice.Protobuf.Protos.PaymentTransaction
@using Google.Protobuf.WellKnownTypes
@inject PaymentTransactionContract.PaymentTransactionContractClient PaymentTxClient
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
<Filters>
<MudItem xs="12" sm="6" md="4">
<BackOffice.Common.BaseComponents.DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه تاریخی" />
<MudItem xs="12">
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true">
گزارش read-only تراکنش‌های درگاه پرداخت. برای ثبت پرداخت دستی به «پرداخت‌های دستی» مراجعه کنید.
</MudAlert>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudSelect T="int?" Clearable="true" Label="وضعیت پرداخت" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_statusFilter">
<MudSelectItem T="int?" Value="@(0)">در انتظار</MudSelectItem>
<MudSelectItem T="int?" Value="@(1)">موفق</MudSelectItem>
<MudSelectItem T="int?" Value="@(2)">ناموفق</MudSelectItem>
<MudSelectItem T="int?" Value="@(3)">بازگشت وجه</MudSelectItem>
<MudItem xs="12" sm="6" md="3">
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_userId" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudTextField T="string" Label="موبایل" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_mobile" />
</MudItem>
<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>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudSelect T="int?" Clearable="true" Label="نوع تراکنش" Variant="Variant.Outlined"
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>
<DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه تاریخ" />
</MudItem>
</Filters>
<Content>
<MudDataGrid @ref="_dataGrid" T="TransactionModel"
ServerData="LoadData"
Hover="true" Dense="true"
Height="calc(100vh - 240px)">
<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="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>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<PropertyColumn Property="x => x.RefId" Title="شماره مرجع" />
<PropertyColumn Property="x => x.Amount" Title="مبلغ">
<CellTemplate>
@context.Item.Amount.ToString("N0")
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.UserId" Title="کاربر" />
<PropertyColumn Property="x => x.Mobile" Title="موبایل" />
<PropertyColumn Property="x => x.GatewayProvider" Title="درگاه" />
<TemplateColumn Title="مبلغ (تومان)">
<CellTemplate>@context.Item.Amount.ToString("N0")</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.Authority" Title="Authority" />
<PropertyColumn Property="x => x.RefId" Title="RefId" />
<PropertyColumn Property="x => x.CardPan" Title="کارت" />
<TemplateColumn Title="وضعیت">
<CellTemplate>
<MudChip T="string"
Size="Size.Small"
Color="@GetStatusColor(context.Item.PaymentStatus)">
@GetStatusText(context.Item.PaymentStatus)
<MudChip T="string" Size="Size.Small" Color="@(context.Item.PaymentStatus ? Color.Success : Color.Warning)">
@ReportLabels.GetPaymentStatusLabel(context.Item.PaymentStatus)
</MudChip>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="تاریخ پرداخت">
<CellTemplate>
@context.Item.PaymentDate.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>
<PropertyColumn Property="x => x.VerificationStatusCode" Title="کد Verify" />
<PropertyColumn Property="x => x.OrderId" Title="OrderId" />
<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>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">
با فیلتر فعلی موردی یافت نشد — بازه تاریخ یا کاربر را تغییر دهید.
</MudAlert>
</NoRecordsContent>
<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}"
RowsPerPageString="تعداد در صفحه" />
</PagerContent>
@@ -80,3 +117,87 @@
</Content>
</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="3">تحویل به مشتری</MudSelectItem>
<MudSelectItem T="int" Value="4">مرجوع شده</MudSelectItem>
<MudSelectItem T="int" Value="5">لغو شده</MudSelectItem>
<MudSelectItem T="int" Value="6">آماده تحویل در دفتر</MudSelectItem>
</MudSelect>
</MudItem>
@@ -56,6 +58,24 @@
</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)
{
<MudItem xs="12">
@@ -37,6 +37,8 @@ public partial class ChangeOrderStatusDialog
2 => "تحویل پست",
3 => "تحویل به مشتری",
4 => "مرجوع شده",
5 => "لغو شده",
6 => "آماده تحویل در دفتر",
_ => "بدون ارسال / نامشخص"
};
}
@@ -49,6 +51,8 @@ public partial class ChangeOrderStatusDialog
2 => Color.Info,
3 => Color.Success,
4 => Color.Error,
5 => Color.Dark,
6 => Color.Primary,
_ => Color.Default
};
}
@@ -95,6 +95,8 @@
<MudSelectItem T="int" Value="2">تحویل پست</MudSelectItem>
<MudSelectItem T="int" Value="3">تحویل به مشتری</MudSelectItem>
<MudSelectItem T="int" Value="4">مرجوع شده</MudSelectItem>
<MudSelectItem T="int" Value="5">لغو شده</MudSelectItem>
<MudSelectItem T="int" Value="6">آماده تحویل در دفتر</MudSelectItem>
</MudSelect>
</MudText>
<MudTextField T="string"
@@ -123,6 +125,14 @@
{
<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>
</MudPaper>
@@ -162,6 +172,13 @@
</MudStack>
</DialogContent>
<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>
@@ -1,6 +1,8 @@
using BackOffice.Common.Utilities;
using CMSMicroservice.Protobuf.Protos;
using CMSMicroservice.Protobuf.Protos.UserOrder;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor;
namespace BackOffice.Pages.UserOrder.Components;
@@ -9,6 +11,7 @@ public partial class UserOrderDetailsDialog
{
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!;
[Inject] public UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } = default!;
[Inject] private IJSRuntime JsRuntime { get; set; } = default!;
[Parameter] public long OrderId { get; set; }
@@ -56,6 +59,8 @@ public partial class UserOrderDetailsDialog
2 => Color.Info, // InTransit
3 => Color.Success, // Delivered
4 => Color.Error, // Returned
5 => Color.Dark, // Cancelled
6 => Color.Primary, // ReadyForOfficePickup
_ => Color.Default // None / Unknown
};
}
@@ -68,6 +73,8 @@ public partial class UserOrderDetailsDialog
2 => "تحویل پست",
3 => "تحویل به مشتری",
4 => "مرجوع شده",
5 => "لغو شده",
6 => "آماده تحویل در دفتر",
_ => "بدون ارسال / نامشخص"
};
}
@@ -178,5 +185,61 @@ public partial class UserOrderDetailsDialog
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();
}
@@ -46,6 +46,7 @@
<MudSelectItem T="int?" Value="@(3)">تحویل به مشتری</MudSelectItem>
<MudSelectItem T="int?" Value="@(4)">مرجوع شده</MudSelectItem>
<MudSelectItem T="int?" Value="@(5)">لغو شده</MudSelectItem>
<MudSelectItem T="int?" Value="@(6)">آماده تحویل در دفتر</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="3">
@@ -1,4 +1,5 @@
using System.Text;
using BackOffice.Common.Utilities;
using CMSMicroservice.Protobuf.Protos;
using CMSMicroservice.Protobuf.Protos.UserOrder;
using Microsoft.AspNetCore.Components;
@@ -215,7 +216,8 @@ public partial class OrderSalesReports
2 => Color.Info,
3 => Color.Success,
4 => Color.Error,
5 => Color.Error,
5 => Color.Dark,
6 => Color.Primary,
_ => Color.Default
};
}
@@ -229,6 +231,7 @@ public partial class OrderSalesReports
3 => "تحویل به مشتری",
4 => "مرجوع شده",
5 => "لغو شده",
6 => "آماده تحویل در دفتر",
_ => "بدون ارسال / نامشخص"
};
}
@@ -241,27 +244,26 @@ public partial class OrderSalesReports
return;
}
var sb = new StringBuilder();
sb.AppendLine("شناسه,کاربر,نام کاربر,مبلغ,وضعیت پرداخت,وضعیت ارسال,تاریخ پرداخت");
foreach (var o in _orders)
var headers = new[]
{
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 base64 = Convert.ToBase64String(bytes);
var filename = $"store-sales-{DateTime.Now:yyyyMMddHHmmss}.csv";
var rows = _orders.Select(o => new object?[]
{
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);
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
var filename = $"store-sales-{DateTime.Now:yyyyMMddHHmmss}.xls";
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
CsvExportHelper.ToExcelXmlBase64("گزارش فروش", headers, rows));
Snackbar.Add("خروجی Excel آماده شد.", Severity.Success);
}
protected async Task ExportToPdf()
@@ -290,7 +292,7 @@ public partial class OrderSalesReports
foreach (var o in _orders)
{
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();
@@ -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="@(4)">مرجوع شده</MudSelectItem>
<MudSelectItem T="int?" Value="@(5)">لغو شده</MudSelectItem>
<MudSelectItem T="int?" Value="@(6)">آماده تحویل در دفتر</MudSelectItem>
</MudSelect>
</MudItem>
</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.UserOrder;
using BackOffice.Common.BaseComponents;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor;
@@ -213,6 +214,8 @@ public partial class UserOrderMainPage
2 => Color.Info, // InTransit
3 => Color.Success, // Delivered
4 => Color.Error, // Returned
5 => Color.Dark, // Cancelled
6 => Color.Primary, // ReadyForOfficePickup
_ => Color.Default // None / Unknown
};
}
@@ -225,6 +228,8 @@ public partial class UserOrderMainPage
2 => "تحویل پست",
3 => "تحویل به مشتری",
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)
{
var parameters = new DialogParameters
@@ -315,40 +329,31 @@ public partial class UserOrderMainPage
return;
}
var sb = new StringBuilder();
sb.AppendLine("شناسه,کاربر,نام کاربر,مبلغ,وضعیت پرداخت,وضعیت ارسال,روش پرداخت,تاریخ پرداخت");
foreach (var o in result.Models)
var headers = new[]
{
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 base64 = Convert.ToBase64String(bytes);
var filename = $"user-orders-{DateTime.Now:yyyyMMddHHmmss}.csv";
var rows = result.Models.Select(o => new object?[]
{
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);
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
var filename = $"user-orders-{DateTime.Now:yyyyMMddHHmmss}.xls";
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
CsvExportHelper.ToExcelXmlBase64("سفارش‌ها", headers, rows));
Snackbar.Add("خروجی Excel آماده شد.", Severity.Success);
}
catch (Exception ex)
{
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,11 +43,16 @@
<MudText Color="Color.Primary"><strong>@context.Item.Balance.ToString("N0")</strong> تومان</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="پاداش تیمی">
<TemplateColumn Title="پاداش های دریافتی">
<CellTemplate>
<MudText Color="Color.Success">@context.Item.NetworkBalance.ToString("N0") تومان</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="موجودی اعتباری">
<CellTemplate>
<MudText Color="Color.Tertiary">@context.Item.DiscountBalance.ToString("N0") تومان</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="عملیات" Sortable="false">
<CellTemplate>
<MudTooltip Text="تاریخچه تغییرات">
@@ -110,16 +115,26 @@
<MudText Color="Color.Primary">@context.Item.CurrentBalance.ToString("N0")</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="تغییر پاداش تیمی">
<TemplateColumn Title="تغییر پاداش های دریافتی">
<CellTemplate>
<MudText>@context.Item.ChangeNerworkValue.ToString("N0")</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="پاداش تیمی فعلی">
<TemplateColumn Title="پاداش های دریافتی فعلی">
<CellTemplate>
<MudText Color="Color.Success">@context.Item.CurrentNetworkBalance.ToString("N0")</MudText>
</CellTemplate>
</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="شناسه مرجع">
<CellTemplate>
@(context.Item.RefrenceId?.ToString() ?? "—")
@@ -102,6 +102,7 @@ public class DiscountOrderService : IDiscountOrderService
DiscountBalanceUsed = response.DiscountBalanceUsed,
GatewayAmount = response.GatewayAmount,
PaymentCompleted = response.PaymentCompleted,
PaymentStatusValue = (int)response.PaymentStatus,
TransactionId = response.TransactionId,
Status = MapFromProtoStatus(response.DeliveryStatus, response.PaymentCompleted),
TrackingCode = response.TrackingCode,
@@ -200,12 +201,13 @@ public class DiscountOrderService : IDiscountOrderService
return status switch
{
OrderStatus.Pending => ProtoDeliveryStatus.DeliveryPending,
OrderStatus.Paid => ProtoDeliveryStatus.DeliveryPending, // Paid but not processed yet
OrderStatus.Paid => ProtoDeliveryStatus.DeliveryPending,
OrderStatus.Processing => ProtoDeliveryStatus.DeliveryProcessing,
OrderStatus.Shipped => ProtoDeliveryStatus.DeliveryShipped,
OrderStatus.Delivered => ProtoDeliveryStatus.DeliveryDelivered,
OrderStatus.Cancelled => ProtoDeliveryStatus.DeliveryCancelled,
OrderStatus.Returned => ProtoDeliveryStatus.DeliveryCancelled, // Treat as cancelled
OrderStatus.ReadyForOfficePickup => ProtoDeliveryStatus.DeliveryReadyForOffice,
OrderStatus.Returned => ProtoDeliveryStatus.DeliveryReturned,
_ => ProtoDeliveryStatus.DeliveryPending
};
}
@@ -221,6 +223,8 @@ public class DiscountOrderService : IDiscountOrderService
ProtoDeliveryStatus.DeliveryShipped => OrderStatus.Shipped,
ProtoDeliveryStatus.DeliveryDelivered => OrderStatus.Delivered,
ProtoDeliveryStatus.DeliveryCancelled => OrderStatus.Cancelled,
ProtoDeliveryStatus.DeliveryReadyForOffice => OrderStatus.ReadyForOfficePickup,
ProtoDeliveryStatus.DeliveryReturned => OrderStatus.Returned,
_ => OrderStatus.Pending
};
}
@@ -235,6 +239,8 @@ public class DiscountOrderService : IDiscountOrderService
ProtoDeliveryStatus.DeliveryShipped => OrderStatus.Shipped,
ProtoDeliveryStatus.DeliveryDelivered => OrderStatus.Delivered,
ProtoDeliveryStatus.DeliveryCancelled => OrderStatus.Cancelled,
ProtoDeliveryStatus.DeliveryReadyForOffice => OrderStatus.ReadyForOfficePickup,
ProtoDeliveryStatus.DeliveryReturned => OrderStatus.Returned,
_ => OrderStatus.Pending
};
}
@@ -27,13 +27,14 @@ public class OrderFilterDto
// For backward compatibility with UI - maps to proto DeliveryStatus
public enum OrderStatus
{
Pending = 0, // DELIVERY_PENDING
Paid = 1, // Not in proto - handle in service
Processing = 2, // DELIVERY_PROCESSING
Shipped = 3, // DELIVERY_SHIPPED
Delivered = 4, // DELIVERY_DELIVERED
Cancelled = 5, // DELIVERY_CANCELLED
Returned = 6 // Not in proto - handle in service
Pending = 0, // DELIVERY_PENDING
Paid = 1, // Not in proto - handle in service
Processing = 2, // DELIVERY_PROCESSING
Shipped = 3, // DELIVERY_SHIPPED
Delivered = 4, // DELIVERY_DELIVERED
Cancelled = 5, // DELIVERY_CANCELLED
Returned = 6, // DELIVERY_RETURNED
ReadyForOfficePickup = 7 // DELIVERY_READY_FOR_OFFICE
}
public class DiscountOrderDto
@@ -62,11 +63,17 @@ public class DiscountOrderDetailsDto
public long Id { get; set; }
public long UserId { get; set; }
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 long TotalPrice { get; set; }
public long DiscountBalanceUsed { get; set; }
public long GatewayAmount { get; set; }
public long VatAmount { 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 OrderStatus Status { get; set; }
public string? TrackingCode { get; set; }
@@ -96,6 +96,7 @@
// Payment
"payment" => "پرداخت",
"transactions" => "تراکنش‌ها",
"ledger-transactions" => "دفتر تراکنش‌های مالی",
"manual-payments" => "پرداخت دستی",
// Network
+24 -1
View File
@@ -75,7 +75,12 @@
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/club/members"
Icon="@Icons.Material.Filled.Groups">
اعضا و آمار
اعضا و گزارش‌ها
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/club/customer-packages"
Icon="@Icons.Material.Filled.PeopleAlt">
گزارش مشتریان پکیج
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/club/features"
@@ -200,6 +205,24 @@
</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"
Href="/wallets"
Icon="@Icons.Material.Filled.AccountBalanceWallet">
+47 -4
View File
@@ -94,8 +94,9 @@
.admin-node-card .node-meta {
display: flex;
align-items: center;
gap: 6px;
margin-top: 2px;
gap: 4px;
margin-top: 3px;
flex-wrap: wrap;
}
.admin-node-card .level-badge {
@@ -124,8 +125,8 @@
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 6px;
padding-top: 6px;
margin-top: 5px;
padding-top: 5px;
border-top: 1px solid #f0f0f0;
font-size: 10px;
}
@@ -204,6 +205,48 @@
animation: pulse-highlight 1.5s ease-in-out infinite;
}
/* نوع فعالسازی - inline pill */
.admin-node-card .activation-pill {
display: inline-flex;
align-items: center;
padding: 1px 6px;
border-radius: 8px;
font-size: 9px;
font-weight: 600;
white-space: nowrap;
line-height: 1.4;
}
.admin-node-card .new-member-pill {
background: #e8f5e9;
color: #2e7d32;
border: 1px solid #a5d6a7;
}
.admin-node-card .renewal-pill {
background: #ede7f6;
color: #5e35b1;
border: 1px solid #b39ddb;
}
/* نام پکیج badge */
.admin-node-card .package-name-badge {
display: inline-flex;
align-items: center;
height: 14px;
padding: 0 6px;
margin-top: 2px;
border-radius: 7px;
font-size: 9px;
color: #546e7a;
background: #eceff1;
border: 1px solid #b0bec5;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 120px;
}
@keyframes pulse-highlight {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.2); opacity: 0.8; }
+23 -7
View File
@@ -39,8 +39,8 @@ window.AdminOrgChart = {
this.chart = new d3.OrgChart()
.container('#' + containerId)
.data(data)
.nodeWidth((d) => 160)
.nodeHeight((d) => 80)
.nodeWidth((d) => 178)
.nodeHeight((d) => 92)
.childrenMargin((d) => 60)
.compactMarginBetween((d) => 20)
.compactMarginPair((d) => 20)
@@ -83,12 +83,19 @@ window.AdminOrgChart = {
</div>`;
}
// Highlight badge for target week matches
let highlightBadge = '';
if (weekFilterActive && isTargetWeek) {
highlightBadge = '<div class="target-week-highlight">✨</div>';
// نوع فعالسازی - inline pill در meta row
let activationTypePill = '';
if (data.isNewActivation === true) {
activationTypePill = '<span class="activation-pill new-member-pill">عضو جدید</span>';
} else if (data.isNewActivation === false) {
activationTypePill = '<span class="activation-pill renewal-pill">تمدید</span>';
}
// نام پکیج badge
const packageBadge = data.packageName
? `<div class="package-name-badge">${data.packageName}</div>`
: '';
// Avatar - first letter of name
const firstChar = data.userName ? data.userName.charAt(0).toUpperCase() : '?';
@@ -114,6 +121,14 @@ window.AdminOrgChart = {
if (data.activationWeekDefinitionId) {
tooltipLines.push(`📆 هفته: ${data.activationWeekDefinitionId}`);
}
if (data.packageName) {
tooltipLines.push(`📦 پکیج: ${data.packageName}`);
}
if (data.isNewActivation === true) {
tooltipLines.push('🆕 نوع: اولین فعال‌سازی');
} else if (data.isNewActivation === false) {
tooltipLines.push('🔄 نوع: خرید مجدد');
}
if (weekFilterActive) {
tooltipLines.push(isTargetWeek ? '🎯 فعال در هفته انتخابی' : '⚪ خارج از هفته انتخابی');
}
@@ -125,14 +140,15 @@ window.AdminOrgChart = {
data-user-id="${data.userId}"
title="${tooltipText}">
${weekIndicator}
${highlightBadge}
<div class="node-header">
<div class="node-avatar">${firstChar}</div>
<div class="node-main-info">
<div class="node-name">${data.userName || 'کاربر ' + data.userId}</div>
${packageBadge}
<div class="node-meta">
${levelBadge}
${legText ? `<span class="${legClass}">${legText}</span>` : ''}
${activationTypePill}
</div>
</div>
</div>