Compare commits

..

78 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
masoodafar-web 6d8d681046 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 6m29s
2026-04-28 23:30:42 +03:30
masoodafar-web b7d1fe13c7 fix(commission/dashboard): fix PackageId type - use long? directly, add global.json for SDK 9.0
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 12m48s
2026-04-24 16:56:11 +03:30
masoodafar-web 67f57ce99f fix(commission/dashboard): fix TriggerWeeklyCalculationRequest.PackageId assignment - use long? directly
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 12m49s
2026-04-24 16:39:22 +03:30
masoodafar-web c4cbe94ed7 fix(commission/dashboard): fix Int64Value construction - use constructor instead of non-existent FromLong
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 11m29s
2026-04-24 05:20:31 +03:30
masoodafar-web ed24f505d3 chore(proto): update CMSMicroservice.Protobuf to 0.0.193
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 11m30s
2026-04-24 04:20:25 +03:30
masoodafar-web 3c2a0edca8 feat(commission/dashboard): add package selector for per-package calculation
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 11m26s
- Dashboard: added PackageSelect component to filter/trigger calculation per package
- LoadPoolData: now sends PackageId in GetWeeklyCommissionPoolRequest
- TriggerCalculation: now sends PackageId in TriggerWeeklyCalculationRequest
- Pool Details: shows PackageTitle in table
2026-04-24 03:43:26 +03:30
masoodafar-web aed6258fb3 fix: update GW_URL in Dockerfile to correct endpoint
Build and Deploy to Production / build-and-deploy (push) Successful in 15m55s
2026-03-15 13:27:48 +03:30
masoodafar-web e6d1d833ec fix: update GW_URL in backoffice deployment to correct endpoint
Build and Deploy to Production / build-and-deploy (push) Successful in 15m40s
2026-03-15 13:04:13 +03:30
masoodafar-web a8c52db544 fix: resolve MudBlazor v8 breaking changes (MUD0002 warnings)
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 15m20s
- MudListItem: Selected → Class with conditional CSS
- MudTextField: Dense → Margin="Margin.Dense"
- MudSwitch: remove HelperText (merged into Label)
- MudDatePicker: remove T parameter (no longer generic in v8)
2026-03-15 12:00:02 +03:30
masoodafar-web c0c7bb3ac5 fix: resolve MudBlazor v8 breaking changes (MUD0002 warnings)
Build and Deploy to Production / build-and-deploy (push) Successful in 16m3s
- MudListItem: Selected → Class with conditional CSS
- MudTextField: Dense → Margin="Margin.Dense"
- MudSwitch: remove HelperText (merged into Label)
- MudDatePicker: remove T parameter (no longer generic in v8)
2026-03-15 11:58:37 +03:30
masoodafar-web d0fe99492d fix: update proto to 0.0.192 on production (merge conflict chose wrong version) 2026-03-15 05:29:46 +03:30
masoodafar-web d38f50584c fix: resolve version conflict for Foursat.CMSMicroservice.Protobuf package
Build and Deploy to Production / build-and-deploy (push) Failing after 13m13s
2026-03-15 03:21:52 +03:30
masoodafar-web 1c67d7798a Merge branch 'kub-stage' into production 2026-03-15 03:21:44 +03:30
masoodafar-web 2a7bb0592d feat: add K8s deployment manifest with GW_URL and BLAZOR_ENVIRONMENT
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 15m26s
- Create k8s/staging/backoffice-deployment.yaml with env vars:
  GW_URL=https://cms.se.kbs1.ir (injected into appsettings at startup)
  BLAZOR_ENVIRONMENT=Staging
- Update CI/CD to apply K8s manifests on deploy (like CMS does)
- This makes configuration persistent across CI/CD deployments
2026-03-15 03:11:38 +03:30
masoodafar-web 9699833ac9 fix: exclude appsettings from service worker cache
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
appsettings*.json files were cached by the service worker, causing
stale GwUrl (cms.kbs1.ir) to persist in the browser even after the
server files were updated. Now appsettings files are always fetched
fresh from the server, where the entrypoint injects the correct GW_URL.
2026-03-15 03:07:14 +03:30
masoodafar-web b3e6066c90 fix: don't send expired JWT tokens with gRPC requests
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 15m19s
AppTokenProvider now checks token expiry before returning it.
Expired tokens are removed from localStorage and not sent.

This fixes 401 errors on anonymous endpoints (login/OTP) caused by
the gRPC channel attaching an old expired Bearer token to every request.
CMS validates the token even on unauthenticated endpoints and rejects
expired ones with 401.
2026-03-15 01:39:27 +03:30
masoodafar-web 148b8e4011 fix: inject GwUrl into appsettings at container startup
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 15m1s
The blazor-environment header is stripped by the Blazor WASM service
worker cache, causing Blazor to fall back to Production environment
and load appsettings.Production.json (GwUrl=https://cms.kbs1.ir).

Fix: use sed in docker-entrypoint.sh to replace GwUrl in ALL
appsettings*.json files at container startup with the GW_URL env var.
Default GW_URL=https://cms.se.kbs1.ir for staging deployment.
2026-03-15 00:42:38 +03:30
masoodafar-web 1ef5edf939 fix: add blazor-environment header to nginx for staging config
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 14m30s
BackOffice WASM app defaulted to Production environment because
nginx didn't set the blazor-environment response header. This caused
the app to load appsettings.Production.json (GwUrl=https://cms.kbs1.ir)
instead of appsettings.Staging.json (GwUrl=https://cms.se.kbs1.ir),
resulting in 401 gRPC errors on the login page.

- Add docker-entrypoint.sh that generates nginx config with blazor-environment header
- Default BLAZOR_ENVIRONMENT=Staging (configurable via K8s env var)
- This ensures the WASM app loads the correct staging configuration
2026-03-15 00:19:04 +03:30
masoodafar-web 76dbee7d41 ci: retry - pushed missing 9.0.12 SDK packages to Nexus
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 14m27s
2026-03-12 07:01:46 +03:30
masoodafar-web 148a9e0493 fix: remove local nupkg source from NuGet.config (breaks Docker builds)
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 11m28s
2026-03-12 02:28:07 +03:30
masoodafar-web 851a1bbc4f ci: retry build - all NuGet packages pushed to Nexus
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 1m21s
2026-03-12 01:14:19 +03:30
masoodafar-web aa1d21a616 Revert "fix: bypass broken Nexus, use nuget.org + local proto build"
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 1m28s
This reverts commit 7147e9930f.
2026-03-12 00:06:24 +03:30
masoodafar-web 7147e9930f fix: bypass broken Nexus, use nuget.org + local proto build
- NuGet.config: nuget.org for standard packages, local nupkg for proto
- Dockerfile: COPY nupkg/ before restore
- CI: build proto from CMS source before Docker build
2026-03-12 00:03:49 +03:30
masoodafar-web 85208da1b5 feat: manual payment - replace amount with package selector
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 1m39s
- Remove manual amount input field
- Add package selector dropdown (shows name + price)
- Pre-select base package, show price in alert
- Validate package selection before submit
- Send package_id to CMS, proto NuGet 0.0.192
2026-03-11 23:44:36 +03:30
masoodafar-web ea7414dc7f fix: update Foursat.CMSMicroservice.Protobuf package version to 0.0.191
Build and Deploy to Production / build-and-deploy (push) Successful in 8m40s
2026-02-28 05:07:46 +03:30
masoodafar-web a450d9a001 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Failing after 2m41s
2026-02-28 04:46:38 +03:30
masoodafar-web 92c992249e F4: ستون پکیج در CSV، اصلاح rename UserWalletChangeLog→UserWalletHistory
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 3m8s
- WithdrawalRequests: اضافه شدن PackageName به مدل و CSV
- ManualPayments: اضافه شدن ستون پکیج به CSV
- ConfigureService: اصلاح using و DI برای UserWalletHistory
- WalletManagementPage: اصلاح تمام رفرنس‌های UserWalletChangeLog
- NuGet: v0.0.189
2026-02-27 08:58:44 +03:30
masoodafar-web 6939780cd1 feat(Q28): UI Guidance alerts across BackOffice (G8-G13)
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m38s
Q28: Added informational/warning alerts to 6 BO pages:
G8: PackageCRUD — price change impact warning
G9: ClubFeatures — feature matrix explanation
G10: ManualPayments — package price matching warning
G11: Commission Dashboard+Payouts — independent pool info
G13: ClubMembers — package filter guidance
2026-02-27 05:41:03 +03:30
masoodafar-web e6cf90e3fa fix: ManualPaymentDialog — remove hardcoded 56M, enable Amount field
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m44s
- Default amount set to 0 (admin must enter)
- Amount field now editable with Required validation
2026-02-27 02:45:11 +03:30
masoodafar-web e0203541bf feat(F2+T4.13): ChangeNetworkParent dialog + PackageFeature checkbox matrix
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m32s
F2: ChangeNetworkParent UI
- Add ChangeParentDialog with parent ID, leg, reason fields
- Add 'تغییر والد' button to UserNetworkInfo operations

T4.13: PackageFeature checkbox matrix
- Update NuGet to v0.0.188
- Add ClubFeature checkbox list to CreateDialog and UpdateDialog
- Load features via ConfigClient.GetClubFeaturesAsync
- Sync selected FeatureIds to proto request on save
2026-02-26 22:00:20 +03:30
masoodafar-web 8be98ae33e feat(commission): add per-package filtering to commission pages
- New PackageSelect component: reusable dropdown for package filtering
- UserPayouts: add package filter + package column with MudChip
- BalancesReport: add package filter + package column with MudChip
- Update NuGet Foursat.CMSMicroservice.Protobuf to v0.0.187
2026-02-26 21:08:16 +03:30
masoodafar-web c96377a674 Phase 8d: NuGet bump 0.0.185 → 0.0.186 (proto cleanup) 2026-02-26 18:00:47 +03:30
masoodafar-web 89f5241680 Phase 8b: Expand Package CRUD with all package-based fields
- CreateDialog: Add 12 new fields (SortOrder, ActivationFee, DiscountMultiplier,
  MagicWalletMultiplier, MagicWalletMaxDeposit, MagicWalletMaxCredit,
  MaxBalancesPerLeg, MaxNetworkLevel, IsActive, IsBasePackage,
  SupportsDirectPurchase, SupportsDayaPurchase)
- UpdateDialog: Same 12 new fields added
- PackageMainPage grid: Add Price, SortOrder, IsActive, IsBasePackage columns
- Dialog size: Small → Medium for expanded forms
- CreateNew defaults: IsActive=true, DiscountMultiplier=2.0, etc.
- Fix HasPurchasedGoldenPackage → HasPurchasedPackage in UserNetworkInfo
- NuGet: 0.0.184 → 0.0.185 + local feed source
2026-02-26 17:44:53 +03:30
masoodafar-web f1b0085181 Cleanup: Rename 'پکیج طلایی' → 'خرید پکیج' in UserNetworkInfo 2026-02-26 02:03:26 +03:30
masoodafar-web 60c91438ec fix: rename wallet labels — اصلی، اعتباری، پاداش تیمی
Build and Deploy to Production / build-and-deploy (push) Successful in 11m53s
- شبکه → پاداش تیمی (WalletManagement, ManualPayments)
- تخفیف → اعتباری (ManualPayments, OrderDetails, DiscountOrders)
- طلایی → پاداش تیمی (WithdrawalReports)

Files: WalletManagementPage, ManualPayments, ManualPaymentDialog,
       WithdrawalReports, DiscountOrdersMainPage, OrderDetailsDialog
2026-02-23 23:26:59 +03:30
masoodafar-web 172101d0ab fix: rename wallet labels — اصلی، اعتباری، پاداش تیمی
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 9m58s
- شبکه → پاداش تیمی (WalletManagement, ManualPayments)
- تخفیف → اعتباری (ManualPayments, OrderDetails, DiscountOrders)
- طلایی → پاداش تیمی (WithdrawalReports)

Files: WalletManagementPage, ManualPayments, ManualPaymentDialog,
       WithdrawalReports, DiscountOrdersMainPage, OrderDetailsDialog
2026-02-23 23:24:41 +03:30
masoodafar-web 1d8f34ae6c chore: prevent appsettings.Production.json from merging — keep ours strategy
Build and Deploy to Production / build-and-deploy (push) Successful in 8m7s
2026-02-23 00:03:28 +03:30
masoodafar-web 76b7211a5c chore: prevent appsettings.Production.json from merging — keep ours strategy
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 7m43s
2026-02-23 00:03:14 +03:30
masoodafar-web fe72774d08 feat: ActivateClubDialog — dropdown پکیج + حذف مدت زمان + ForceActivation
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
- جایگزینی MudNumericField پکیج با MudSelect dropdown (لود از سرور)
- حذف فیلد مدت زمان (DurationMonths) — عضویت باشگاه زمانی نیست
- ارسال ForceActivation=true در request (bypass order validation)
- اضافه کردن PackageContract client injection
- Proto NuGet بروزرسانی به 0.0.184
2026-02-22 23:51:55 +03:30
masoodafar-web bdea2e8aef Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 7m46s
2026-02-22 22:23:35 +03:30
masoodafar-web 0386a2b1aa feat: club UserAutoComplete + member filter + wallet user name display
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 7m59s
- ActivateClubDialog: replaced numeric UserId input with UserAutoComplete
- ClubMembers: added UserAutoComplete filter in toolbar, wired to proto user_id
- WalletManagementPage: display user name + ID instead of just UserId
- Updated proto NuGet to 0.0.183 (user_name field)
2026-02-22 22:03:27 +03:30
masoodafar-web 8f29fbaf71 fix: replace ریال with تومان across all 30 admin UI files
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 7m55s
2026-02-22 21:30:47 +03:30
masoodafar-web 7a62e3caac docs: move docs to totalDoc, replace with placeholders
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 10m16s
2026-02-18 21:44:14 +03:30
masoodafar-web 4eaca9bc4c docs: update changelog with phase 16 (site pages) and phase 17 (inventory UX)
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
2026-02-18 01:05:51 +03:30
99 changed files with 4481 additions and 2821 deletions
+2
View File
@@ -0,0 +1,2 @@
# Keep production config from current branch during merges — never overwrite
src/BackOffice/wwwroot/appsettings.Production.json merge=ours
+81
View File
@@ -0,0 +1,81 @@
name: Build and Deploy to Kubernetes
on:
push:
branches:
- kub-stage
env:
REGISTRY: 194.5.195.53:30080
IMAGE_NAME: admin/backoffice
K8S_SERVER: 194.5.195.53
jobs:
build-and-deploy:
runs-on: ubuntu-latest
container:
image: 194.5.195.53:32082/docker-sshpass:latest
options: --privileged
steps:
- name: Start Docker daemon
run: |
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'DAEMON'
{
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
}
DAEMON
echo "🚀 Starting Docker daemon..."
dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs &
# Wait up to 3 minutes for Docker to be ready
for i in $(seq 1 90); do
if docker info >/dev/null 2>&1; then
echo "✅ Docker daemon is ready (attempt $i)"
docker version
break
else
echo "⏳ Waiting for Docker daemon... (attempt $i/90)"
sleep 2
fi
done
# Final check
if ! docker info >/dev/null 2>&1; then
echo "❌ Docker daemon failed to start after 3 minutes"
exit 1
fi
- name: Checkout code
run: |
git clone --depth 1 --branch kub-stage http://gitea-svc:3000/admin/BackOffice.git .
- name: Login to Docker registries
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 194.5.195.53:32082 -u admin --password-stdin
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
- name: Build Docker Image
run: |
cd src
DOCKER_BUILDKIT=0 docker build --network host -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest -f BackOffice/Dockerfile .
- name: Push to Registry
run: |
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- name: Deploy to Kubernetes
run: |
export SSHPASS="${{ secrets.SERVER_PASSWORD }}"
# Copy K8s manifests to server
sshpass -e scp -o StrictHostKeyChecking=no k8s/staging/backoffice-deployment.yaml root@${{ env.K8S_SERVER }}:/tmp/backoffice-deployment.yaml
# Apply manifests and restart
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
kubectl apply -f /tmp/backoffice-deployment.yaml &&
kubectl rollout restart deployment/backoffice &&
kubectl rollout status deployment/backoffice --timeout=180s &&
rm -f /tmp/backoffice-deployment.yaml
"
echo "✅ Deployed!"
+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
+2 -1008
View File
File diff suppressed because it is too large Load Diff
+2 -791
View File
@@ -1,792 +1,3 @@
# 📝 BackOffice — Changelog # Docs moved to totalDoc
**آخرین بروزرسانی:** اسفند ۱۴۰۴ (February 17, 2026) See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation.
---
## 🏷 فاز ۱۵ — Production Deploy + فیکس CI/CD Cross-Deploy (اسفند ۱۴۰۴)
> **هدف:** دیپلوی به سرور Production و رفع باگ cross-deploy
### ۱۵.۱ — Merge به برنچ production ✅
- **کامیت:** `4454b8d` — merge `kub-stage``production`
- **ریموت:** `kub-stage` (Gitea)
### ۱۵.۲ — فیکس باگ Cross-Deploy ✅
- **مشکل:** Gitea Act Runner همه workflowهای `.gitea/workflows/` را اجرا می‌کرد — بدون توجه به `branches:` filter
- **عارضه:** push به `production` باعث دیپلوی همزمان به staging و production می‌شد
- **رفع:** حذف فایل‌های workflow staging از برنچ production
- **فایل حذف شده:** `.gitea/workflows/deploy.yml` (staging) از برنچ production
### ۱۵.۳ — بروزرسانی Proto به v0.0.179 ✅
- **تغییر:** `Foursat.CMSMicroservice.Protobuf``0.0.179`
- **فایل:** `BackOffice.csproj`
### ۱۵.۴ — فیکس K8S_SERVER در prod-deploy.yml ✅
- **مشکل:** `K8S_SERVER` هنوز `194.5.195.53` (staging) بود
- **رفع:** تغییر به `45.149.79.127` (production)
- **فیکس اضافی:** `kubectl rollout restart``kubectl set image` (تا ایمیج SHA-tagged واقعاً set بشه)
### ۱۵.۵ — ساخت appsettings.Production.json ✅
- **مشکل:** فایل `appsettings.Production.json` وجود نداشت → `GwUrl` fallback به `localhost`
- **رفع:** ساخت `wwwroot/appsettings.Production.json` با `GwUrl=https://cms.kbs1.ir`
### ۱۵.۶ — فیکس nginx image path در Dockerfile (production branch) ✅
- **مشکل:** Dockerfile روی برنچ production از `library/nginx:alpine` استفاده می‌کرد — وجود نداشت
- **ارور:** `manifest for 194.5.195.53:32082/library/nginx:alpine not found`
- **رفع:** تغییر به `194.5.195.53:32082/nginx:alpine`
- **کامیت:** `743403e` (مستقیم روی برنچ production)
---
## 🏷 فاز ۱۴ — فیکس Docker Build + Deployment (بهمن ۱۴۰۴)
> **هدف:** رفع مشکلات Docker build و CI/CD pipeline
### ۱۴.۱ — تغییر به NuGet Package ✅
- **مشکل:** `ProjectReference` به `../../../CMS/src/CMSMicroservice.Protobuf/` خارج از Docker build context بود
- **رفع:** تغییر به `PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.178"`
- **فایل:** `BackOffice.csproj`
### ۱۴.۲ — حذف COPY libs/ از Dockerfile ✅
- **مشکل:** `COPY ["libs/", "libs/"]` فیل می‌شد — `libs/` خارج از build context
- **رفع:** حذف خط — دیگه نیازی به DLLهای BFF نیست
- **فایل:** `Dockerfile`
### ۱۴.۳ — تغییر nginx:alpine به رجیستری لوکال ✅
- **مشکل:** `FROM nginx:alpine` از Docker Hub → TLS handshake timeout
- **رفع:** `FROM 194.5.195.53:32082/nginx:alpine`
- **فایل:** `Dockerfile`
### ۱۴.۴ — اضافه شدن SERVER_PASSWORD secret ✅
- **مشکل:** مرحله Deploy to Kubernetes با `Permission denied` فیل می‌شد
- **رفع:** سکرت `SERVER_PASSWORD` به ریپو Gitea اضافه شد (BackOffice + FrontOffice)
---
## 🏷 فاز ۱۳ — بهبودهای UI بک‌آفیس (بهمن ۱۴۰۴)
> **هدف:** رفع مشکلات بصری، بهبود UX فرم‌ها و ناوبری
### ۱۳.۱ — فیکس کامپوننت Image (پیشوند `/uploads/`) ✅
- **مشکل:** تصاویر آپلود‌شده (بلاگ، صفحات سایت، سکشن‌ها) در بک‌آفیس نمایش داده نمی‌شدند — مسیر `GwUrl + Src` با مسیر `UploadsController` هماهنگ نبود
- **رفع:** اضافه شدن `/uploads/` بین `FileBaseUrl` و مسیر نسبی تصویر
- **فایل:** `BackOffice/Common/BaseComponents/Image.razor.cs`
### ۱۳.۲ — فیکس فیلتر هفته در گزارش‌های هفتگی ✅
- **مشکل:** فیلتر هفته از `MudTextField` (ورودی متنی) استفاده می‌کرد — کاربر باید عدد حدس می‌زد
- **رفع:** جایگزینی با `WeekNumberPicker` — انتخاب هفته از لیست با بایندینگ `WeekDefinitionId`
- **فایل‌ها:**
- `CMS/src/CMSMicroservice.Protobuf/Protos/commission.proto` — تغییر فیلدها از `StringValue` به `Int64Value`
- `CMS/.../CommissionCQ/Queries/GetAllWeeklyPools/GetAllWeeklyPoolsQuery.cs` + `Handler.cs`
- `BackOffice/Pages/Commission/WeeklyReports.razor`
### ۱۳.۳ — فیکس دانلود PDF قراردادها ✅
- **مشکل:** فیلد `SignedPdfFile` محتوای HTML کامل دارد (نه URL فایل PDF) — دکمه دانلود کار نمی‌کرد
- **رفع:** تابع JS جدید `jsHtmlToPdf()` برای باز کردن HTML در پنجره جدید و چاپ/ذخیره به‌عنوان PDF
- **فایل‌ها:**
- `BackOffice/wwwroot/js/main.js` — تابع `jsHtmlToPdf`
- `BackOffice/Pages/Contract/UserContractPage.razor`
- `BackOffice/Pages/Contract/ContractDetailsDialog.razor`
### ۱۳.۴ — یکسان‌سازی ارتفاع ویرایشگر HTML ✅
- **مشکل:** ارتفاع `MudHtmlEditor` در صفحات مختلف متفاوت بود — برخی خیلی کوتاه
- **رفع:** اضافه شدن `MinHeight="300px"` به تمام ۷ ویرایشگر HTML
- **فایل‌ها:** `BlogPostEditDialog`, `SitePageSectionEditDialog`, `DiscountShop/ProductFormDialog`, `Package/CreateDialog`, `Package/UpdateDialog`, `Products/CreateDialog`, `Products/UpdateDialog`
### ۱۳.۵ — تولید خودکار اسلاگ بلاگ ✅
- **مشکل:** کاربر باید اسلاگ URL را دستی وارد می‌کرد — غیرضروری و مستعد خطا
- **رفع:** حذف فیلد اسلاگ از فرم، تولید خودکار `post-{GUID}` در بک‌اند (حداکثر ۲۰ کاراکتر)
- **فایل‌ها:**
- `CMS/.../BlogPostCQ/Commands/CreateBlogPost/CreateBlogPostCommandHandler.cs`
- `CMS/.../BlogPostCQ/Commands/UpdateBlogPost/UpdateBlogPostCommandHandler.cs`
- `BackOffice/Pages/Blog/Components/BlogPostEditDialog.razor`
### ۱۳.۶ — انتقال تگ‌ها به منوی جداگانه ✅
- **مشکل:** تگ‌ها فقط از تب سوم بلاگ قابل دسترسی بودند — ولی تگ‌ها بین محصولات و بلاگ مشترکند
- **رفع:** اضافه شدن مسیر `/tags` به `TagManagementPage`، لینک جدید «مدیریت تگ‌ها» در منوی ناوبری، حذف تب تگ‌ها از `BlogHub`
- **فایل‌ها:**
- `BackOffice/Pages/Tag/TagManagementPage.razor`
- `BackOffice/Shared/NavMenu.razor`
- `BackOffice/Pages/Blog/BlogHub.razor`
### ۱۳.۷ — تولید خودکار کلید سکشن ✅
- **مشکل:** کاربر باید کلید سکشن صفحه سایت را دستی وارد می‌کرد (مثلاً `about-us-section`)
- **رفع:** فیلد اختیاری شد، تولید خودکار `section-{GUID}` (حداکثر ۲۰ کاراکتر) در هندلر CMS
- **فایل‌ها:**
- `CMS/.../SitePageCQ/Commands/CreateSitePageSection/CreateSitePageSectionCommandHandler.cs`
- `CMS/.../SitePageCQ/Commands/CreateSitePageSection/CreateSitePageSectionCommandValidator.cs`
- `BackOffice/Pages/Content/Components/SitePageSectionEditDialog.razor`
---
## 🏷 فاز ۱۲ — فیکس فلوی ثبت‌نام / ورود FrontOffice (بهمن ۱۴۰۴)
> **هدف:** رفع ۳ باگ بحرانی در فلوی ثبت‌نام و ورود FrontOffice
> **مستند کامل:** [`totalDoc/cms/REGISTRATION-FLOW-FIXES.md`](../../totalDoc/cms/REGISTRATION-FLOW-FIXES.md)
### ۱۲.۱ — رفع ایجاد کاربر جدید در VerifyOtpToken 🔴 ✅
- **مشکل:** `UserCQ/VerifyOtpTokenCommandHandler` کاربران جدید ایجاد نمی‌کرد — فقط خطای «کاربر یافت نشد»
- **رفع:** اضافه شدن لاجیک کامل: اعتبارسنجی کد معرف، بررسی ظرفیت درخت باینری (حداکثر ۲ فرزند)، تعیین شاخه (چپ/راست)، ایجاد User + UserRole + UserWallet، رویدادهای دامنه
- **فایل:** `CMS/src/CMSMicroservice.Application/UserCQ/Commands/VerifyOtpToken/VerifyOtpTokenCommandHandler.cs`
### ۱۲.۲ — رفع جستجوی موبایل در AcceptContract 🔴 ✅
- **مشکل:** `_currentUserService.Username` = `"{FirstName} {LastName}"` — نه شماره موبایل! در نتیجه OTP و کاربر هیچوقت پیدا نمی‌شد
- **رفع:** ابتدا کاربر از `UserId` پیدا شود، سپس `user.Mobile` برای جستجوی OTP استفاده شود
- **فایل:** `CMS/src/CMSMicroservice.Application/UserCQ/Commands/AcceptContract/AcceptContractCommandHandler.cs`
### ۱۲.۳ — رفع `IsCompleteRegister()` در FrontOffice 🟡 ✅
- **مشکل:** `InitUserAuthInfo().GetAwaiter()` بدون `.GetResult()` — عملیات async اجرا نمی‌شود، منوی کناری وضعیت نادرست
- **رفع:** تغییر به `InitUserAuthInfo().GetAwaiter().GetResult()`
- **فایل:** `FrontOffice/src/FrontOffice.Main/Utilities/AuthService.cs`
---
## 🏷 فاز ۱۱ — رفع خطای صفحات + سرویس عمومی تصاویر (بهمن ۱۴۰۴)
> **هدف:** رفع خطای ۳ صفحه BackOffice + ایجاد اندپوینت HTTP عمومی برای سرو تصاویر
### ۱۱.۱ — سرویس عمومی سرو تصاویر (CMS) ✅
- **`UploadsController`** — اندپوینت `GET /uploads/{path}` برای سرو مستقیم فایل‌ها
- `[AllowAnonymous]` — بدون نیاز به احراز هویت
- کش مرورگر ۲۴ ساعته (`ResponseCache`)
- پشتیبانی از Range Requests
- **FMS Fallback:** اگر فایل محلی نباشد، از `dl.afrino.co` دانلود و کش می‌شود
- `IHttpClientFactory` با named client `"FMS"` (timeout: 30s)
- **فایل‌ها:** `CMS/WebApi/Controllers/UploadsController.cs`, `CMS/WebApi/Program.cs`
### ۱۱.۲ — رفع صفحه «صفحات سایت» ✅
- اضافه شدن **دکمه «افزودن صفحه جدید»** به بالای صفحه
- اضافه شدن **دکمه «حذف»** (آیکون قرمز) برای هر ردیف
- ایجاد **`SitePageCreateDialog`** — دیالوگ ایجاد صفحه جدید با فیلدها:
- کلید صفحه (PageKey)، عنوان، توضیحات متا، عنوان هیرو، زیرعنوان هیرو، تصویر هیرو، وضعیت
- Proto: اضافه شدن `CreateSitePage` و `DeleteSitePage` RPC
- CQRS: `CreateSitePageCommand/Handler` و `DeleteSitePageCommand/Handler`
- **فایل‌ها:**
- `sitepage.proto` — 2 RPC جدید + پیام‌های Request/Response
- `SitePageCQ/Commands/CreateSitePage/` — Command + Handler
- `SitePageCQ/Commands/DeleteSitePage/` — Command + Handler
- `CMS/WebApi/Services/SitePageService.cs` — 2 override جدید
- `BackOffice/Services/Content/ISitePageService.cs``CreateAsync` + `DeleteAsync` + `SitePageCreateDto`
- `BackOffice/Services/Content/SitePageService.cs` — پیاده‌سازی Create/Delete
- `BackOffice/Pages/Content/SitePageManagementPage.razor` — دکمه‌های Add/Delete
- `BackOffice/Pages/Content/SitePageManagementPage.razor.cs` — متدهای CreatePage/DeletePage
- `BackOffice/Pages/Content/Components/SitePageCreateDialog.razor` + `.razor.cs`
### ۱۱.۳ — رفع صفحه «قراردادها» (Contracts) ✅
- **مشکل:** صفحه `/contracts` اصلاً بارگذاری نمی‌شد — crash در DI
- **علت:** `UserContractContract.UserContractContractClient` در DI ثبت نشده بود
- **رفع:** اضافه شدن `using CMSMicroservice.Protobuf.Protos.UserContract` و ثبت gRPC client
- **فایل:** `BackOffice/Common/Configure/ConfigureService.cs`
### ۱۱.۴ — رفع صفحه «کیف پول کاربران» (Wallets) ✅
- **مشکل:** صفحه `/wallets` اصلاً بارگذاری نمی‌شد — crash در DI
- **علت:** دو سرویس ثبت نشده بود:
- `UserWalletContract.UserWalletContractClient`
- `UserWalletChangeLogContract.UserWalletChangeLogContractClient`
- **رفع:** اضافه شدن using‌ها و ثبت هر دو gRPC client
- **فایل:** `BackOffice/Common/Configure/ConfigureService.cs`
---
## 🏷 فاز ۱۰ — بهبود مدیریت محتوا: بلاگ + صفحات سایت (بهمن ۱۴۰۴)
> **هدف:** فعال‌سازی آپلود تصویر باینری و ادیتور HTML در صفحات بلاگ و صفحات سایت
### ۱۰.۱ — بهبود ادیتور پست بلاگ ✅
- **`MudTextField Lines="10"`** → **`MudHtmlEditor`** برای ویرایش محتوای HTML
- **فیلد متنی مسیر تصویر** → **`MudFileUpload`** با پیش‌نمایش تصویر (`Image` component)
- حذف فیلد `FeaturedImageThumbnailPath` (تولید خودکار توسط بک‌اند)
- اضافه شدن `MudForm` با اعتبارسنجی + حالت بارگذاری (`_loading`)
- مدیریت خطا: try-catch + Snackbar
- **فایل‌ها:** `Pages/Blog/Components/BlogPostEditDialog.razor` + `.razor.cs`
### ۱۰.۲ — بهبود ادیتور صفحه سایت ✅
- **فیلد متنی Hero Image** → **`MudFileUpload`** با پیش‌نمایش تصویر
- اضافه شدن `MudForm` + حالت آپلود + مدیریت خطا
- **فایل‌ها:** `Pages/Content/Components/SitePageEditDialog.razor` + `.razor.cs`
### ۱۰.۳ — بهبود ادیتور بخش صفحه سایت (Section) ✅
- **`MudTextField Lines="8"`** → **`MudHtmlEditor`** برای ویرایش محتوای HTML بخش‌ها
- **فیلدهای متنی تصویر/thumbnail** → **`MudFileUpload`** با پیش‌نمایش
- اضافه شدن `MudForm` + حالت آپلود + مدیریت خطا
- **فایل‌ها:** `Pages/Content/Components/SitePageSectionEditDialog.razor` + `.razor.cs`
### ۱۰.۴ — تغییرات Proto ✅
| Proto | تغییر |
|-------|-------|
| `blogpost.proto` | اضافه شدن `BlogImageFileModel` + فیلد `image_file` در Create/Update |
| `sitepage.proto` | اضافه شدن `SitePageImageFileModel` + فیلد `image_file` در Update/CreateSection/UpdateSection |
### ۱۰.۵ — تغییرات CQRS (بک‌اند CMS) ✅
- **Commands:** اضافه شدن `ImageFileBytes`, `ImageMime`, `ImageFileName` به:
- `CreateBlogPostCommand`, `UpdateBlogPostCommand`
- `UpdateSitePageCommand`, `CreateSitePageSectionCommand`, `UpdateSitePageSectionCommand`
- **Handlers:** تزریق `IFileManager` + فراخوانی `UploadImageAsync`:
- BlogPost → `Images/BlogPosts/`
- SitePage → `Images/SitePages/`
- SitePageSection → `Images/SitePageSections/`
### ۱۰.۶ — تغییرات سرویس‌های BackOffice ✅
- `BlogPostService.cs` — ارسال `BlogImageFileModel` با `ByteString.CopyFrom(dto.ImageFile)`
- `SitePageService.cs` — ارسال `SitePageImageFileModel` در Update/CreateSection/UpdateSection
- **DTOs:** اضافه شدن `ImageFile`, `ImageMime`, `ImageFileName` به `BlogPostEditDto`, `SitePageEditDto`, `SitePageSectionEditDto`
### خلاصه آمار فاز ۱۰
| معیار | تعداد |
|---|---|
| فایل‌های proto تغییریافته | 2 |
| Command‌های بروزشده | 5 |
| Handler‌های بروزشده | 5 |
| سرویس‌های gRPC تغییریافته | 2 (BlogPostService + SitePageService) |
| دیالوگ‌های UI بازنویسی‌شده | 3 (BlogPost, SitePage, SitePageSection) |
| سرویس‌های BackOffice بروزشده | 2 |
| DTO‌های بروزشده | 3 |
| Build | 0 Error ✅ (هر ۳ پروژه) |
---
## 🏷 فاز ۹ — معماری مدیریت فایل + رفع سفارشات + مهاجرت DB (بهمن ۱۴۰۴)
> **مستند کامل معماری فایل:** [`totalDoc/cms/FILE-MANAGEMENT-ARCHITECTURE.md`](../../totalDoc/cms/FILE-MANAGEMENT-ARCHITECTURE.md)
### ۹.۱ — حذف کد مرده FMS ✅
- حذف ۳ فایل غیرقابل استفاده مرتبط با FMS خارجی (`https://dl.afrino.co`):
| فایل حذف‌شده | شرح |
|-------------|------|
| `Infrastructure/Services/FmsFileManager.cs` | پیاده‌سازی HTTP upload به FMS |
| `Application/Common/FileManager/FileManagementService.cs` | سرویس قدیمی مدیریت فایل |
| `Application/Common/FileManager/IFileManagementService.cs` | اینترفیس قدیمی |
### ۹.۲ — رفع صفحه سفارشات (Orders) ✅
**مشکل ۱ — صفحه خالی (بدون پیغام خطا)**
- **علت:** `ServerReload` بدون try-catch — exception‌های gRPC ساکت می‌شدند
- **رفع:** اضافه شدن try-catch + نمایش خطا با Snackbar
- **فایل:** `Pages/UserOrder/UserOrderMainPage.razor.cs`
**مشکل ۲ — Shadow FK تکراری در OrderVAT**
- **علت:** `.WithOne()` بدون inverse navigation → EF Core یک shadow FK اضافی می‌ساخت
- **رفع:** `.WithOne()``.WithOne(x => x.OrderVAT)`
- **فایل:** `Persistence/Configurations/OrderVATConfiguration.cs`
**مشکل ۳ — فیلترهای oneof همیشه فعال**
- **علت:** فیلدهای proto `oneof` مقدار پیش‌فرض enum دارند (هیچ‌وقت null نیستند)، پس `PaymentStatus != null` همیشه true بود
- **رفع:** `PaymentStatus != null``HasPaymentStatus == true` (و مشابه برای DeliveryStatus و PaymentMethod)
- **فایل‌ها:** `UserOrderService.cs` — هر دو متد `GetAllUserOrderByFilter` و `GetCustomerOrders`
### ۹.۳ — مهاجرت DB: حذف محدودیت طول ستون‌های تصویر ✅
- **علت:** data URI‌های base64 بزرگ‌تر از `nvarchar(500)` بودند → truncation خاموش
- **رفع:** حذف `HasMaxLength(500)` از تمام ستون‌های image path
- **Migration:** `RemoveImagePathMaxLength`
| Entity | ستون‌ها |
|--------|---------|
| Product | `ImagePath`, `ImageThumbnailPath` |
| DiscountProduct | `ImagePath`, `ImageThumbnailPath` |
| Category | `ImagePath` |
| DiscountCategory | `ImagePath` |
| BlogPost | `FeaturedImagePath`, `FeaturedImageThumbnailPath` |
| SitePage | `HeroImagePath` |
| SitePageSection | `ImagePath`, `ImageThumbnailPath` |
### ۹.۴ — بازنویسی کامل مدیریت فایل: ذخیره دیسکی ✅
**معماری قبلی:** ذخیره data URI مستقیم در DB (ناکارآمد + حجم بالا)
**معماری جدید:**
1. فایل‌ها در دیسک ذخیره می‌شوند: `Uploads/Images/{folder}/{guid}.jpg`
2. مسیر نسبی در DB ذخیره می‌شود
3. `ImagePathResolverInterceptor` هنگام serve تبدیل به base64 می‌کند
**فایل‌های اصلی:**
| فایل | شرح |
|------|------|
| `Application/Common/FileManager/IFileManager.cs` | اینترفیس: Upload, UploadImage, Delete, ResolveImageUrl |
| `Infrastructure/Services/LocalFileManager.cs` | پیاده‌سازی: ImageSharp, main 1200×1200 + thumb 300×300, JPEG Q75 |
| `WebApi/Interceptors/ImagePathResolverInterceptor.cs` | gRPC Interceptor: walk بازگشتی response → تبدیل path به data URI |
**تنظیمات:**
- `FileStorage:UploadPath` → پوشه ریشه آپلود (پیش‌فرض: `AppContext.BaseDirectory/Uploads`)
- gRPC MaxReceiveMessageSize: 50MB
### ۹.۵ — پاکسازی LoggingBehaviour ✅
- **قبل:** فیلدهای باینری (byte[] تصاویر) کامل در لاگ چاپ می‌شدند → لاگ‌های چند مگابایتی
- **بعد:**
- فرمت: `JsonFormatter.Default.Format()` به جای `{@Request}`
- Regex: حذف فیلدهای باینری (`File`, `ImageFile`, `image_file`, `file`)
- محدودیت: حداکثر 2000 کاراکتر
- **فایل:** `WebApi/Common/Behaviours/LoggingBehaviour.cs`
### خلاصه آمار فاز ۹
| معیار | تعداد |
|---|---|
| فایل‌های حذف‌شده (کد مرده) | 3 |
| فایل‌های جدید | 2 (LocalFileManager بازنویسی + ImagePathResolverInterceptor) |
| فایل‌های ویرایش‌شده | ~12 |
| باگ‌های رفع‌شده | 4 (Orders page ×3 + DB truncation) |
| Migration‌های جدید | 1 (RemoveImagePathMaxLength) |
| ستون‌های DB تغییریافته | 9 (nvarchar(500) → nvarchar(max)) |
| Build | 0 Error ✅ (هر ۳ پروژه) |
---
## 🏷 فاز ۸ — یکسان‌سازی فروشگاه عادی و تخفیفی (بهمن ۱۴۰۴)
> **مستند کامل:** [`totalDoc/ui-modernization/BACKOFFICE-STORE-UNIFICATION.md`](../../totalDoc/ui-modernization/BACKOFFICE-STORE-UNIFICATION.md)
### ۸.۱ — بازسازی NavMenu ✅
- ایجاد دو NavGroup مجزا: «فروشگاه عادی» و «فروشگاه تخفیفی»
- حذف «ویرایش دسته‌جمعی» از منو
- انبارداری → گروه مستقل | پکیج‌ها → آیتم مستقل
- **فایل:** `Shared/NavMenu.razor`
### ۸.۲ — حذف گزارش‌های کوچک از سفارشات ✅
- حذف کارت‌های آماری (TotalOrders + TotalAmount) و نمودار Bar ارسال
- حذف فیلدها: `_stats`, `_statusChartLabels`, `_statusChartSeries`
- حذف متد `UpdateStats()` و کلاس `OrderStatsViewModel`
- عنوان: «سفارش‌های کاربر» → «لیست سفارشات»
- **فایل‌ها:** `Pages/UserOrder/UserOrderMainPage.razor` + `.razor.cs`
### ۸.۳ — رفع باگ لیست خالی سفارشات ✅
- **علت:** `PaymentDate.ToDateTime()` بدون null check → exception ساکت در WASM
- **رفع:** `@if (context.Item.PaymentDate != null)` + fallback "-"
- **فایل:** `Pages/UserOrder/UserOrderMainPage.razor`
### ۸.۴ — بازنویسی صفحه محصولات تخفیفی ✅
- تبدیل به `BasePageComponent` (مطابق فروشگاه عادی)
- فیلترهای جدید: وضعیت + موجودی
- ستون‌های جدید: تصویر inline + عنوان truncate + چیپ رنگی موجودی + چیپ وضعیت
- **فایل‌ها:** `Pages/DiscountShop/DiscountProductsMainPage.razor` + `.razor.cs` (بازنویسی کامل)
### ۸.۵ — بازنویسی صفحه دسته‌بندی‌های تخفیفی ✅
- تبدیل به `BasePageComponent` (مطابق فروشگاه عادی)
- ستون‌های جدید: نام لاتین، دسته‌بندی والد (resolve شده)، تعداد محصولات، ترتیب
- code-behind جدید (`@code` درون‌خطی → `.razor.cs`)
- **فایل‌ها:** `Pages/DiscountShop/DiscountCategoriesMainPage.razor` + `.razor.cs` (بازنویسی کامل)
### خلاصه آمار فاز ۸
| معیار | تعداد |
|---|---|
| فایل‌های بازنویسی‌شده | 4 |
| فایل‌های ویرایش‌شده | 3 |
| باگ‌های رفع‌شده | 2 (PaymentDate null + لیست خالی) |
| Build | 0 Error ✅ |
---
## 🏷 فاز ۷ — پس از اجرای Audit v7.0 (تیر ۱۴۰۴)
### خلاصه تغییرات
- **۵۷ مورد** از ۵۸ یافته audit اجرا شد (≈98%)
- **تمام ۱۶ مورد بحرانی 🔴** حل شد
- **تمام ۱۸ مورد جزئی 🟢** حل شد
- **فازهای ۱ تا ۶ تکمیل شدند** (به‌جز DayaLoan و Audit Trail که نیاز به بک‌اند دارند)
- **فاز ۷** — Global Search, Notification Bell, NavMenu Badge, Export Excel 4 صفحه جدید
- **۱۳+ فایل جدید** ساخته شد
- **۹ صفحه** ادغام شدند (۱۴ صفحه → ۵ صفحه)
- **Build**: 0 error ✅
---
## فاز ۷ — Global Search + Notifications + Export All + NavMenu Badge (v7.0)
### 6.5 — Badge Pending در NavMenu — ✅
- **`NavMenu.razor`**: اضافه شدن `MudBadge` روی لینک «درخواست‌های برداشت»
- نمایش تعداد درخواست‌های Pending بصورت real-time از gRPC (`CommissionContract.GetWithdrawalRequestsAsync` با `Status=0`)
- fire-and-forget در `OnInitializedAsync` با try/catch silent
### 6.2 — Global Search در AppBar — ✅
- **فایل جدید**: `Shared/GlobalSearch.razor`
- `MudAutocomplete<SearchResultItem>` با Debounce 400ms
- دو نوع جستجو: ۱) نام صفحات (۱۵ صفحه اصلی) ۲) کاربران از gRPC (`UserContract.GetAllUserByFilterAsync`)
- Template سفارشی با آیکون + عنوان + زیرنویس
- ناوبری خودکار با `Navigation.NavigateTo`
- اضافه شده در `MainLayout.razor` داخل `<AuthorizeView>`
- CSS در `app.css` — rounded border-radius 24px, compact padding
### 6.1 — Notification Bell (placeholder) — ✅
- **`MainLayout.razor`**: اضافه شدن `MudMenu` + `MudBadge` (Dot) + آیکون Notifications
- Dropdown با هدر «اعلان‌ها» + placeholder «اعلان جدیدی وجود ندارد»
- آماده اتصال به بک‌اند — فقط `Visible=true` و populate items
### 6.3 — Export Excel صفحات جدید — ✅ (۱۰ صفحه کامل)
- **۴ صفحه جدید** اضافه شدند به ۶ صفحه قبلی:
1. **UserMainPage** — خروجی CSV: شناسه, موبایل, نام, نام خانوادگی, کدملی
2. **DiscountOrdersMainPage** — خروجی CSV: شماره سفارش, تاریخ, مبلغ کل, تخفیف, قابل پرداخت, تعداد, وضعیت, پرداخت
3. **ManualPayments** — خروجی CSV: شناسه, شناسه کاربر, نام کاربر, مبلغ, نوع, وضعیت, تاریخ
4. **InventoryMainPage** — خروجی CSV: شناسه, محصول, انبار, موجودی, رزرو, قابل فروش, حد هشدار, وضعیت
- هر ۴ صفحه: دکمه «خروجی Excel» + `ExportToExcel()` + `EscapeCsv()` + UTF-8 BOM + `jsSaveAsFile`
- فایل‌های code-behind جدید: `ManualPayments.razor.cs`, `DiscountOrdersMainPage.razor.cs`
---
## فاز ۶ — تاریخ شمسی + تاریخچه‌ها + Export Excel (v6.0)
### تاریخ شمسی یکپارچه (3.6) — ✅ کامل
- **۱۹+ فایل** آپدیت شدند: تمام تاریخ‌ها از فرمت میلادی (`yyyy/MM/dd`) به شمسی تبدیل شدند
- از extension method‌های `DateTimeConverterCL` استفاده شد:
- `.MiladiToJalali()` — فرمت تاریخ فقط (مثال: ۱۴۰۴/۰۴/۱۲)
- `.MiladiToJalaliWithTime()` — فرمت تاریخ و ساعت
- **صفحات اصلاح‌شده شامل**: Club (Members, Statistics, Features, MemberDetailsDialog)، Commission (Dashboard, WeeklyReports, WithdrawalRequests, Payouts, WithdrawalReports)، Network (UserNetworkInfo, BalancesReport, Statistics, NetworkTreeViewer)، UserOrder, Products, Package, Inventory, DiscountShop, Blog, Content, و غیره
- `PropertyColumn Format="yyyy/MM/dd"``TemplateColumn` با `.MiladiToJalali()`
- `MudDatePicker DateFormat` تغییر نکرد (قابلیت تقویم شمسی نیاز به کتابخانه جداگانه دارد)
### تاریخچه عضویت باشگاه (5.7) — ✅
- **`MemberDetailsDialog.razor`**: تب تاریخچه با فیلدهای صحیح proto:
- ستون‌ها: `Action`, `Created`, `PackageName`, `Reason`, `PerformedBy`
- Request: `PageIndex = 0` (نه PageNumber)
### تاریخچه تغییرات شبکه (5.8) — ✅
- **`UserNetworkInfo.razor`**: تب تاریخچه با فیلدهای تخصصی:
- ستون‌ها: `Action`, `Created`, `OldParentId → NewParentId`, `OldNetworkLeg → NewNetworkLeg` (چپ/راست)
- ستون `Reason` و `PerformedBy` اضافه شد
- Request: `PageIndex = 0`
### لاگ اجرای Worker‌ها (5.6) — ✅
- در تب WorkerControl از SystemHub قبلاً پیاده‌سازی شده بود
### Export Excel / CSV (6.3) — ✅ ۶ صفحه
قابلیت خروجی CSV (سازگار با Excel) به ۶ صفحه اصلی اضافه شد:
| صفحه | فایل(ها) | ستون‌های خروجی |
|---|---|---|
| **Products** (قبلاً موجود) | ProductsMainPage.razor.cs | — |
| **Club Members** | ClubMembers.razor + .razor.cs | شناسه,کاربر,نام,پکیج,کد فعال‌سازی,تاریخ فعال‌سازی,تاریخ انقضا,فعال,منقضی |
| **Withdrawal Requests** | WithdrawalRequests.razor + .razor.cs | شناسه,کاربر,نام,مبلغ,وضعیت,روش,شبا,تاریخ درخواست,تاریخ پردازش,مرجع بانکی |
| **Weekly Reports** | WeeklyReports.razor + .razor.cs | هفته,مبلغ استخر,موجودی‌ها,ارزش,وضعیت,تاریخ محاسبه |
| **User Orders** | UserOrderMainPage.razor + .razor.cs | شناسه,کاربر,نام,مبلغ,وضعیت پرداخت,وضعیت ارسال,روش پرداخت,تاریخ |
| **Stock Movements** | MovementsPage.razor + .razor.cs | شناسه,محصول,نوع,تعداد,قبل,بعد,مرجع,یادداشت,تاریخ |
**الگوی پیاده‌سازی:**
- دکمه `<MudButton>` با آیکون `FileDownload` در ToolBarContent
- `StringBuilder` → CSV با هدرهای فارسی
- UTF-8 BOM (`Encoding.UTF8.GetPreamble()`) برای نمایش صحیح فارسی در Excel
- `jsSaveAsFile(filename, base64)` از `wwwroot/js/main.js`
- متد `EscapeCsv()` برای مقادیر حاوی کاما/کوتیشن
- تاریخ‌ها در خروجی نیز شمسی هستند
### فایل‌های جدید (این نسخه)
| فایل | عملکرد |
|---|---|
| `Pages/Commission/WeeklyReports.razor.cs` | Code-behind برای export (به‌دلیل محدودیت Razor parser با escaped quotes) |
---
## نسخه قبلی — Audit v5.0 (خرداد ۱۴۰۴)
### تم رنگی یکپارچه
- **`CustomMudTheme.cs`**: تم Primary از `#0380C0` به `#6366f1` (Indigo) تغییر کرد — هماهنگ با FrontOffice
- **`PaletteDark`** اضافه شد: Surface `#1a1a2e`, Background `#16213e`, AppbarBackground `#0f3460`
- **`app.css`**: رنگ loading از `#1b6ec2` به `var(--mud-palette-primary)` تغییر کرد
- **`NavMenu.razor.css`**: رنگ active از `#0380C0` هاردکد به CSS variable تغییر کرد
- **`MainLayout.razor`**: حذف inline style `border: 1px solid #5e4df9`
### Dark Mode Toggle
- آیکون ماه/آفتاب در AppBar اضافه شد
- `_isDarkMode` state با `MudThemeProvider` اتصال دارد
- تعویض بین `PaletteLight` و `PaletteDark` در CustomMudTheme
### عنوان اپلیکیشن
- "پنل مدیریت **فرصت**" → "پنل مدیریت **کارا بازار سلامت**"
---
## فاز ۲ — بازطراحی لایوت صفحات
### BasePageComponent بازنویسی کامل
- **قبل**: فیلتر ثابت سمت راست (20%) + جدول (80%)، ارتفاع 85vh هاردکد
- **بعد**: فیلتر collapsible بالای جدول، فیلدها در `MudGrid` با `MudItem`، responsive
- **۱۳ صفحه** آپدیت شدند: User, Products, Package, Category, UserOrder, UserAddress, UserRole, Role, BlogPost, BlogCategory, Tag, Transactions, Content
### DataGrid Height یکسان‌سازی
- همه DataGrid‌ها: `Height="calc(100vh - 240px)"`
- **صفحات اصلاح‌شده**: تمام ~33 صفحه دارای DataGrid
### Pager یکسان‌سازی
- **PageSize**: همه `20, 50, 100`
- **متن فارسی**: `RowsPerPageString="تعداد در صفحه"` و `InfoFormat="سطر {first_item} تا {last_item} از {all_items}"`
### MaxWidth یکسان‌سازی
- `MainLayout.razor` MudContainer wrapper اضافه شد → MaxWidth یکسان برای همه صفحات
- **۳۱ صفحه**: حذف `MudContainer` تکراری (double nesting)
### Breadcrumb
- **`Shared/AppBreadcrumb.razor`** ساخته شد
- جداکننده چپ‌به‌راست (`ChevronLeft`)
- نقشه فارسی URL segments (60+ مسیر)
- آخرین آیتم disabled (صفحه فعلی)
### Login + VerifyCode بازطراحی
- عرض ثابت 35% → responsive `MaxWidth.Small`
- لوگو + برندینگ "کارا بازار سلامت" اضافه شد
- طراحی gradient background
---
## فاز ۲.۵ — پاکسازی ساختاری
### NavMenu بازسازی کامل
- **لینک مرده `categories-dragdrop`**: حذف شد
- **Transactions**: حذف از NavMenu (صفحه stub خالی)
- **AlertsMonitoring**: حذف از NavMenu (100% mock)
- **WorkerControl**: به SystemHub منتقل شد (قبلاً لینک نداشت)
- **«پیام‌های عمومی»**: از بخش فروشگاه تخفیفی به «مدیریت محتوا» منتقل شد
- **ساختار گروه‌بندی** اصلاح شد:
- کمیسیون و شبکه: NavGroup واضح
- باشگاه مشتریان: اعضا و آمار + فیچرها
- فروشگاه: پکیج‌ها، محصولات (group)، سفارش‌ها، فروشگاه تخفیفی (group)
- مدیریت: کاربران، نقش‌ها، پرداخت دستی، **کیف‌پول (جدید)**، **قراردادها (جدید)**
- محتوا: بلاگ، صفحات سایت، پیام‌های عمومی
- سیستم: مدیریت سیستم، نسخه اپ‌ها
---
## فاز ۳ — UX بهبودها
### NoRecordsContent (Empty State)
- **۳۳ DataGrid** آپدیت شدند
- پیام: `<MudAlert Severity="Info">موردی یافت نشد.</MudAlert>`
### Delete Confirmation با نام آیتم
- **۱۱ صفحه** اصلاح شدند:
- Role → `«{model.Title}»`
- User → `«{model.FirstName} {model.LastName}»`
- Package → `«{model.Title}»`
- UserAddress → `«{model.Title}»`
- UserRole → `شناسه «{model.Id}»`
- UserOrder → `شماره «{model.Id}»`
- Category → `«{model.Title}»`
- Products → `«{model.Title}»`
- DiscountCategories → `«{category?.Title}»` (lookup اضافه شد)
- DiscountProducts → `«{product?.Title}»` (lookup اضافه شد)
- PublicMessages → `«{message?.Title}»` (lookup اضافه شد)
### User Menu در AppBar
- `MudMenu` جایگزین متن ساده شماره موبایل شد
- آیتم‌ها: نام کاربر (غیرفعال) + خروج
- آیکون `AccountCircle`
- خروج (`signout`) از NavMenu به AppBar منتقل شد
---
## فاز ۳.۵ — ادغام صفحات
### Dashboard Merge ← `Pages/Index.razor`
- **`Index.razor` + `SystemOverview.razor`** ادغام شدند
- Routes: `/` + `/dashboard/overview`
- بخش‌ها:
- 6 کارت آمار CMS (کاربران، محصولات، پکیج‌ها، دسته‌بندی‌ها، سفارش‌ها، مبلغ پرداخت‌شده)
- آمار کمیسیون (هفته جاری، استخر، موجودی‌ها، وضعیت محاسبه)
- آمار باشگاه (کل/فعال/غیرفعال)
- جدول آخرین ۵ سفارش پرداخت‌شده
- ۶ دکمه Quick Action
- بارگذاری موازی با `Task.WhenAll` + `.ResponseAsync`
- `SystemOverview.razor` **حذف شد**
### DiscountShopHub ← `Pages/DiscountShop/DiscountShopHub.razor`
- Routes: `/discount-shop`, `/discount-orders`, `/discount-sales-reports`
- ۲ تب: DiscountOrdersMainPage + SalesReports
- انتخاب تب از URL
### ClubHub ← `Pages/Club/ClubHub.razor`
- Routes: `/club`, `/club/members`, `/club/statistics`
- ۲ تب: ClubMembers + Statistics
### BlogHub ← `Pages/Blog/BlogHub.razor`
- Routes: `/blog`, `/blog/posts`, `/blog/categories`, `/tags`
- ۳ تب: BlogPostManagementPage + BlogCategoryManagementPage + TagManagementPage
### SystemHub ← `Pages/SystemManagement/SystemHub.razor`
- Routes: `/system`, `/system/configuration`, `/system/worker-control`, `/system/health`
- ۳ تب: Configuration + WorkerControl + HealthDashboard
### ۱۰ صفحه — `@page` directive حذف شد
صفحاتی که به hub منتقل شدند دیگر route مستقل ندارند:
DiscountOrdersMainPage, SalesReports, ClubMembers, Statistics, BlogPostManagementPage, BlogCategoryManagementPage, TagManagementPage, Configuration, WorkerControl, HealthDashboard
---
## فاز ۴ — فیکس‌های بیزینسی
### Dashboard آمار اصلاح شد
- **قبل**: آمار از ۱۰ رکورد اول محاسبه می‌شد (نادرست)
- **بعد**: از `metadata.TotalCount` و API‌های جداگانه استفاده می‌شود
### ManualPayments — تأیید/رد
- دکمه‌های Approve/Reject وقتی `Status==0` (در انتظار) نمایش داده می‌شوند
- Confirmation dialog قبل از اجرا
- از `ManualPaymentId` (نه `Id`) استفاده می‌شود
### UserOrder — فیلتر Cancelled
- `<MudSelectItem Value="@(5)">لغو شده</MudSelectItem>` اضافه شد
### WithdrawalRequests — بهبود کامل
- **Pager**: 10/25/50/100 → 20/50/100
- **`WithdrawalDetailsDialog.razor`** (جدید): نمایش کامل اطلاعات کاربر، درخواست، و بانکی
- **`RejectReasonDialog.razor`** (جدید): ورود دلیل رد با اعتبارسنجی
- ViewDetails: از Snackbar TODO به دیالوگ واقعی تغییر کرد
- RejectRequest: از دلیل هاردکد به دیالوگ custom تغییر کرد
---
## فاز ۵ — صفحات مفقود (CMS-Derived)
### WalletManagementPage ← `Pages/Wallet/WalletManagementPage.razor`
- Route: `/wallets`
- **تب ۱ — کیف‌پول‌ها**: DataGrid با ServerData، فیلتر UserAutoComplete
- ستون‌ها: شناسه، کاربر، موجودی اصلی، موجودی شبکه، مشاهده تغییرات
- نوع: `GetAllUserWalletByFilterResponseModel`
- **تب ۲ — تاریخچه تغییرات**: DataGrid با ServerData، فیلتر کاربر + نوع (افزایش/کاهش)
- ستون‌ها: شناسه، کیف‌پول، نوع (چیپ رنگی)، تغییر موجودی، موجودی فعلی، تغییر شبکه، موجودی شبکه فعلی، شناسه مرجع، تاریخ
- نوع: `GetAllUserWalletChangeLogByFilterResponseModel`
- فیلدهای proto با typo: `ChangeNerworkValue`, `RefrenceId`
### UserContractPage ← `Pages/Contract/UserContractPage.razor`
- Route: `/contracts`
- DataGrid با ServerData، فیلتر UserAutoComplete
- ستون‌ها: شناسه، کاربر، قرارداد، کد امضا، لینک PDF، جزئیات
- نوع: `GetAllUserContractByFilterResponseModel`
### ContractDetailsDialog ← `Pages/Contract/ContractDetailsDialog.razor`
- جدول ساده: شناسه، کاربر، قالب قرارداد، کد امضا، لینک دانلود PDF
### ClubFeaturesPage ← `Pages/Club/ClubFeaturesPage.razor`
- Route: `/club/features`
- **تب ۱ — فیچرهای تعریف‌شده**: کارت‌های MudPaper با عنوان، توضیحات، وضعیت (فعال/غیرفعال)، ترتیب
- از `ConfigClient.GetClubFeaturesAsync`
- **تب ۲ — فیچرهای کاربران**: DataGrid با فیلتر کاربر، سوئیچ فعال/غیرفعال
- از `ClubClient.GetUserClubFeaturesAsync` + `ToggleUserClubFeatureAsync`
---
## ⚠️ نکات فنی مهم
### الگوهای Proto
- **فیلتر**: `new GetAllXxxByFilterFilter()` (سطح بالا، بدون `.Types.`)
- **پاسخ لیست**: `GetAllXxxByFilterResponseModel` (نه `GetXxxResponse`)
- **Typo‌ها در proto**: `ChangeNerworkValue` و `RefrenceId` — باید دقیقاً همین‌ها استفاده شوند
### _Imports.razor — سرویس‌های Global
سرویس‌های زیر از `_Imports.razor` تزریق می‌شوند و نباید در code-behind با `[Inject]` دوباره تعریف شوند:
- `NavigationManager Navigation`
- `ILocalStorageService LocalStorageService`
- `ISnackbar Snackbar`
- `IJSRuntime jsRuntime`
- `IDialogService DialogService`
- `AuthenticationStateProvider`
### MudBlazor محدودیت‌ها
- `Variant.Flat` وجود ندارد → از `Variant.Text` استفاده شود
- `GutterSize` روی `MudGrid` deprecated است
---
## فاز ۴ (ادامه) — تکمیل فیکس‌های بیزینسی
### UserSettings — حذف تب‌های جعلی (2.5.5)
- **`UserSettings.razor`**: تب Notifications (۷ تاگل جعلی بدون بک‌اند) → placeholder «به‌زودی فعال می‌شود» با آیکون NotificationsActive
- **`UserSettings.razor`**: تب Security (فرم تغییر رمز + 2FA بدون بک‌اند) → placeholder «به‌زودی فعال می‌شود» با آیکون Lock
- **حذف کد مرده**: ~۱۵ متغیر notification/security + متدهای `SaveNotificationSettings()`, `SaveSecuritySettings()`, `ChangePassword()`
### HealthDashboard — حذف متریک‌های Mock (2.5.6)
- **`HealthDashboard.razor`**: بخش System Resources (۴ کارت CPU=45.2%, Memory=62.8%, Disk=73.5%, Network) → یک کارت با پیام «مانیتورینگ منابع سیستم در نسخه‌های آینده با اتصال به Prometheus/Grafana فعال خواهد شد»
- **حذف کد مرده**: ۹ متغیر mock (`_cpuUsage`, `_memoryUsage`, etc.) + متد `GetResourceColor()`
### حذف cursor:pointer زائد (4.10)
- `Style="cursor:pointer;"` از **۱۷ فایل** حذف شد (MudButton/MudIconButton خودشان cursor:pointer دارند)
- شامل: ProductsMainPage, GalleryDialog, Image.razor (base component), RoleMainPage, UserOrderMainPage, PackageMainPage, etc.
### حذف Console.WriteLine (4.11)
- از **۱۱ فایل** حذف شد: DiscountProductService, DiscountCategoryService, NetworkTreeViewer, UserSettings, CategoryMainPage, UserOrderMainPage, Commission/Dashboard, DiscountProductsMainPage, DiscountCategoryMultiSelectCombo, UserRole/CreateDialog
- **`Commission/Dashboard.razor.cs`**: `using System.Text.Json` هم حذف شد (دیگر استفاده نمی‌شد)
### DiscountProducts — فیلتر دسته‌بندی (4.7)
- **`DiscountProductsMainPage.razor`**: `@inject IDiscountCategoryService` اضافه شد
- دسته‌بندی‌های فعال از سرویس بارگذاری و در MudSelect نمایش داده می‌شوند
- جایگزینی `@* TODO: Load categories *@` با `@foreach` واقعی
### DiscountProducts — Items → ServerData (4.8)
- **`DiscountProductsMainPage.razor`**: تبدیل از `Items="@_products"` به `ServerData="LoadServerData"`
- متد `LoadServerData(GridState<T>)` اضافه شد با `PageNumber` و `PageSize` از state
- `Height="calc(100vh - 300px)"` + `FixedHeader="true"` اضافه شد
- Snackbar اطلاع‌رسانی اضافی در هر بارگذاری حذف شد (redundant)
### UserOrder — فعال‌سازی آمار (4.6)
- **`UserOrderMainPage.razor`**: بخش آمار (MudCard + MudChart) از حالت comment خارج شد
- نمایش تعداد سفارش‌ها + مجموع مبلغ + نمودار Bar وضعیت ارسال
### UserSettings — persist با localStorage (4.9)
- تب General از قبل با localStorage کار می‌کرد ✅ (فقط تب‌های جعلی حذف شدند)
---
## فاز ۴.۵ — Cross-Navigation
### لینک نام کاربر → پروفایل (4.5.1, 4.5.2)
- **`UserOrderMainPage.razor`**: ستون `UserFullName` از PropertyColumn به TemplateColumn تبدیل شد — MudLink به `/network/user-info/{UserId}`
- **`UserPayouts.razor`**: نام/شناسه کاربر به MudLink تبدیل شد → `/network/user-info/{UserId}`
- **`WithdrawalRequests.razor`**: نام کاربر به MudLink تبدیل شد → `/network/user-info/{UserId}`
### لینک محصول ↔ انبار (4.5.3, 4.5.4) — Skip
- صفحه جزئیات محصول وجود ندارد (فقط لیست + دیالوگ ویرایش)
- لینک cross-navigation بدون صفحه مقصد معنادار نیست
### Extract shared status helpers (3.5.6) — Skip
- بررسی نشان داد هر context مقادیر متفاوتی دارد:
- UserPayouts: status 1 = Success/پرداخت شده
- WithdrawalRequests: status 1 = Info/واریز شده
- PayoutDetailsDialog: فقط ۳ وضعیت (نه ۶)
- استخراج به helper مشترک باعث از دست رفتن معنای domain-specific می‌شود
---
## 📈 آمار تغییرات
| معیار | تعداد |
|---|---|
| فایل‌های جدید | 11 |
| فایل‌های حذف‌شده | 1 (SystemOverview.razor) |
| فایل‌های ویرایش‌شده | ~65+ |
| DataGrid‌های آپدیت‌شده | 33 |
| Delete confirmation اصلاح‌شده | 11 |
| صفحات ادغام‌شده | 14 → 5 |
| خطاهای build حل‌شده | 9+ |
| Console.WriteLine حذف‌شده | 22+ (از ۱۱ فایل) |
| cursor:pointer حذف‌شده | 17 فایل |
| Cross-link اضافه‌شده | 3 صفحه (UserOrder, UserPayouts, WithdrawalRequests) |
+82
View File
@@ -0,0 +1,82 @@
---
# BackOffice UI Deployment (Blazor WASM + nginx)
apiVersion: apps/v1
kind: Deployment
metadata:
name: backoffice
namespace: default
labels:
app: backoffice
spec:
replicas: 1
selector:
matchLabels:
app: backoffice
template:
metadata:
labels:
app: backoffice
spec:
containers:
- name: backoffice
image: 194.5.195.53:30080/admin/backoffice:latest
imagePullPolicy: Always
ports:
- containerPort: 80
name: http
env:
- name: BLAZOR_ENVIRONMENT
value: "Staging"
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
imagePullSecrets:
- name: gitea-registry-secret
---
# BackOffice UI Service
apiVersion: v1
kind: Service
metadata:
name: backoffice-svc
namespace: default
spec:
selector:
app: backoffice
ports:
- port: 80
targetPort: 80
type: ClusterIP
---
# BackOffice UI Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: backoffice-ingress
namespace: default
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/service-upstream: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- backoffice.se.kbs1.ir
secretName: backoffice-tls
rules:
- host: backoffice.se.kbs1.ir
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: backoffice-svc
port:
number: 80
+8 -3
View File
@@ -9,6 +9,8 @@
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute> <GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<!-- Suppress pre-existing warnings that do not affect runtime correctness -->
<NoWarn>MUD0002;CA2022;BL0005</NoWarn>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -136,9 +138,12 @@
<!-- This enables direct communication with CMS (no BFF needed) --> <!-- This enables direct communication with CMS (no BFF needed) -->
<!-- ============================================================ --> <!-- ============================================================ -->
<!-- CMS Protobuf NuGet package (used for both local dev and Docker builds) --> <!-- CMS Protobuf: NuGet in CI/Docker; ProjectReference for local monorepo dev -->
<ItemGroup> <ItemGroup Condition="Exists('../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj')">
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.180" /> <ProjectReference Include="../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj" />
</ItemGroup>
<ItemGroup Condition="!Exists('../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj')">
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.208" />
</ItemGroup> </ItemGroup>
<!-- ============================================================ --> <!-- ============================================================ -->
@@ -8,21 +8,46 @@ namespace BackOffice.Common.BaseComponents;
public partial class DateRangePicker public partial class DateRangePicker
{ {
private DateRange _dateRange = new(); private DateRange _dateRange = new();
private MudDateRangePicker _picker = default!;
private DateTime? _lastSyncedFrom;
private DateTime? _lastSyncedTo;
private MudDateRangePicker _picker;
[Parameter] public string Label { get; set; } = "انتخاب بازه زمانی"; [Parameter] public string Label { get; set; } = "انتخاب بازه زمانی";
[Parameter] public DateTime? DefaultStart { get; set; } [Parameter] public DateTime? DefaultStart { get; set; }
[Parameter] public DateTime? DefaultEnd { get; set; } [Parameter] public DateTime? DefaultEnd { get; set; }
[Parameter] public EventCallback<DateRange> OnChanged { get; set; } [Parameter] public EventCallback<DateRange> OnChanged { get; set; }
[Parameter] public DateTime? From { get; set; }
[Parameter] public EventCallback<DateTime?> FromChanged { get; set; }
[Parameter] public DateTime? To { get; set; }
[Parameter] public EventCallback<DateTime?> ToChanged { get; set; }
protected override void OnInitialized() protected override void OnInitialized()
{ {
base.OnInitialized(); base.OnInitialized();
if (DefaultStart.HasValue) var start = From ?? DefaultStart;
_dateRange.Start = DefaultStart.Value.Date; var end = To ?? DefaultEnd;
ApplyExternalRange(start, end);
if (DefaultEnd.HasValue) _lastSyncedFrom = From;
_dateRange.End = DefaultEnd.Value.Date; _lastSyncedTo = To;
} }
protected override void OnParametersSet()
{
// فقط وقتی والد From/To را عوض کرده (مثلاً پاک کردن فیلتر)، نه هنگام انتخاب داخل پیکر
if (From == _lastSyncedFrom && To == _lastSyncedTo)
return;
_lastSyncedFrom = From;
_lastSyncedTo = To;
ApplyExternalRange(From, To);
}
private void ApplyExternalRange(DateTime? start, DateTime? end)
{
_dateRange = new DateRange(start?.Date, end?.Date);
}
public CultureInfo GetPersianCulture() public CultureInfo GetPersianCulture()
{ {
var culture = new CultureInfo("fa-IR"); var culture = new CultureInfo("fa-IR");
@@ -53,19 +78,35 @@ public partial class DateRangePicker
culture.NumberFormat.NumberNegativePattern = 0; culture.NumberFormat.NumberNegativePattern = 0;
return culture; return culture;
} }
private async Task OnClickOK() private async Task OnClickOK()
{ {
if (_picker.DateRange is not null) var range = _picker.DateRange ?? _dateRange;
{ _dateRange = range ?? new DateRange();
await OnChanged.InvokeAsync(_picker.DateRange); await NotifyRangeChangedAsync(_dateRange);
}
await _picker.CloseAsync(); await _picker.CloseAsync();
} }
private async Task OnClickClear() private async Task OnClickClear()
{ {
_dateRange = new DateRange(); _dateRange = new DateRange();
await OnChanged.InvokeAsync(_dateRange); await NotifyRangeChangedAsync(_dateRange);
await _picker.CloseAsync(); await _picker.CloseAsync();
} }
private async Task NotifyRangeChangedAsync(DateRange range)
{
var start = range.Start?.Date;
var end = range.End?.Date;
_lastSyncedFrom = start;
_lastSyncedTo = end;
if (From != start)
await FromChanged.InvokeAsync(start);
if (To != end)
await ToChanged.InvokeAsync(end);
await OnChanged.InvokeAsync(range);
}
} }
@@ -30,7 +30,12 @@ using CMSMicroservice.Protobuf.Protos.SitePage;
using CMSMicroservice.Protobuf.Protos.SitePageSettings; using CMSMicroservice.Protobuf.Protos.SitePageSettings;
using CMSMicroservice.Protobuf.Protos.UserContract; using CMSMicroservice.Protobuf.Protos.UserContract;
using CMSMicroservice.Protobuf.Protos.UserWallet; using CMSMicroservice.Protobuf.Protos.UserWallet;
using CMSMicroservice.Protobuf.Protos.UserWalletChangeLog; using CMSMicroservice.Protobuf.Protos.UserWalletHistory;
using CMSMicroservice.Protobuf.Protos.PaymentTransaction;
using CMSMicroservice.Protobuf.Protos.Transactions;
using CMSMicroservice.Protobuf.Protos.UserPackagePurchase;
using CMSMicroservice.Protobuf.Protos.ClubMembershipCycle;
using CMSMicroservice.Protobuf.Protos.ClubMembershipCycleHistory;
using BackOffice.Common.Utilities; using BackOffice.Common.Utilities;
using BackOffice.Services.DiscountProduct; using BackOffice.Services.DiscountProduct;
using BackOffice.Services.DiscountCategory; using BackOffice.Services.DiscountCategory;
@@ -167,7 +172,14 @@ public static class ConfigureServices
// User Wallet Services // User Wallet Services
services.AddTransient(sp => new UserWalletContract.UserWalletContractClient(sp.GetRequiredService<CallInvoker>())); services.AddTransient(sp => new UserWalletContract.UserWalletContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new UserWalletChangeLogContract.UserWalletChangeLogContractClient(sp.GetRequiredService<CallInvoker>())); services.AddTransient(sp => new UserWalletHistoryContract.UserWalletHistoryContractClient(sp.GetRequiredService<CallInvoker>()));
// Admin Report Services
services.AddTransient(sp => new PaymentTransactionContract.PaymentTransactionContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new TransactionsContract.TransactionsContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new UserPackagePurchaseContract.UserPackagePurchaseContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new ClubMembershipCycleContract.ClubMembershipCycleContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new ClubMembershipCycleHistoryContract.ClubMembershipCycleHistoryContractClient(sp.GetRequiredService<CallInvoker>()));
return services; return services;
} }
@@ -1,4 +1,5 @@
using BackOffice.Common.Utilities; using System.IdentityModel.Tokens.Jwt;
using BackOffice.Common.Utilities;
using Blazored.LocalStorage; using Blazored.LocalStorage;
namespace BackOffice.Common.Utilities; namespace BackOffice.Common.Utilities;
@@ -19,9 +20,41 @@ public class AppTokenProvider : ITokenProvider
{ {
var authorizationToken = await _localStorage.GetItemAsync<string>(GlobalConstants.JwtTokenKey); var authorizationToken = await _localStorage.GetItemAsync<string>(GlobalConstants.JwtTokenKey);
if (!string.IsNullOrEmpty(authorizationToken)) if (!string.IsNullOrEmpty(authorizationToken))
_token = authorizationToken.ToString().Replace("Bearer ", ""); {
var raw = authorizationToken.ToString().Replace("Bearer ", "");
// Don't send expired tokens — they cause 401 on anonymous endpoints like login
if (IsTokenExpired(raw))
{
await _localStorage.RemoveItemAsync(GlobalConstants.JwtTokenKey);
_token = null;
return _token;
}
_token = raw;
}
}
else if (IsTokenExpired(_token))
{
// Cached token has expired since last check
await _localStorage.RemoveItemAsync(GlobalConstants.JwtTokenKey);
_token = null;
} }
return _token; return _token;
} }
private static bool IsTokenExpired(string token)
{
try
{
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(token);
// Add 30-second buffer to avoid edge-case race
return jwt.ValidTo < DateTime.UtcNow.AddSeconds(-30);
}
catch
{
// Malformed token — treat as expired
return true;
}
}
} }
@@ -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 ? "بله" : "خیر")}";
}
+9 -9
View File
@@ -25,14 +25,14 @@ RUN rm -rf /usr/share/nginx/html/*
# Copy published wwwroot (Blazor WASM output) # Copy published wwwroot (Blazor WASM output)
COPY --from=build /app/publish/wwwroot . COPY --from=build /app/publish/wwwroot .
# Configure nginx for SPA routing # Copy entrypoint script that sets blazor-environment header
RUN echo 'server { \ COPY ["BackOffice/docker-entrypoint.sh", "/docker-entrypoint.sh"]
listen 80; \ RUN chmod +x /docker-entrypoint.sh
server_name _; \
location / { \ # Default Blazor environment (override via K8s env var BLAZOR_ENVIRONMENT)
root /usr/share/nginx/html; \ ENV BLAZOR_ENVIRONMENT=Staging
try_files $uri $uri/ /index.html; \ # Default gateway URL for gRPC calls (injected into appsettings at startup)
} \ ENV GW_URL=https://cms.kbs2.ir
}' > /etc/nginx/conf.d/default.conf
EXPOSE 80 EXPOSE 80
CMD ["/docker-entrypoint.sh"]
+2
View File
@@ -2,6 +2,8 @@
<configuration> <configuration>
<packageSources> <packageSources>
<clear /> <clear />
<!-- Local packages (dev builds) -->
<!-- <add key="Local" value="/home/masoud/Apps/project/FourSat/nupkg" /> -->
<!-- Nexus (hosts FourSat packages + proxies nuget.org) --> <!-- Nexus (hosts FourSat packages + proxies nuget.org) -->
<add key="Nexus" value="http://194.5.195.53:32081/repository/nuget-all/index.json" allowInsecureConnections="true" /> <add key="Nexus" value="http://194.5.195.53:32081/repository/nuget-all/index.json" allowInsecureConnections="true" />
</packageSources> </packageSources>
@@ -0,0 +1,16 @@
@using CMSMicroservice.Protobuf.Protos.Package
<MudSelect T="long?"
Label="@Label"
Value="@SelectedPackageId"
ValueChanged="OnValueChanged"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Clearable="true"
Style="min-width: 160px;">
<MudSelectItem T="long?" Value="@((long?)null)">همه پکیج‌ها</MudSelectItem>
@foreach (var pkg in _packages)
{
<MudSelectItem T="long?" Value="@((long?)pkg.Id)">@pkg.Title</MudSelectItem>
}
</MudSelect>
@@ -0,0 +1,46 @@
using CMSMicroservice.Protobuf.Protos.Package;
using Microsoft.AspNetCore.Components;
namespace BackOffice.Pages.AutoComplete;
public partial class PackageSelect
{
[Inject] public PackageContract.PackageContractClient PackageClient { get; set; }
[Parameter] public string Label { get; set; } = "پکیج";
[Parameter] public long? SelectedPackageId { get; set; }
[Parameter] public EventCallback<long?> SelectedPackageIdChanged { get; set; }
private List<GetAllPackageByFilterResponseModel> _packages = new();
protected override async Task OnInitializedAsync()
{
await LoadPackages();
}
private async Task LoadPackages()
{
try
{
var request = new GetAllPackageByFilterRequest
{
PaginationState = new() { PageNumber = 1, PageSize = 100 },
Filter = new()
};
var result = await PackageClient.GetAllPackageByFilterAsync(request);
_packages = result.Models?.OrderBy(p => p.SortOrder).ToList()
?? new List<GetAllPackageByFilterResponseModel>();
}
catch
{
_packages = new List<GetAllPackageByFilterResponseModel>();
}
}
private async Task OnValueChanged(long? value)
{
SelectedPackageId = value;
await SelectedPackageIdChanged.InvokeAsync(value);
}
}
@@ -12,6 +12,11 @@
Required="true" Required="true"
Variant="Variant.Outlined" /> Variant="Variant.Outlined" />
<MudTextField @bind-Value="Model.Slug"
Label="نشانی URL (slug)"
Variant="Variant.Outlined"
HelperText="اختیاری — خالی بگذارید تا خودکار از عنوان ساخته شود. فقط حروف کوچک انگلیسی، عدد و خط تیره." />
<MudTextField @bind-Value="Model.Summary" <MudTextField @bind-Value="Model.Summary"
Label="خلاصه" Label="خلاصه"
Variant="Variant.Outlined" Variant="Variant.Outlined"
@@ -11,6 +11,11 @@
<div> <div>
<MudText Typo="Typo.h5" Class="mb-3">فیچرهای باشگاه</MudText> <MudText Typo="Typo.h5" Class="mb-3">فیچرهای باشگاه</MudText>
@* ── G9: راهنمای ماتریس فیچر (Q28) ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
فیچرهای تیک‌خورده برای خریداران این پکیج فعال می‌شود. تغییر فیچرها روی کاربران فعلی بلافاصله اعمال می‌شود.
</MudAlert>
<MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true" <MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true"
PanelClass="pt-4" @bind-ActivePanelIndex="_activeTab"> PanelClass="pt-4" @bind-ActivePanelIndex="_activeTab">
+24 -3
View File
@@ -1,17 +1,34 @@
@page "/club" @page "/club"
@page "/club/members" @page "/club/members"
@page "/club/purchases"
@page "/club/customer-packages"
@page "/club/cycles"
@page "/club/history"
@page "/club/statistics" @page "/club/statistics"
@attribute [Authorize] @attribute [Authorize]
@using BackOffice.Pages.Club.Reports
<div> <div>
<MudText Typo="Typo.h5" Class="mb-3">باشگاه مشتریان</MudText> <MudText Typo="Typo.h5" Class="mb-3">باشگاه مشتریان</MudText>
<MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true" <MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true"
PanelClass="pt-4" @bind-ActivePanelIndex="_activeTab"> PanelClass="pt-4" @bind-ActivePanelIndex="_activeTab">
<MudTabPanel Text="اعضای باشگاه" Icon="@Icons.Material.Filled.Groups"> <MudTabPanel Text="اعضا" Icon="@Icons.Material.Filled.Groups">
<ClubMembers /> <ClubMembers />
</MudTabPanel> </MudTabPanel>
<MudTabPanel Text="آمار باشگاه" Icon="@Icons.Material.Filled.PieChart"> <MudTabPanel Text="گزارش خرید پکیج" Icon="@Icons.Material.Filled.ShoppingBag">
<ClubPackagePurchasesReport />
</MudTabPanel>
<MudTabPanel Text="گزارش مشتریان پکیج" Icon="@Icons.Material.Filled.PeopleAlt">
<CustomerPackagePurchasesReport />
</MudTabPanel>
<MudTabPanel Text="گزارش دوره‌ها" Icon="@Icons.Material.Filled.Loop">
<ClubCyclesReport />
</MudTabPanel>
<MudTabPanel Text="گزارش تاریخچه" Icon="@Icons.Material.Filled.History">
<ClubHistoriesReport />
</MudTabPanel>
<MudTabPanel Text="آمار" Icon="@Icons.Material.Filled.PieChart">
<Statistics /> <Statistics />
</MudTabPanel> </MudTabPanel>
</MudTabs> </MudTabs>
@@ -25,7 +42,11 @@
var uri = Navigation.ToBaseRelativePath(Navigation.Uri).ToLowerInvariant().TrimEnd('/'); var uri = Navigation.ToBaseRelativePath(Navigation.Uri).ToLowerInvariant().TrimEnd('/');
_activeTab = uri switch _activeTab = uri switch
{ {
"club/statistics" => 1, "club/purchases" => 1,
"club/customer-packages" => 2,
"club/cycles" => 3,
"club/history" => 4,
"club/statistics" => 5,
_ => 0 _ => 0
}; };
} }
+8 -1
View File
@@ -3,11 +3,17 @@
@using Google.Protobuf.WellKnownTypes @using Google.Protobuf.WellKnownTypes
@using BackOffice.Pages.Club.Components @using BackOffice.Pages.Club.Components
@using BackOffice.Pages.AutoComplete
@using CMSMicroservice.Protobuf.Protos.ClubMembership @using CMSMicroservice.Protobuf.Protos.ClubMembership
<div> <div>
<MudText Typo="Typo.h4" Class="mb-4">مدیریت اعضای باشگاه</MudText> <MudText Typo="Typo.h4" Class="mb-4">مدیریت اعضای باشگاه</MudText>
@* ── G13: راهنمای اعضای باشگاه (Q28) ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
فیلتر بر اساس آخرین پکیج خریداری‌شده اعمال می‌شود. هر کاربر می‌تواند چندین دوره پکیج داشته باشد.
</MudAlert>
<MudDataGrid T="ClubMembershipModel" <MudDataGrid T="ClubMembershipModel"
ServerData="@(new Func<GridState<ClubMembershipModel>, Task<GridData<ClubMembershipModel>>>(ServerReload))" ServerData="@(new Func<GridState<ClubMembershipModel>, Task<GridData<ClubMembershipModel>>>(ServerReload))"
Hover="true" Hover="true"
@@ -16,7 +22,8 @@
<ToolBarContent> <ToolBarContent>
<MudText Typo="Typo.h6">لیست اعضا</MudText> <MudText Typo="Typo.h6">لیست اعضا</MudText>
<MudSpacer /> <MudSpacer />
<MudStack Row="true" Spacing="2"> <MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<UserAutoComplete Label="جستجوی کاربر" @bind-SelectedUserId="_filterUserId" />
<MudSwitch T="bool" @bind-Value="_filterIsActive" <MudSwitch T="bool" @bind-Value="_filterIsActive"
Color="Color.Success" Color="Color.Success"
Label="فقط فعال‌ها" /> Label="فقط فعال‌ها" />
@@ -16,6 +16,7 @@ public partial class ClubMembers
private MudDataGrid<ClubMembershipModel> _gridData; private MudDataGrid<ClubMembershipModel> _gridData;
private bool _filterIsActive = true; private bool _filterIsActive = true;
private long? _filterUserId;
private async Task<GridData<ClubMembershipModel>> ServerReload(GridState<ClubMembershipModel> state) private async Task<GridData<ClubMembershipModel>> ServerReload(GridState<ClubMembershipModel> state)
{ {
@@ -30,6 +31,9 @@ public partial class ClubMembers
// Always use the filter value since it's now a bool (not nullable) // Always use the filter value since it's now a bool (not nullable)
request.IsActive = _filterIsActive; request.IsActive = _filterIsActive;
if (_filterUserId.HasValue && _filterUserId.Value > 0)
request.UserId = _filterUserId.Value;
var result = await ClubContract.GetAllClubMembershipsAsync(request); var result = await ClubContract.GetAllClubMembershipsAsync(request);
if (result?.Models != null && result.Models.Any()) if (result?.Models != null && result.Models.Any())
@@ -1,32 +1,39 @@
@using CMSMicroservice.Protobuf.Protos.ClubMembership @using CMSMicroservice.Protobuf.Protos.ClubMembership
@using CMSMicroservice.Protobuf.Protos.Package
@using BackOffice.Pages.AutoComplete
<MudDialog> <MudDialog>
<DialogContent> <DialogContent>
<MudForm @ref="_form" @bind-IsValid="_formValid"> <MudForm @ref="_form" @bind-IsValid="_formValid">
<MudStack Spacing="3"> <MudStack Spacing="3">
<MudNumericField @bind-Value="_model.UserId" <UserAutoComplete Label="انتخاب کاربر" @bind-SelectedUserId="_model.UserId" />
Label="شناسه کاربر"
Required="true"
RequiredError="شناسه کاربر الزامی است"
Variant="Variant.Outlined" />
<MudNumericField @bind-Value="_model.PackageId" @if (_isLoadingPackages)
Label="شناسه پکیج" {
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Height="56px" />
}
else
{
<MudSelect T="long" @bind-Value="_model.PackageId"
Label="انتخاب پکیج"
Required="true" Required="true"
RequiredError="شناسه پکیج الزامی است" RequiredError="انتخاب پکیج الزامی است"
Variant="Variant.Outlined" /> Variant="Variant.Outlined"
AnchorOrigin="Origin.BottomCenter">
<MudNumericField @bind-Value="_model.DurationMonths" @foreach (var pkg in _packages)
Label="مدت زمان (ماه)" {
Required="true" <MudSelectItem T="long" Value="pkg.Id">
RequiredError="مدت زمان الزامی است" @pkg.Title — @($"{pkg.Price:N0}") ریال
Min="1" </MudSelectItem>
Max="12" }
Variant="Variant.Outlined" /> </MudSelect>
}
<MudAlert Severity="Severity.Info" Dense="true"> <MudAlert Severity="Severity.Info" Dense="true">
<MudText Typo="Typo.body2"> <MudText Typo="Typo.body2">
با تایید، عضویت باشگاه برای کاربر فعال می‌شود. با تایید، عضویت باشگاه برای کاربر فعال می‌شود.
<br/>
<small>عضویت باشگاه بدون محدودیت زمانی است.</small>
</MudText> </MudText>
</MudAlert> </MudAlert>
</MudStack> </MudStack>
@@ -37,7 +44,7 @@
<MudButton Color="Color.Success" <MudButton Color="Color.Success"
Variant="Variant.Filled" Variant="Variant.Filled"
OnClick="Submit" OnClick="Submit"
Disabled="@(!_formValid || _isSubmitting)"> Disabled="@(!_formValid || _isSubmitting || _isLoadingPackages)">
@if (_isSubmitting) @if (_isSubmitting)
{ {
<MudProgressCircular Size="Size.Small" Indeterminate="true" /> <MudProgressCircular Size="Size.Small" Indeterminate="true" />
@@ -54,12 +61,49 @@
@code { @code {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } [CascadingParameter] IMudDialogInstance MudDialog { get; set; }
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubContract { get; set; } [Inject] public ClubMembershipContract.ClubMembershipContractClient ClubContract { get; set; }
[Inject] public PackageContract.PackageContractClient PackageClient { get; set; }
private MudForm _form; private MudForm _form;
private bool _formValid; private bool _formValid;
private bool _isSubmitting; private bool _isSubmitting;
private bool _isLoadingPackages = true;
private List<GetAllPackageByFilterResponseModel> _packages = new();
private ActivateModel _model = new(); private ActivateModel _model = new();
protected override async Task OnInitializedAsync()
{
await LoadPackages();
}
private async Task LoadPackages()
{
_isLoadingPackages = true;
try
{
var request = new GetAllPackageByFilterRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = 1,
PageSize = 50
}
};
var result = await PackageClient.GetAllPackageByFilterAsync(request);
if (result?.Models != null)
{
_packages = result.Models.ToList();
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری پکیج‌ها: {ex.Message}", Severity.Error);
}
finally
{
_isLoadingPackages = false;
}
}
private void Cancel() => MudDialog.Cancel(); private void Cancel() => MudDialog.Cancel();
private async Task Submit() private async Task Submit()
@@ -67,19 +111,31 @@
await _form.Validate(); await _form.Validate();
if (!_formValid) return; if (!_formValid) return;
if (!_model.UserId.HasValue || _model.UserId.Value <= 0)
{
Snackbar.Add("لطفاً کاربر را انتخاب کنید.", Severity.Warning);
return;
}
if (_model.PackageId <= 0)
{
Snackbar.Add("لطفاً پکیج را انتخاب کنید.", Severity.Warning);
return;
}
_isSubmitting = true; _isSubmitting = true;
try try
{ {
var request = new ActivateClubMembershipRequest var request = new ActivateClubMembershipRequest
{ {
UserId = _model.UserId, UserId = _model.UserId.Value,
PackageId = _model.PackageId, PackageId = _model.PackageId,
DurationMonths = _model.DurationMonths ForceActivation = true
}; };
var response = await ClubContract.ActivateClubMembershipAsync(request); var response = await ClubContract.ActivateClubMembershipAsync(request);
Snackbar.Add($"عضویت با موفقیت فعال شد.", Severity.Success); Snackbar.Add("عضویت با موفقیت فعال شد.", Severity.Success);
MudDialog.Close(DialogResult.Ok(true)); MudDialog.Close(DialogResult.Ok(true));
} }
catch (Exception ex) catch (Exception ex)
@@ -94,8 +150,7 @@
private class ActivateModel private class ActivateModel
{ {
public long UserId { get; set; } public long? UserId { get; set; }
public long PackageId { get; set; } public long PackageId { get; set; }
public int DurationMonths { get; set; } = 1;
} }
} }
@@ -33,23 +33,11 @@
</tr> </tr>
<tr> <tr>
<td><strong>پکیج:</strong></td> <td><strong>پکیج:</strong></td>
<td>@Member.PackageName</td> <td>@(string.IsNullOrEmpty(Member.PackageName) ? "—" : Member.PackageName)</td>
</tr>
<tr>
<td><strong>کد فعال‌سازی:</strong></td>
<td dir="ltr">@Member.ActivationCode</td>
</tr> </tr>
<tr> <tr>
<td><strong>تاریخ فعال‌سازی:</strong></td> <td><strong>تاریخ فعال‌سازی:</strong></td>
<td>@Member.ActivatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td> <td>@(Member.ActivatedAt != null ? Member.ActivatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() : "—")</td>
</tr>
<tr>
<td><strong>تاریخ انقضا:</strong></td>
<td>
<MudText Color="@(Member.IsExpired ? Color.Error : Color.Success)">
@Member.ExpiresAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()
</MudText>
</td>
</tr> </tr>
<tr> <tr>
<td><strong>وضعیت:</strong></td> <td><strong>وضعیت:</strong></td>
@@ -61,60 +49,17 @@
</MudChip> </MudChip>
</td> </td>
</tr> </tr>
<tr>
<td><strong>منقضی شده:</strong></td>
<td>
<MudChip T="string"
Color="@(Member.IsExpired ? Color.Warning : Color.Default)"
Size="Size.Small">
@(Member.IsExpired ? "بله" : "خیر")
</MudChip>
</td>
</tr>
</tbody>
</MudSimpleTable>
</MudPaper>
<MudPaper Class="pa-3" Elevation="0">
<MudText Typo="Typo.h6" Color="Color.Primary">تاریخچه</MudText>
<MudDivider Class="my-2" />
<MudSimpleTable Dense="true">
<tbody>
<tr> <tr>
<td><strong>تاریخ ایجاد:</strong></td> <td><strong>تاریخ ایجاد:</strong></td>
<td>@Member.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td> <td>@Member.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td>
</tr> </tr>
</tbody> </tbody>
</MudSimpleTable> </MudSimpleTable>
@if (_historyLoading)
{
<MudProgressLinear Indeterminate="true" Color="Color.Primary" Class="my-2" />
}
else if (_history.Any())
{
<MudTable T="ClubMembershipHistoryModel" Items="_history" Dense="true" Hover="true" Class="mt-2">
<HeaderContent>
<MudTh>عملیات</MudTh>
<MudTh>تاریخ</MudTh>
<MudTh>دلیل</MudTh>
<MudTh>توسط</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="عملیات">
<MudChip T="string" Size="Size.Small" Color="Color.Info">@context.Action</MudChip>
</MudTd>
<MudTd DataLabel="تاریخ">@context.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</MudTd>
<MudTd DataLabel="دلیل">@(string.IsNullOrEmpty(context.Reason) ? "-" : context.Reason)</MudTd>
<MudTd DataLabel="توسط">@(string.IsNullOrEmpty(context.PerformedBy) ? "-" : context.PerformedBy)</MudTd>
</RowTemplate>
</MudTable>
}
else
{
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="mt-2">تاریخچه‌ای یافت نشد.</MudText>
}
</MudPaper> </MudPaper>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text">
تاریخچه تغییرات عضویت در تب «گزارش تاریخچه» باشگاه قابل مشاهده است.
</MudAlert>
</MudStack> </MudStack>
} }
</DialogContent> </DialogContent>
@@ -126,39 +71,5 @@
@code { @code {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } [CascadingParameter] IMudDialogInstance MudDialog { get; set; }
[Parameter] public ClubMembershipModel Member { get; set; } [Parameter] public ClubMembershipModel Member { get; set; }
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubClient { get; set; }
private List<ClubMembershipHistoryModel> _history = new();
private bool _historyLoading;
protected override async Task OnInitializedAsync()
{
await LoadHistory();
}
private async Task LoadHistory()
{
_historyLoading = true;
try
{
var request = new GetClubMembershipHistoryRequest
{
UserId = Member.UserId,
PageIndex = 0,
PageSize = 50
};
var response = await ClubClient.GetClubMembershipHistoryAsync(request);
_history = response.Models?.ToList() ?? new();
}
catch
{
_history = new();
}
finally
{
_historyLoading = false;
}
}
private void Close() => MudDialog.Close(); private void Close() => MudDialog.Close();
} }
@@ -0,0 +1,153 @@
@using BackOffice.Common.BaseComponents
@using BackOffice.Pages.AutoComplete
@using CMSMicroservice.Protobuf.Protos.ClubMembershipCycle
@using Google.Protobuf.WellKnownTypes
@inject ClubMembershipCycleContract.ClubMembershipCycleContractClient CycleClient
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
گزارش دوره‌های عضویت باشگاه — هر خرید پکیج یک دور جدید ایجاد می‌کند.
</MudAlert>
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
<Filters>
<MudItem xs="12" sm="6" md="3">
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_userId" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="bool?" Clearable="true" Label="دوره جاری" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_isCurrent">
<MudSelectItem T="bool?" Value="@((bool?)true)">فقط دوره جاری</MudSelectItem>
<MudSelectItem T="bool?" Value="@((bool?)false)">دوره‌های قبلی</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="int?" Clearable="true" Label="وضعیت جادویی" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_magicState">
<MudSelectItem T="int?" Value="0">عادی</MudSelectItem>
<MudSelectItem T="int?" Value="1">در حالت جادویی</MudSelectItem>
<MudSelectItem T="int?" Value="2">جادویی تکمیل‌شده</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه خرید پکیج" />
</MudItem>
</Filters>
<Content>
<MudGrid Class="mb-3" Spacing="2">
<MudItem xs="4">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">کل دوره‌ها</MudText>
<MudText Typo="Typo.h6">@_totalCount.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="4">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">دوره جاری</MudText>
<MudText Typo="Typo.h6">@_currentCount.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="4">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">جادویی فعال</MudText>
<MudText Typo="Typo.h6">@_magicActive.ToString("N0")</MudText>
</MudPaper>
</MudItem>
</MudGrid>
<MudDataGrid @ref="_grid" T="ClubMembershipCycleModel" ServerData="LoadData" ReadOnly="true"
Hover="true" Dense="true" Height="calc(100vh - 420px)">
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<PropertyColumn Property="x => x.UserName" Title="کاربر" />
<PropertyColumn Property="x => x.CycleNumber" Title="شماره دور" />
<PropertyColumn Property="x => x.PackageName" Title="پکیج" />
<TemplateColumn Title="مبلغ (ریال)">
<CellTemplate>@context.Item.PackageAmount.ToString("N0")</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="روش خرید">
<CellTemplate>@ReportLabels.GetPurchaseMethodLabel(context.Item.PurchaseMethod)</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="تاریخ خرید">
<CellTemplate>@context.Item.PackagePurchasedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="وضعیت جادویی">
<CellTemplate>
@ReportLabels.GetMagicStateLabel(null,
context.Item.MagicStartedAt?.ToDateTime(),
context.Item.MagicCompletedAt?.ToDateTime())
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="دوره جاری">
<CellTemplate>
<MudChip T="string" Size="Size.Small" Color="@(context.Item.IsCurrentCycle ? Color.Success : Color.Default)">
@(context.Item.IsCurrentCycle ? "بله" : "خیر")
</MudChip>
</CellTemplate>
</TemplateColumn>
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="ClubMembershipCycleModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد در صفحه" />
</PagerContent>
</MudDataGrid>
</Content>
</BasePageComponent>
@code {
private BasePageComponent? _basePage;
private MudDataGrid<ClubMembershipCycleModel>? _grid;
private long? _userId;
private bool? _isCurrent;
private int? _magicState;
private DateTime? _fromDate, _toDate;
private long _totalCount, _currentCount, _magicActive;
private GetAllClubMembershipCycleByFilterFilter BuildFilter() => new()
{
UserId = _userId > 0 ? _userId : null,
IsCurrentCycle = _isCurrent,
MagicState = _magicState,
PackagePurchasedFrom = _fromDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_fromDate.Value, DateTimeKind.Utc)) : null,
PackagePurchasedTo = _toDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_toDate.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc)) : null
};
private async Task<GridData<ClubMembershipCycleModel>> LoadData(GridState<ClubMembershipCycleModel> state)
{
try
{
var filter = BuildFilter();
var summary = await CycleClient.GetClubMembershipCycleSummaryAsync(new GetClubMembershipCycleSummaryRequest { Filter = filter });
_totalCount = summary.TotalCount;
_currentCount = summary.CurrentCycleCount;
_magicActive = summary.MagicActiveCount;
var result = await CycleClient.GetAllClubMembershipCycleByFilterAsync(new GetAllClubMembershipCycleByFilterRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize },
Filter = filter
});
return new GridData<ClubMembershipCycleModel>
{
Items = result?.Models?.ToList() ?? new(),
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
return new GridData<ClubMembershipCycleModel>();
}
}
private async Task OnFilterSubmit() { if (_grid != null) await _grid.ReloadServerData(); }
private async Task OnFilterCleared()
{
_userId = null; _isCurrent = null; _magicState = null; _fromDate = _toDate = null;
if (_grid != null) await _grid.ReloadServerData();
}
}
@@ -0,0 +1,153 @@
@using BackOffice.Common.BaseComponents
@using BackOffice.Pages.AutoComplete
@using CMSMicroservice.Protobuf.Protos.ClubMembership
@using CMSMicroservice.Protobuf.Protos.ClubMembershipCycleHistory
@using Google.Protobuf.WellKnownTypes
@inject ClubMembershipContract.ClubMembershipContractClient ClubClient
@inject ClubMembershipCycleHistoryContract.ClubMembershipCycleHistoryContractClient CycleHistoryClient
<MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true" PanelClass="pt-4">
<MudTabPanel Text="تغییرات عضویت" Icon="@Icons.Material.Filled.History">
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
تاریخچه فعال/غیرفعال شدن عضویت باشگاه.
</MudAlert>
<MudStack Row="true" Spacing="2" Class="mb-3">
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_memUserId" />
<MudSelect T="int?" Clearable="true" Label="عملیات" Variant="Variant.Outlined" Margin="Margin.Dense"
@bind-Value="_memAction" Style="max-width:200px;">
<MudSelectItem T="int?" Value="0">فعال‌سازی</MudSelectItem>
<MudSelectItem T="int?" Value="1">غیرفعال‌سازی</MudSelectItem>
<MudSelectItem T="int?" Value="2">ویرایش</MudSelectItem>
<MudSelectItem T="int?" Value="3">اصلاح دستی</MudSelectItem>
</MudSelect>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ReloadMembership">جستجو</MudButton>
</MudStack>
<MudDataGrid @ref="_memGrid" T="ClubMembershipHistoryModel" ServerData="LoadMembershipHistory" ReadOnly="true"
Hover="true" Dense="true" Height="calc(100vh - 400px)">
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<PropertyColumn Property="x => x.UserName" Title="کاربر" />
<TemplateColumn Title="وضعیت">
<CellTemplate>@ReportLabels.FormatActiveChange(context.Item.OldIsActive, context.Item.NewIsActive)</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="مشارکت اولیه">
<CellTemplate>
@(context.Item.OldInitialContribution != null ? context.Item.OldInitialContribution.Value.ToString("N0") : "—")
→ @(context.Item.NewInitialContribution != null ? context.Item.NewInitialContribution.Value.ToString("N0") : "—")
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="عملیات">
<CellTemplate>@ReportLabels.GetClubMembershipActionLabel(context.Item.Action)</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.PerformedBy" Title="انجام‌دهنده" />
<PropertyColumn Property="x => x.Reason" Title="دلیل" />
<TemplateColumn Title="تاریخ">
<CellTemplate>@context.Item.CreatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
</TemplateColumn>
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="ClubMembershipHistoryModel" PageSizeOptions="@(new int[] { 20, 50, 100 })" />
</PagerContent>
</MudDataGrid>
</MudTabPanel>
<MudTabPanel Text="تغییرات دوره" Icon="@Icons.Material.Filled.Timeline">
<MudAlert Severity="Severity.Warning" Variant="Variant.Text" Dense="true" Class="mb-3">
ردیابی تغییرات دوره از زمان deploy فعال شد. رکوردهای قدیمی‌تر ممکن است خالی باشند.
</MudAlert>
<MudStack Row="true" Spacing="2" Class="mb-3">
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_cycleUserId" />
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ReloadCycle">جستجو</MudButton>
</MudStack>
<MudDataGrid @ref="_cycleGrid" T="ClubMembershipCycleHistoryModel" ServerData="LoadCycleHistory" ReadOnly="true"
Hover="true" Dense="true" Height="calc(100vh - 400px)">
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<PropertyColumn Property="x => x.UserName" Title="کاربر" />
<PropertyColumn Property="x => x.CycleNumber" Title="دور" />
<TemplateColumn Title="دوره جاری">
<CellTemplate>@ReportLabels.FormatBoolChange(context.Item.OldIsCurrentCycle, context.Item.NewIsCurrentCycle)</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="عملیات">
<CellTemplate>@ReportLabels.GetClubCycleActionLabel(context.Item.Action)</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.PerformedBy" Title="انجام‌دهنده" />
<PropertyColumn Property="x => x.Reason" Title="دلیل" />
<TemplateColumn Title="تاریخ">
<CellTemplate>@context.Item.CreatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
</TemplateColumn>
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="ClubMembershipCycleHistoryModel" PageSizeOptions="@(new int[] { 20, 50, 100 })" />
</PagerContent>
</MudDataGrid>
</MudTabPanel>
</MudTabs>
@code {
private MudDataGrid<ClubMembershipHistoryModel>? _memGrid;
private MudDataGrid<ClubMembershipCycleHistoryModel>? _cycleGrid;
private long? _memUserId, _cycleUserId;
private int? _memAction;
private async Task<GridData<ClubMembershipHistoryModel>> LoadMembershipHistory(GridState<ClubMembershipHistoryModel> state)
{
try
{
var result = await ClubClient.GetClubMembershipHistoryAsync(new GetClubMembershipHistoryRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize },
Filter = new GetClubMembershipHistoryFilter
{
UserId = _memUserId > 0 ? _memUserId : null,
Action = _memAction
}
});
return new GridData<ClubMembershipHistoryModel>
{
Items = result?.Models?.ToList() ?? new(),
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
return new GridData<ClubMembershipHistoryModel>();
}
}
private async Task<GridData<ClubMembershipCycleHistoryModel>> LoadCycleHistory(GridState<ClubMembershipCycleHistoryModel> state)
{
try
{
var result = await CycleHistoryClient.GetAllClubMembershipCycleHistoryByFilterAsync(new GetAllClubMembershipCycleHistoryByFilterRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize },
Filter = new GetAllClubMembershipCycleHistoryByFilterFilter
{
UserId = _cycleUserId > 0 ? _cycleUserId : null
}
});
return new GridData<ClubMembershipCycleHistoryModel>
{
Items = result?.Models?.ToList() ?? new(),
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
return new GridData<ClubMembershipCycleHistoryModel>();
}
}
private async Task ReloadMembership() { if (_memGrid != null) await _memGrid.ReloadServerData(); }
private async Task ReloadCycle() { if (_cycleGrid != null) await _cycleGrid.ReloadServerData(); }
}
@@ -0,0 +1,131 @@
@using BackOffice.Common.BaseComponents
@using BackOffice.Pages.AutoComplete
@using CMSMicroservice.Protobuf.Protos.UserPackagePurchase
@using Google.Protobuf.WellKnownTypes
@inject UserPackagePurchaseContract.UserPackagePurchaseContractClient PurchaseClient
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
گزارش read-only خریدهای پکیج — فقط مشاهده و فیلتر.
</MudAlert>
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
<Filters>
<MudItem xs="12" sm="6" md="3">
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_userId" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="int?" Clearable="true" Label="روش خرید" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_purchaseMethod">
<MudSelectItem T="int?" Value="1">دایا</MudSelectItem>
<MudSelectItem T="int?" Value="3">دستی</MudSelectItem>
<MudSelectItem T="int?" Value="2">درگاه</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه خرید" />
</MudItem>
</Filters>
<Content>
<MudGrid Class="mb-3" Spacing="2">
<MudItem xs="6" sm="4">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">تعداد</MudText>
<MudText Typo="Typo.h6">@_totalCount.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6" sm="4">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">جمع مبلغ (ریال)</MudText>
<MudText Typo="Typo.h6">@_totalAmount.ToString("N0")</MudText>
</MudPaper>
</MudItem>
</MudGrid>
<MudDataGrid @ref="_grid" T="UserPackagePurchaseModel" ServerData="LoadData" ReadOnly="true"
Hover="true" Dense="true" Height="calc(100vh - 420px)">
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<TemplateColumn Title="کاربر">
<CellTemplate>
<MudStack Spacing="0">
<MudText Typo="Typo.body2">@context.Item.UserName</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">@context.Item.UserMobile</MudText>
</MudStack>
</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.PackageName" Title="پکیج" />
<TemplateColumn Title="روش خرید">
<CellTemplate>@ReportLabels.GetPurchaseMethodLabel(context.Item.PurchaseMethod)</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="مبلغ (ریال)">
<CellTemplate>@context.Item.Amount.ToString("N0")</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="تاریخ خرید">
<CellTemplate>@context.Item.PurchasedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.OrderId" Title="OrderId" />
<PropertyColumn Property="x => x.TransactionId" Title="TransactionId" />
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="UserPackagePurchaseModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد در صفحه" />
</PagerContent>
</MudDataGrid>
</Content>
</BasePageComponent>
@code {
private BasePageComponent? _basePage;
private MudDataGrid<UserPackagePurchaseModel>? _grid;
private long? _userId;
private int? _purchaseMethod;
private DateTime? _fromDate, _toDate;
private long _totalCount, _totalAmount;
private GetAllUserPackagePurchaseByFilterFilter BuildFilter() => new()
{
UserId = _userId > 0 ? _userId : null,
PurchaseMethod = _purchaseMethod,
PurchasedFrom = _fromDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_fromDate.Value, DateTimeKind.Utc)) : null,
PurchasedTo = _toDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_toDate.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc)) : null
};
private async Task<GridData<UserPackagePurchaseModel>> LoadData(GridState<UserPackagePurchaseModel> state)
{
try
{
var filter = BuildFilter();
var summary = await PurchaseClient.GetUserPackagePurchaseSummaryAsync(new GetUserPackagePurchaseSummaryRequest { Filter = filter });
_totalCount = summary.TotalCount;
_totalAmount = summary.TotalAmount;
var result = await PurchaseClient.GetAllUserPackagePurchaseByFilterAsync(new GetAllUserPackagePurchaseByFilterRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize },
Filter = filter
});
return new GridData<UserPackagePurchaseModel>
{
Items = result?.Models?.ToList() ?? new(),
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
return new GridData<UserPackagePurchaseModel>();
}
}
private async Task OnFilterSubmit() { if (_grid != null) await _grid.ReloadServerData(); }
private async Task OnFilterCleared()
{
_userId = null; _purchaseMethod = null; _fromDate = _toDate = null;
if (_grid != null) await _grid.ReloadServerData();
}
}
@@ -0,0 +1,105 @@
@using CMSMicroservice.Protobuf.Protos.UserPackagePurchase
@using Google.Protobuf.WellKnownTypes
@using DateTimeConverterCL
@inject UserPackagePurchaseContract.UserPackagePurchaseContractClient PurchaseClient
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">
جزئیات خرید پکیج — @UserName
</MudText>
</TitleContent>
<DialogContent>
@if (_loading)
{
<MudProgressLinear Indeterminate="true" Color="Color.Primary" Class="my-4" />
}
else if (_items.Count == 0)
{
<MudAlert Severity="Severity.Info" Dense="true">خریدی برای این کاربر یافت نشد.</MudAlert>
}
else
{
<MudTable Items="_items" Dense="true" Hover="true" Breakpoint="Breakpoint.Sm">
<HeaderContent>
<MudTh>شناسه</MudTh>
<MudTh>پکیج</MudTh>
<MudTh>روش</MudTh>
<MudTh>مبلغ (ریال)</MudTh>
<MudTh>تاریخ خرید</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="شناسه">@context.Id</MudTd>
<MudTd DataLabel="پکیج">@context.PackageName</MudTd>
<MudTd DataLabel="روش">
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined"
Color="@GetMethodColor(context.PurchaseMethod)">
@ReportLabels.GetPurchaseMethodLabel(context.PurchaseMethod)
</MudChip>
</MudTd>
<MudTd DataLabel="مبلغ">@context.Amount.ToString("N0")</MudTd>
<MudTd DataLabel="تاریخ">
@(context.PurchasedAt != null
? context.PurchasedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()
: "-")
</MudTd>
</RowTemplate>
</MudTable>
}
</DialogContent>
<DialogActions>
<MudButton OnClick="Close" Variant="Variant.Text">بستن</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!;
[Parameter] public long UserId { get; set; }
[Parameter] public string UserName { get; set; } = string.Empty;
private bool _loading = true;
private List<UserPackagePurchaseModel> _items = new();
protected override async Task OnInitializedAsync()
{
try
{
var result = await PurchaseClient.GetAllUserPackagePurchaseByFilterAsync(
new GetAllUserPackagePurchaseByFilterRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = 1,
PageSize = 500
},
Filter = new GetAllUserPackagePurchaseByFilterFilter
{
UserId = UserId
}
});
_items = result?.Models?
.OrderBy(x => x.PurchasedAt?.ToDateTime() ?? DateTime.MinValue)
.ToList() ?? new();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری جزئیات: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
private static Color GetMethodColor(int method) => method switch
{
1 => Color.Info, // دایا
2 => Color.Primary, // درگاه
3 => Color.Secondary, // دستی
_ => Color.Default
};
private void Close() => MudDialog.Close();
}
@@ -0,0 +1,282 @@
@using System.Text
@using BackOffice.Common.BaseComponents
@using BackOffice.Common.Utilities
@using BackOffice.Pages.AutoComplete
@using BackOffice.Pages.Club.Reports.Components
@using CMSMicroservice.Protobuf.Protos.UserPackagePurchase
@using Google.Protobuf.WellKnownTypes
@using DateTimeConverterCL
@using Microsoft.JSInterop
@inject UserPackagePurchaseContract.UserPackagePurchaseContractClient PurchaseClient
@inject IDialogService DialogService
@inject IJSRuntime JsRuntime
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
گزارش تجمیعی مشتریان — اولین/آخرین پکیج، تفکیک دایا / دستی / درگاه. خروجی Excel مطابق فیلتر فعال است.
</MudAlert>
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
<Filters>
<MudItem xs="12" sm="6" md="3">
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_userId" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="int?" Clearable="true" Label="روش خرید" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_purchaseMethod">
<MudSelectItem T="int?" Value="1">دایا</MudSelectItem>
<MudSelectItem T="int?" Value="3">دستی</MudSelectItem>
<MudSelectItem T="int?" Value="2">درگاه</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه خرید" />
</MudItem>
</Filters>
<Content>
<MudStack Row="true" Justify="Justify.FlexEnd" Class="mb-3">
<MudButton Variant="Variant.Outlined" Color="Color.Success"
StartIcon="@Icons.Material.Filled.FileDownload"
OnClick="ExportToExcel" Disabled="_exporting">
خروجی Excel
</MudButton>
</MudStack>
<MudDataGrid @ref="_grid" T="CustomerPackagePurchaseRollupModel" ServerData="LoadData" ReadOnly="true"
Hover="true" Dense="true" Height="calc(100vh - 420px)">
<Columns>
<TemplateColumn Title="مشتری">
<CellTemplate>
<MudStack Spacing="0">
<MudText Typo="Typo.body2">@context.Item.UserName</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">@context.Item.UserMobile</MudText>
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="اولین پکیج">
<CellTemplate>
<MudStack Spacing="0">
<MudText Typo="Typo.body2">@context.Item.FirstPackageName</MudText>
<MudText Typo="Typo.caption">@context.Item.FirstAmount.ToString("N0") ریال</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">
@(context.Item.FirstPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-")
</MudText>
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="آخرین پکیج">
<CellTemplate>
<MudStack Spacing="0">
<MudText Typo="Typo.body2">@context.Item.LastPackageName</MudText>
<MudText Typo="Typo.caption">@context.Item.LastAmount.ToString("N0") ریال</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">
@(context.Item.LastPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-")
</MudText>
<MudChip T="string" Size="Size.Small" Class="mt-1" Variant="Variant.Filled"
Color="@GetMethodColor(context.Item.LastPurchaseMethod)">
@ReportLabels.GetPurchaseMethodLabel(context.Item.LastPurchaseMethod)
</MudChip>
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="تعداد / جمع">
<CellTemplate>
<MudStack Spacing="0">
<MudText>@context.Item.PurchaseCount خرید</MudText>
<MudText Typo="Typo.caption">@context.Item.TotalAmount.ToString("N0") ریال</MudText>
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="دایا">
<CellTemplate>
<MudText Typo="Typo.caption">@context.Item.DayaCount عدد</MudText>
<MudText Typo="Typo.caption">@context.Item.DayaAmount.ToString("N0")</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="دستی">
<CellTemplate>
<MudText Typo="Typo.caption">@context.Item.ManualCount عدد</MudText>
<MudText Typo="Typo.caption">@context.Item.ManualAmount.ToString("N0")</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="درگاه">
<CellTemplate>
<MudText Typo="Typo.caption">@context.Item.GatewayCount عدد</MudText>
<MudText Typo="Typo.caption">@context.Item.GatewayAmount.ToString("N0")</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="عملیات">
<CellTemplate>
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
OnClick="() => OpenDetails(context.Item)">
جزئیات
</MudButton>
</CellTemplate>
</TemplateColumn>
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="CustomerPackagePurchaseRollupModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد در صفحه" />
</PagerContent>
</MudDataGrid>
</Content>
</BasePageComponent>
@code {
private BasePageComponent? _basePage;
private MudDataGrid<CustomerPackagePurchaseRollupModel>? _grid;
private long? _userId;
private int? _purchaseMethod;
private DateTime? _fromDate, _toDate;
private bool _exporting;
private GetCustomerPackagePurchaseRollupFilter BuildFilter()
{
var filter = new GetCustomerPackagePurchaseRollupFilter();
if (_userId is > 0)
filter.UserId = _userId;
if (_purchaseMethod.HasValue)
filter.PurchaseMethod = _purchaseMethod.Value;
if (_fromDate.HasValue)
filter.PurchasedFrom = Timestamp.FromDateTime(DateTime.SpecifyKind(_fromDate.Value.Date, DateTimeKind.Utc));
if (_toDate.HasValue)
filter.PurchasedTo = Timestamp.FromDateTime(DateTime.SpecifyKind(_toDate.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc));
return filter;
}
private async Task<GridData<CustomerPackagePurchaseRollupModel>> LoadData(GridState<CustomerPackagePurchaseRollupModel> state)
{
try
{
var result = await PurchaseClient.GetCustomerPackagePurchaseRollupAsync(
new GetCustomerPackagePurchaseRollupRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = state.Page + 1,
PageSize = state.PageSize
},
Filter = BuildFilter()
});
return new GridData<CustomerPackagePurchaseRollupModel>
{
Items = result?.Models?.ToList() ?? new(),
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
return new GridData<CustomerPackagePurchaseRollupModel>();
}
}
private async Task OnFilterSubmit()
{
if (_grid != null) await _grid.ReloadServerData();
}
private async Task OnFilterCleared()
{
_userId = null;
_purchaseMethod = null;
_fromDate = _toDate = null;
if (_grid != null) await _grid.ReloadServerData();
}
private async Task OpenDetails(CustomerPackagePurchaseRollupModel row)
{
var parameters = new DialogParameters
{
{ nameof(CustomerPackagePurchaseDetailsDialog.UserId), row.UserId },
{ nameof(CustomerPackagePurchaseDetailsDialog.UserName), row.UserName }
};
var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true, CloseButton = true };
await DialogService.ShowAsync<CustomerPackagePurchaseDetailsDialog>("جزئیات خرید پکیج", parameters, options);
}
private async Task ExportToExcel()
{
_exporting = true;
try
{
// PageSize=0 → همهٔ ردیف‌های مطابق فیلتر، مستقل از صفحه‌بندی گرید
var result = await PurchaseClient.GetCustomerPackagePurchaseRollupAsync(
new GetCustomerPackagePurchaseRollupRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = 1,
PageSize = 0
},
Filter = BuildFilter()
});
var all = result?.Models?.ToList() ?? new();
if (all.Count == 0)
{
Snackbar.Add("داده‌ای برای خروجی با فیلتر فعلی وجود ندارد.", Severity.Info);
return;
}
var headers = new[]
{
"شناسه کاربر", "نام مشتری", "موبایل",
"اولین پکیج", "مبلغ اولین", "تاریخ اولین",
"آخرین پکیج", "مبلغ آخرین", "تاریخ آخرین", "روش آخرین",
"تعداد خرید", "جمع مبلغ",
"تعداد دایا", "مبلغ دایا",
"تعداد دستی", "مبلغ دستی",
"تعداد درگاه", "مبلغ درگاه"
};
var rows = all.Select(o => new object?[]
{
o.UserId,
o.UserName,
o.UserMobile,
o.FirstPackageName,
o.FirstAmount,
o.FirstPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-",
o.LastPackageName,
o.LastAmount,
o.LastPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-",
ReportLabels.GetPurchaseMethodLabel(o.LastPurchaseMethod),
o.PurchaseCount,
o.TotalAmount,
o.DayaCount,
o.DayaAmount,
o.ManualCount,
o.ManualAmount,
o.GatewayCount,
o.GatewayAmount
});
var filename = $"customer-packages-{DateTime.Now:yyyyMMddHHmmss}.xls";
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
CsvExportHelper.ToExcelXmlBase64("مشتریان پکیج", headers, rows));
Snackbar.Add($"خروجی Excel آماده شد ({all.Count} مشتری).", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
}
finally
{
_exporting = false;
}
}
private static Color GetMethodColor(int method) => method switch
{
1 => Color.Info,
2 => Color.Primary,
3 => Color.Secondary,
_ => Color.Default
};
}
@@ -41,11 +41,11 @@
</tr> </tr>
<tr> <tr>
<td><strong>ارزش هر بالانس:</strong></td> <td><strong>ارزش هر بالانس:</strong></td>
<td>@Payout.ValuePerBalance.ToString("N0") ریال</td> <td>@Payout.ValuePerBalance.ToString("N0") تومان</td>
</tr> </tr>
<tr> <tr>
<td><strong>مبلغ کل:</strong></td> <td><strong>مبلغ کل:</strong></td>
<td><strong>@Payout.TotalAmount.ToString("N0") ریال</strong></td> <td><strong>@Payout.TotalAmount.ToString("N0") تومان</strong></td>
</tr> </tr>
<tr> <tr>
<td><strong>وضعیت:</strong></td> <td><strong>وضعیت:</strong></td>
@@ -2,7 +2,7 @@
<DialogContent> <DialogContent>
<MudStack Spacing="3"> <MudStack Spacing="3">
<MudAlert Severity="Severity.Warning" Dense="true"> <MudAlert Severity="Severity.Warning" Dense="true">
درخواست برداشت @Amount.ToString("N0") ریال برای @UserName رد خواهد شد. درخواست برداشت @Amount.ToString("N0") تومان برای @UserName رد خواهد شد.
</MudAlert> </MudAlert>
<MudTextField @bind-Value="_reason" <MudTextField @bind-Value="_reason"
Label="دلیل رد درخواست" Label="دلیل رد درخواست"
@@ -27,7 +27,7 @@
</tr> </tr>
<tr> <tr>
<td><strong>مبلغ:</strong></td> <td><strong>مبلغ:</strong></td>
<td><MudText Color="Color.Primary"><strong>@Request.Amount.ToString("N0") ریال</strong></MudText></td> <td><MudText Color="Color.Primary"><strong>@Request.Amount.ToString("N0") تومان</strong></MudText></td>
</tr> </tr>
<tr> <tr>
<td><strong>روش برداشت:</strong></td> <td><strong>روش برداشت:</strong></td>
+232 -164
View File
@@ -5,116 +5,59 @@
@using MudBlazor @using MudBlazor
@using BackOffice.Pages.AutoComplete @using BackOffice.Pages.AutoComplete
<div> <MudStack Spacing="4">
<MudText Typo="Typo.h4" Class="mb-4">داشبورد کمیسیون</MudText>
@if (_isLoading) @* ── عنوان صفحه ── *@
{ <div>
<MudProgressCircular Color="Color.Primary" Indeterminate="true" /> <MudText Typo="Typo.h5" Class="mb-1">داشبورد کمیسیون</MudText>
}
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"> <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> </MudText>
</MudCardContent> </div>
</MudCard>
</MudItem>
</MudGrid>
<!-- Week Selector -->
<MudPaper Class="pa-4 mt-4" Elevation="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
<MudText Typo="Typo.h6">انتخاب هفته:</MudText>
@* ── فیلتر (اول از همه) ── *@
<MudPaper Elevation="1" Class="pa-4">
<MudGrid AlignItems="AlignItems.End" Spacing="3">
<MudItem xs="12" sm="5">
@if (_allWeeks.Any()) @if (_allWeeks.Any())
{ {
<MudSelect T="long" <MudSelect T="long"
Value="_currentWeekDefinitionId" Value="_currentWeekDefinitionId"
Label="شماره هفته" Label="هفته"
Variant="Variant.Outlined" Variant="Variant.Outlined"
Margin="Margin.Dense" Margin="Margin.Dense"
AnchorOrigin="Origin.BottomCenter" AnchorOrigin="Origin.BottomLeft"
Style="min-width: 400px;" TransformOrigin="Origin.TopLeft"
AdornmentIcon="@Icons.Material.Filled.CalendarToday"
Adornment="Adornment.Start"
ValueChanged="OnWeekChanged"> ValueChanged="OnWeekChanged">
@if (_availableWeeks?.CurrentWeek != null) @if (_availableWeeks?.CurrentWeek != null)
{ {
<MudSelectItem T="long" Value="@_availableWeeks.CurrentWeek.WeekDefinitionId"> <MudSelectItem T="long" Value="@_availableWeeks.CurrentWeek.WeekDefinitionId">
<MudText> <MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.Today" Size="Size.Small" Class="ml-2" /> <MudIcon Icon="@Icons.Material.Filled.Today" Size="Size.Small" Color="Color.Primary" />
@_availableWeeks.CurrentWeek.DisplayText <span>@_availableWeeks.CurrentWeek.DisplayText</span>
</MudText> <MudChip T="string" Size="Size.Small" Color="Color.Primary" Variant="Variant.Text">جاری</MudChip>
</MudStack>
</MudSelectItem> </MudSelectItem>
<MudDivider /> <MudDivider />
} }
@if (_availableWeeks?.CalculatedWeeks?.Any() == true) @if (_availableWeeks?.CalculatedWeeks?.Any() == true)
{ {
<MudListSubheader>هفته‌های محاسبه شده</MudListSubheader> <MudListSubheader>تسویه‌شده</MudListSubheader>
@foreach (var week in _availableWeeks.CalculatedWeeks) @foreach (var week in _availableWeeks.CalculatedWeeks)
{ {
<MudSelectItem T="long" Value="@week.WeekDefinitionId"> <MudSelectItem T="long" Value="@week.WeekDefinitionId">
<MudText> <MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" Class="ml-2" /> <MudIcon Icon="@Icons.Material.Filled.CheckCircle" Size="Size.Small" Color="Color.Success" />
@week.DisplayText <span>@week.DisplayText</span>
@if (week.TotalPoolAmount > 0) @if (week.TotalPoolAmount > 0)
{ {
<MudChip T="string" Size="Size.Small" Color="Color.Success" Class="mr-2"> <MudText Typo="Typo.caption" Color="Color.Secondary">(@week.TotalPoolAmount.ToString("N0") T)</MudText>
@week.TotalPoolAmount.ToString("N0") ریال
</MudChip>
} }
</MudText> </MudStack>
</MudSelectItem> </MudSelectItem>
} }
<MudDivider /> <MudDivider />
@@ -122,14 +65,14 @@
@if (_availableWeeks?.PendingWeeks?.Any() == true) @if (_availableWeeks?.PendingWeeks?.Any() == true)
{ {
<MudListSubheader>هفته‌های محاسبه نشده</MudListSubheader> <MudListSubheader>در انتظار تسویه</MudListSubheader>
@foreach (var week in _availableWeeks.PendingWeeks) @foreach (var week in _availableWeeks.PendingWeeks)
{ {
<MudSelectItem T="long" Value="@week.WeekDefinitionId"> <MudSelectItem T="long" Value="@week.WeekDefinitionId">
<MudText> <MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.AccessTime" Color="Color.Warning" Size="Size.Small" Class="ml-2" /> <MudIcon Icon="@Icons.Material.Filled.AccessTime" Size="Size.Small" Color="Color.Warning" />
@week.DisplayText <span>@week.DisplayText</span>
</MudText> </MudStack>
</MudSelectItem> </MudSelectItem>
} }
<MudDivider /> <MudDivider />
@@ -141,10 +84,10 @@
@foreach (var week in _availableWeeks.FutureWeeks) @foreach (var week in _availableWeeks.FutureWeeks)
{ {
<MudSelectItem T="long" Value="@week.WeekDefinitionId"> <MudSelectItem T="long" Value="@week.WeekDefinitionId">
<MudText> <MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.CalendarMonth" Color="Color.Info" Size="Size.Small" Class="ml-2" /> <MudIcon Icon="@Icons.Material.Filled.CalendarMonth" Size="Size.Small" Color="Color.Tertiary" />
@week.DisplayText <span>@week.DisplayText</span>
</MudText> </MudStack>
</MudSelectItem> </MudSelectItem>
} }
} }
@@ -154,97 +97,222 @@
{ {
<MudText Color="Color.Warning">لیست هفته‌ها بارگذاری نشده</MudText> <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" <MudButton Variant="Variant.Filled"
Color="Color.Primary" Color="Color.Primary"
FullWidth="true"
OnClick="LoadPoolData" OnClick="LoadPoolData"
StartIcon="@Icons.Material.Filled.Refresh"> StartIcon="@Icons.Material.Filled.Search"
بارگذاری Disabled="_isLoading">
نمایش
</MudButton> </MudButton>
</MudStack> </MudItem>
</MudGrid>
</MudPaper> </MudPaper>
<!-- Pool Details --> @* ── لودینگ ── *@
@if (_poolData != null) @if (_isLoading)
{ {
<MudPaper Class="pa-4 mt-4" Elevation="2"> <MudPaper Elevation="0" Class="pa-6 d-flex justify-center align-center">
<MudText Typo="Typo.h6" Class="mb-3">جزئیات استخر</MudText> <MudStack AlignItems="AlignItems.Center" Spacing="2">
<MudSimpleTable Hover="true" Dense="true"> <MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Medium" />
<tbody> <MudText Typo="Typo.body2" Color="Color.Secondary">در حال بارگذاری اطلاعات استخر…</MudText>
<tr> </MudStack>
<td><strong>شناسه استخر:</strong></td> </MudPaper>
<td>@_poolData.Id</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 else
{ {
<MudText Color="Color.Secondary">-</MudText> @* ── نوار وضعیت هفته / پکیج ── *@
} <MudPaper Elevation="0" Class="pa-3 mud-background-gray rounded">
</td> <MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Wrap="Wrap.Wrap">
</tr> <MudIcon Icon="@Icons.Material.Filled.DateRange" Color="Color.Default" Size="Size.Small" />
<tr> <MudText Typo="Typo.body1">@_currentWeekDisplayText</MudText>
<td><strong>تاریخ ایجاد:</strong></td>
<td>@_poolData.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td> @if (!string.IsNullOrEmpty(_poolData?.PackageTitle))
</tr> {
</tbody> <MudDivider Vertical="true" FlexItem="true" />
</MudSimpleTable> <MudChip T="string" Size="Size.Small" Color="Color.Primary" Icon="@Icons.Material.Filled.WorkspacePremium">
</MudPaper> @_poolData.PackageTitle
</MudChip>
} }
<!-- Quick Actions --> <MudSpacer />
<MudPaper Class="pa-4 mt-4" Elevation="2">
<MudText Typo="Typo.h6" Class="mb-3">عملیات سریع</MudText> @if (_poolData != null && _poolData.Id > 0)
<MudStack Row="true" Spacing="2"> {
@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" <MudButton Variant="Variant.Filled"
Color="Color.Primary" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Payments" StartIcon="@Icons.Material.Filled.Payments"
Href="/commission/payouts"> Href="/commission/payouts"
مشاهده پرداخت‌ها Size="Size.Medium">
</MudButton> پرداخت‌های این هفته
<MudButton Variant="Variant.Filled"
Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.AccountBalance"
Href="/commission/withdrawals">
درخواست‌های برداشت
</MudButton> </MudButton>
<MudButton Variant="Variant.Outlined" <MudButton Variant="Variant.Outlined"
Color="Color.Info" Color="Color.Default"
StartIcon="@Icons.Material.Filled.Calculate" StartIcon="@Icons.Material.Filled.AccountBalance"
Disabled="@(_poolData?.IsCalculated == true)" Href="/commission/withdrawals"
OnClick="TriggerCalculation"> Size="Size.Medium">
اجرای محاسبه دستی درخواست‌های برداشت
</MudButton> </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> </MudStack>
</MudPaper> </MudPaper>
} }
</div>
</MudStack>
@@ -22,6 +22,9 @@ public partial class Dashboard
private string _currentWeekDisplayText = string.Empty; // نمایش فارسی private string _currentWeekDisplayText = string.Empty; // نمایش فارسی
private GetWeeklyCommissionPoolResponse? _poolData; private GetWeeklyCommissionPoolResponse? _poolData;
// Package filter
private long? _selectedPackageId;
// Available weeks data // Available weeks data
private CMSMicroservice.Protobuf.Protos.Commission.GetAvailableWeeksResponse? _availableWeeks; private CMSMicroservice.Protobuf.Protos.Commission.GetAvailableWeeksResponse? _availableWeeks;
private List<CMSMicroservice.Protobuf.Protos.Commission.WeekInfo> _allWeeks = new(); private List<CMSMicroservice.Protobuf.Protos.Commission.WeekInfo> _allWeeks = new();
@@ -91,6 +94,12 @@ public partial class Dashboard
await LoadPoolData(); await LoadPoolData();
} }
private async Task OnPackageChanged(long? packageId)
{
_selectedPackageId = packageId;
await LoadPoolData();
}
private async Task LoadPoolData() private async Task LoadPoolData()
{ {
if (_currentWeekDefinitionId <= 0) if (_currentWeekDefinitionId <= 0)
@@ -106,7 +115,8 @@ public partial class Dashboard
var request = new GetWeeklyCommissionPoolRequest var request = new GetWeeklyCommissionPoolRequest
{ {
WeekDefinitionId = _currentWeekDefinitionId WeekDefinitionId = _currentWeekDefinitionId,
PackageId = _selectedPackageId
}; };
_poolData = await CommissionContract.GetWeeklyCommissionPoolAsync(request); _poolData = await CommissionContract.GetWeeklyCommissionPoolAsync(request);
@@ -126,24 +136,41 @@ public partial class Dashboard
private async Task TriggerCalculation() private async Task TriggerCalculation()
{ {
var packageInfo = _selectedPackageId.HasValue
? $" (فقط پکیج #{_selectedPackageId})"
: " (همه پکیج‌ها)";
var confirmed = await DialogService.ShowMessageBox( var confirmed = await DialogService.ShowMessageBox(
"تایید", "تایید",
$"آیا از اجرای محاسبه دستی برای هفته {_currentWeekDisplayName} مطمئن هستید؟", $"آیا از اجرای محاسبه دستی برای هفته {_currentWeekDisplayName}{packageInfo} مطمئن هستید؟",
yesText: "بله", cancelText: "خیر"); yesText: "بله", cancelText: "خیر");
if (confirmed == true) if (confirmed == true)
{ {
try try
{ {
await CommissionContract.TriggerWeeklyCalculationAsync(new TriggerWeeklyCalculationRequest() var req = new TriggerWeeklyCalculationRequest()
{ {
WeekDefinitionId = _currentWeekDefinitionId, WeekDefinitionId = _currentWeekDefinitionId,
ForceRecalculate = true, ForceRecalculate = true,
SkipBalances = false, SkipBalances = false,
SkipPayouts = false, SkipPayouts = false,
SkipPool = false, SkipPool = false,
}); };
Snackbar.Add("محاسبه با موفقیت آغاز شد. این عملیات ممکن است چند دقیقه طول بکشد.", Severity.Info);
req.PackageId = _selectedPackageId;
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 // Reload data after a delay
await Task.Delay(2000); await Task.Delay(2000);
@@ -19,4 +19,5 @@ public class WithdrawalRequestModel
public string? BankTrackingCode { get; set; } public string? BankTrackingCode { get; set; }
public string? PaymentFailureReason { get; set; } public string? PaymentFailureReason { get; set; }
public string? IbanNumber { get; set; } public string? IbanNumber { get; set; }
public string? PackageName { get; set; }
} }
@@ -9,6 +9,11 @@
<div> <div>
<MudText Typo="Typo.h4" Class="mb-4"> پرداخت های کاربران</MudText> <MudText Typo="Typo.h4" Class="mb-4"> پرداخت های کاربران</MudText>
@* ── G11b: راهنمای پرداخت‌ها (Q28) ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
پاداش‌ها بر اساس هر پکیج جداگانه محاسبه می‌شود. ستون «پکیج» نشان‌دهنده پکیج منبع پاداش است.
</MudAlert>
<MudDataGrid T="UserCommissionPayoutModel" <MudDataGrid T="UserCommissionPayoutModel"
ServerData="@(new Func<GridState<UserCommissionPayoutModel>, Task<GridData<UserCommissionPayoutModel>>>(ServerReload))" ServerData="@(new Func<GridState<UserCommissionPayoutModel>, Task<GridData<UserCommissionPayoutModel>>>(ServerReload))"
Hover="true" Hover="true"
@@ -36,12 +41,29 @@
<MudSelectItem T="int?" Value="@((int?)4)">شکست خورده</MudSelectItem> <MudSelectItem T="int?" Value="@((int?)4)">شکست خورده</MudSelectItem>
<MudSelectItem T="int?" Value="@((int?)5)">لغو شده</MudSelectItem> <MudSelectItem T="int?" Value="@((int?)5)">لغو شده</MudSelectItem>
</MudSelect> </MudSelect>
<PackageSelect Label="پکیج"
@bind-SelectedPackageId="_filterPackageId" />
<MudButton Variant="Variant.Filled" <MudButton Variant="Variant.Filled"
Color="Color.Primary" Color="Color.Primary"
OnClick="ApplyFilter" OnClick="ApplyFilter"
StartIcon="@Icons.Material.Filled.Search"> StartIcon="@Icons.Material.Filled.Search">
جستجو جستجو
</MudButton> </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> </MudStack>
</ToolBarContent> </ToolBarContent>
<Columns> <Columns>
@@ -64,6 +86,14 @@
</CellTemplate> </CellTemplate>
</PropertyColumn> </PropertyColumn>
<PropertyColumn Property="x => x.PackageTitle" Title="پکیج">
<CellTemplate>
<MudChip T="string" Color="Color.Tertiary" Size="Size.Small">
@(string.IsNullOrEmpty(context.Item.PackageTitle) ? "-" : context.Item.PackageTitle)
</MudChip>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.BalancesEarned" Title="بالانس‌ها"> <PropertyColumn Property="x => x.BalancesEarned" Title="بالانس‌ها">
<CellTemplate> <CellTemplate>
<MudChip T="string" Color="Color.Info" Size="Size.Small"> <MudChip T="string" Color="Color.Info" Size="Size.Small">
@@ -17,6 +17,8 @@ public partial class UserPayouts
private long? _filterUserId; private long? _filterUserId;
private long? _filterWeekDefinitionId; private long? _filterWeekDefinitionId;
private int? _filterStatus; private int? _filterStatus;
private long? _filterPackageId;
private bool _isCreditingWallets;
private async Task<GridData<UserCommissionPayoutModel>> ServerReload(GridState<UserCommissionPayoutModel> state) private async Task<GridData<UserCommissionPayoutModel>> ServerReload(GridState<UserCommissionPayoutModel> state)
{ {
@@ -44,6 +46,11 @@ public partial class UserPayouts
request.Status = _filterStatus.Value; request.Status = _filterStatus.Value;
} }
if (_filterPackageId.HasValue && _filterPackageId.Value > 0)
{
request.PackageId = _filterPackageId.Value;
}
var result = await CommissionContract.GetUserCommissionPayoutsAsync(request); var result = await CommissionContract.GetUserCommissionPayoutsAsync(request);
if (result?.Models != null && result.Models.Any()) if (result?.Models != null && result.Models.Any())
@@ -119,7 +126,7 @@ public partial class UserPayouts
{ {
var confirmed = await DialogService.ShowMessageBox( var confirmed = await DialogService.ShowMessageBox(
"تایید برداشت", "تایید برداشت",
$"آیا از تایید برداشت برای کاربر {payout.UserName} (مبلغ: {payout.TotalAmount:N0} ریال) مطمئن هستید؟", $"آیا از تایید برداشت برای کاربر {payout.UserName} (مبلغ: {payout.TotalAmount:N0} تومان) مطمئن هستید؟",
yesText: "تایید", cancelText: "لغو"); yesText: "تایید", cancelText: "لغو");
if (confirmed == true) if (confirmed == true)
@@ -141,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) private async Task RejectWithdrawal(UserCommissionPayoutModel payout)
{ {
var reason = await DialogService.ShowMessageBox( var reason = await DialogService.ShowMessageBox(
@@ -67,13 +67,13 @@
<PropertyColumn Property="x => x.WeekDisplayName" Title="هفته" /> <PropertyColumn Property="x => x.WeekDisplayName" Title="هفته" />
<PropertyColumn Property="x => x.TotalPoolAmount" Title="مبلغ کل استخر" Format="N0"> <PropertyColumn Property="x => x.TotalPoolAmount" Title="مبلغ کل استخر" Format="N0">
<CellTemplate> <CellTemplate>
<MudText>@context.Item.TotalPoolAmount.ToString("N0") ریال</MudText> <MudText>@context.Item.TotalPoolAmount.ToString("N0") تومان</MudText>
</CellTemplate> </CellTemplate>
</PropertyColumn> </PropertyColumn>
<PropertyColumn Property="x => x.TotalBalances" Title="مجموع موجودی‌ها" /> <PropertyColumn Property="x => x.TotalBalances" Title="مجموع موجودی‌ها" />
<PropertyColumn Property="x => x.ValuePerBalance" Title="ارزش هر موجودی" Format="N0"> <PropertyColumn Property="x => x.ValuePerBalance" Title="ارزش هر موجودی" Format="N0">
<CellTemplate> <CellTemplate>
<MudText>@context.Item.ValuePerBalance.ToString("N0") ریال</MudText> <MudText>@context.Item.ValuePerBalance.ToString("N0") تومان</MudText>
</CellTemplate> </CellTemplate>
</PropertyColumn> </PropertyColumn>
<PropertyColumn Property="x => x.IsCalculated" Title="وضعیت"> <PropertyColumn Property="x => x.IsCalculated" Title="وضعیت">
@@ -128,7 +128,7 @@
<MudPaper Elevation="0" Class="pa-4 text-center"> <MudPaper Elevation="0" Class="pa-4 text-center">
<MudText Typo="Typo.h6" Color="Color.Primary">مجموع استخرها</MudText> <MudText Typo="Typo.h6" Color="Color.Primary">مجموع استخرها</MudText>
<MudText Typo="Typo.h4">@_totalPoolSum.ToString("N0")</MudText> <MudText Typo="Typo.h4">@_totalPoolSum.ToString("N0")</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">ریال</MudText> <MudText Typo="Typo.caption" Color="Color.Secondary">تومان</MudText>
</MudPaper> </MudPaper>
</MudItem> </MudItem>
<MudItem xs="12" md="3"> <MudItem xs="12" md="3">
@@ -149,7 +149,7 @@
<MudPaper Elevation="0" Class="pa-4 text-center"> <MudPaper Elevation="0" Class="pa-4 text-center">
<MudText Typo="Typo.h6" Color="Color.Info">میانگین ارزش</MudText> <MudText Typo="Typo.h6" Color="Color.Info">میانگین ارزش</MudText>
<MudText Typo="Typo.h4">@_averageValue.ToString("N0")</MudText> <MudText Typo="Typo.h4">@_averageValue.ToString("N0")</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">ریال</MudText> <MudText Typo="Typo.caption" Color="Color.Secondary">تومان</MudText>
</MudPaper> </MudPaper>
</MudItem> </MudItem>
</MudGrid> </MudGrid>
@@ -55,7 +55,7 @@
Margin="Margin.Dense"> Margin="Margin.Dense">
<MudSelectItem T="int?" Value="@(null as int?)">همه وضعیت‌ها</MudSelectItem> <MudSelectItem T="int?" Value="@(null as int?)">همه وضعیت‌ها</MudSelectItem>
<MudSelectItem T="int?" Value="0">در انتظار واریز به کیف پول</MudSelectItem> <MudSelectItem T="int?" Value="0">در انتظار واریز به کیف پول</MudSelectItem>
<MudSelectItem T="int?" Value="1">واریز شده به کیف پول طلایی</MudSelectItem> <MudSelectItem T="int?" Value="1">واریز شده به پاداش های دریافتی</MudSelectItem>
<MudSelectItem T="int?" Value="2">درخواست برداشت ثبت شده</MudSelectItem> <MudSelectItem T="int?" Value="2">درخواست برداشت ثبت شده</MudSelectItem>
<MudSelectItem T="int?" Value="3">برداشت انجام شده</MudSelectItem> <MudSelectItem T="int?" Value="3">برداشت انجام شده</MudSelectItem>
<MudSelectItem T="int?" Value="4">خطا در پرداخت بانکی</MudSelectItem> <MudSelectItem T="int?" Value="4">خطا در پرداخت بانکی</MudSelectItem>
@@ -107,19 +107,19 @@
<MudItem xs="12" md="3"> <MudItem xs="12" md="3">
<MudPaper Elevation="1" Class="pa-4 text-center"> <MudPaper Elevation="1" Class="pa-4 text-center">
<MudText Typo="Typo.subtitle2" Color="Color.Primary">مجموع مبلغ برداشت‌ها</MudText> <MudText Typo="Typo.subtitle2" Color="Color.Primary">مجموع مبلغ برداشت‌ها</MudText>
<MudText Typo="Typo.h5">@_summary.TotalAmount.ToString("N0") ریال</MudText> <MudText Typo="Typo.h5">@_summary.TotalAmount.ToString("N0") تومان</MudText>
</MudPaper> </MudPaper>
</MudItem> </MudItem>
<MudItem xs="12" md="3"> <MudItem xs="12" md="3">
<MudPaper Elevation="1" Class="pa-4 text-center"> <MudPaper Elevation="1" Class="pa-4 text-center">
<MudText Typo="Typo.subtitle2" Color="Color.Success">مبلغ پرداخت شده</MudText> <MudText Typo="Typo.subtitle2" Color="Color.Success">مبلغ پرداخت شده</MudText>
<MudText Typo="Typo.h5">@_summary.TotalPaid.ToString("N0") ریال</MudText> <MudText Typo="Typo.h5">@_summary.TotalPaid.ToString("N0") تومان</MudText>
</MudPaper> </MudPaper>
</MudItem> </MudItem>
<MudItem xs="12" md="3"> <MudItem xs="12" md="3">
<MudPaper Elevation="1" Class="pa-4 text-center"> <MudPaper Elevation="1" Class="pa-4 text-center">
<MudText Typo="Typo.subtitle2" Color="Color.Warning">مبلغ در انتظار</MudText> <MudText Typo="Typo.subtitle2" Color="Color.Warning">مبلغ در انتظار</MudText>
<MudText Typo="Typo.h5">@_summary.TotalPending.ToString("N0") ریال</MudText> <MudText Typo="Typo.h5">@_summary.TotalPending.ToString("N0") تومان</MudText>
</MudPaper> </MudPaper>
</MudItem> </MudItem>
<MudItem xs="12" md="3"> <MudItem xs="12" md="3">
@@ -189,17 +189,17 @@
<PropertyColumn Property="x => x.PendingCount" Title="در انتظار" /> <PropertyColumn Property="x => x.PendingCount" Title="در انتظار" />
<PropertyColumn Property="x => x.TotalAmount" Title="مبلغ کل" Format="N0"> <PropertyColumn Property="x => x.TotalAmount" Title="مبلغ کل" Format="N0">
<CellTemplate> <CellTemplate>
@context.Item.TotalAmount.ToString("N0") ریال @context.Item.TotalAmount.ToString("N0") تومان
</CellTemplate> </CellTemplate>
</PropertyColumn> </PropertyColumn>
<PropertyColumn Property="x => x.PaidAmount" Title="پرداخت شده" Format="N0"> <PropertyColumn Property="x => x.PaidAmount" Title="پرداخت شده" Format="N0">
<CellTemplate> <CellTemplate>
@context.Item.PaidAmount.ToString("N0") ریال @context.Item.PaidAmount.ToString("N0") تومان
</CellTemplate> </CellTemplate>
</PropertyColumn> </PropertyColumn>
<PropertyColumn Property="x => x.PendingAmount" Title="در انتظار پرداخت" Format="N0"> <PropertyColumn Property="x => x.PendingAmount" Title="در انتظار پرداخت" Format="N0">
<CellTemplate> <CellTemplate>
@context.Item.PendingAmount.ToString("N0") ریال @context.Item.PendingAmount.ToString("N0") تومان
</CellTemplate> </CellTemplate>
</PropertyColumn> </PropertyColumn>
</Columns> </Columns>
@@ -431,9 +431,9 @@
sb.AppendLine("خلاصه:"); sb.AppendLine("خلاصه:");
sb.AppendLine($"تعداد کل درخواست‌ها: {_summary.TotalRequests}"); sb.AppendLine($"تعداد کل درخواست‌ها: {_summary.TotalRequests}");
sb.AppendLine($"مبلغ کل برداشت‌ها: {_summary.TotalAmount:N0} ریال"); sb.AppendLine($"مبلغ کل برداشت‌ها: {_summary.TotalAmount:N0} تومان");
sb.AppendLine($"مبلغ پرداخت شده: {_summary.TotalPaid:N0} ریال"); sb.AppendLine($"مبلغ پرداخت شده: {_summary.TotalPaid:N0} تومان");
sb.AppendLine($"مبلغ در انتظار: {_summary.TotalPending:N0} ریال"); sb.AppendLine($"مبلغ در انتظار: {_summary.TotalPending:N0} تومان");
sb.AppendLine($"تعداد کاربران یکتا: {_summary.UniqueUsers}"); sb.AppendLine($"تعداد کاربران یکتا: {_summary.UniqueUsers}");
sb.AppendLine($"نرخ موفقیت: {_summary.SuccessRate:N2}%"); sb.AppendLine($"نرخ موفقیت: {_summary.SuccessRate:N2}%");
sb.AppendLine(); sb.AppendLine();
@@ -69,7 +69,7 @@
<PropertyColumn Property="x => x.Amount" Title="مبلغ"> <PropertyColumn Property="x => x.Amount" Title="مبلغ">
<CellTemplate> <CellTemplate>
<MudText Typo="Typo.body2" Color="Color.Primary"> <MudText Typo="Typo.body2" Color="Color.Primary">
<strong>@context.Item.Amount.ToString("N0") ریال</strong> <strong>@context.Item.Amount.ToString("N0") تومان</strong>
</MudText> </MudText>
</CellTemplate> </CellTemplate>
</PropertyColumn> </PropertyColumn>
@@ -87,7 +87,8 @@ public partial class WithdrawalRequests
BankReferenceId = grpcModel.BankReferenceId, BankReferenceId = grpcModel.BankReferenceId,
BankTrackingCode = grpcModel.BankTrackingCode, BankTrackingCode = grpcModel.BankTrackingCode,
PaymentFailureReason = grpcModel.PaymentFailureReason, PaymentFailureReason = grpcModel.PaymentFailureReason,
IbanNumber = grpcModel.IbanNumber IbanNumber = grpcModel.IbanNumber,
PackageName = grpcModel.PackageName
}; };
} }
@@ -163,7 +164,7 @@ public partial class WithdrawalRequests
{ {
var confirmed = await DialogService.ShowMessageBox( var confirmed = await DialogService.ShowMessageBox(
"تایید درخواست", "تایید درخواست",
$"آیا از تایید درخواست برداشت {request.Amount:N0} ریال برای {request.UserName} مطمئن هستید؟", $"آیا از تایید درخواست برداشت {request.Amount:N0} تومان برای {request.UserName} مطمئن هستید؟",
yesText: "تایید", cancelText: "لغو"); yesText: "تایید", cancelText: "لغو");
if (confirmed == true) if (confirmed == true)
@@ -222,7 +223,7 @@ public partial class WithdrawalRequests
{ {
var confirmed = await DialogService.ShowMessageBox( var confirmed = await DialogService.ShowMessageBox(
"پردازش پرداخت", "پردازش پرداخت",
$"آیا پرداخت {request.Amount:N0} ریال به {request.UserName} انجام شده است؟", $"آیا پرداخت {request.Amount:N0} تومان به {request.UserName} انجام شده است؟",
yesText: "بله، پردازش شد", cancelText: "لغو"); yesText: "بله، پردازش شد", cancelText: "لغو");
if (confirmed == true) if (confirmed == true)
@@ -269,7 +270,7 @@ public partial class WithdrawalRequests
} }
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.AppendLine("شناسه,کاربر,نام کاربر,مبلغ,وضعیت,روش,شماره شبا,تاریخ درخواست,تاریخ پردازش,شماره مرجع بانکی"); sb.AppendLine("شناسه,کاربر,نام کاربر,پکیج,مبلغ,وضعیت,روش,شماره شبا,تاریخ درخواست,تاریخ پردازش,شماره مرجع بانکی");
foreach (var r in result.Models) foreach (var r in result.Models)
{ {
@@ -278,6 +279,7 @@ public partial class WithdrawalRequests
mapped.Id, mapped.Id,
mapped.UserId, mapped.UserId,
EscapeCsv(mapped.UserName), EscapeCsv(mapped.UserName),
EscapeCsv(mapped.PackageName ?? "-"),
mapped.Amount, mapped.Amount,
EscapeCsv(GetStatusText(mapped.Status)), EscapeCsv(GetStatusText(mapped.Status)),
EscapeCsv(GetMethodText(mapped.Method)), EscapeCsv(GetMethodText(mapped.Method)),
@@ -32,10 +32,10 @@
<MudStack Spacing="1"> <MudStack Spacing="1">
<MudText Typo="Typo.body2" Color="Color.Secondary">مجموع فروش (۷ روز)</MudText> <MudText Typo="Typo.body2" Color="Color.Secondary">مجموع فروش (۷ روز)</MudText>
<MudText Typo="Typo.h5" Color="Color.Primary"> <MudText Typo="Typo.h5" Color="Color.Primary">
@_stats.TotalSalesLast7Days.ToString("N0") ریال @_stats.TotalSalesLast7Days.ToString("N0") تومان
</MudText> </MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary"> <MudText Typo="Typo.caption" Color="Color.Secondary">
امروز: @_stats.TodaySales.ToString("N0") ریال امروز: @_stats.TodaySales.ToString("N0") تومان
</MudText> </MudText>
</MudStack> </MudStack>
</MudItem> </MudItem>
@@ -43,7 +43,7 @@
<MudStack Spacing="1"> <MudStack Spacing="1">
<MudText Typo="Typo.body2" Color="Color.Secondary">میانگین مبلغ هر سفارش</MudText> <MudText Typo="Typo.body2" Color="Color.Secondary">میانگین مبلغ هر سفارش</MudText>
<MudText Typo="Typo.h5"> <MudText Typo="Typo.h5">
@_stats.AverageOrderAmount.ToString("N0") ریال @_stats.AverageOrderAmount.ToString("N0") تومان
</MudText> </MudText>
</MudStack> </MudStack>
</MudItem> </MudItem>
@@ -26,6 +26,7 @@
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Processing">در حال آماده‌سازی</MudSelectItem> <MudSelectItem T="OrderStatus" Value="@OrderStatus.Processing">در حال آماده‌سازی</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem> <MudSelectItem T="OrderStatus" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem> <MudSelectItem T="OrderStatus" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.ReadyForOfficePickup">آماده تحویل در دفتر</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem> <MudSelectItem T="OrderStatus" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem> <MudSelectItem T="OrderStatus" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
</MudSelect> </MudSelect>
@@ -55,6 +56,15 @@
</MudItem> </MudItem>
} }
@if (Model.Status == OrderStatus.ReadyForOfficePickup)
{
<MudItem xs="12">
<MudAlert Severity="Severity.Info">
<MudText>سفارش برای تحویل حضوری در دفتر آماده است.</MudText>
</MudAlert>
</MudItem>
}
@if (Model.Status == OrderStatus.Shipped) @if (Model.Status == OrderStatus.Shipped)
{ {
<MudItem xs="12"> <MudItem xs="12">
@@ -126,8 +136,9 @@
OrderStatus.Processing => Color.Primary, OrderStatus.Processing => Color.Primary,
OrderStatus.Shipped => Color.Secondary, OrderStatus.Shipped => Color.Secondary,
OrderStatus.Delivered => Color.Success, OrderStatus.Delivered => Color.Success,
OrderStatus.Cancelled => Color.Error, OrderStatus.ReadyForOfficePickup => Color.Primary,
OrderStatus.Returned => Color.Dark, OrderStatus.Cancelled => Color.Dark,
OrderStatus.Returned => Color.Error,
_ => Color.Default _ => Color.Default
}; };
} }
@@ -141,6 +152,7 @@
OrderStatus.Processing => "در حال آماده‌سازی", OrderStatus.Processing => "در حال آماده‌سازی",
OrderStatus.Shipped => "ارسال شده", OrderStatus.Shipped => "ارسال شده",
OrderStatus.Delivered => "تحویل داده شده", OrderStatus.Delivered => "تحویل داده شده",
OrderStatus.ReadyForOfficePickup => "آماده تحویل در دفتر",
OrderStatus.Cancelled => "لغو شده", OrderStatus.Cancelled => "لغو شده",
OrderStatus.Returned => "مرجوع شده", OrderStatus.Returned => "مرجوع شده",
_ => "نامشخص" _ => "نامشخص"
@@ -1,36 +1,72 @@
@using BackOffice.Services.DiscountOrder @using BackOffice.Services.DiscountOrder
@using BackOffice.Common.BaseComponents
<MudDialog> <MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.ShoppingCart" Class="ml-2" />
جزئیات سفارش #@Order.OrderNumber
</MudText>
</TitleContent>
<DialogContent> <DialogContent>
<MudGrid> <MudStack Spacing="2">
<!-- اطلاعات کاربر --> <MudText Typo="Typo.h6">جزئیات سفارش شماره @(string.IsNullOrWhiteSpace(Order.OrderNumber) ? Order.Id.ToString() : Order.OrderNumber)</MudText>
<MudItem xs="12"> <MudText Typo="Typo.body2">
<MudPaper Class="pa-4" Elevation="1"> کاربر:
<MudText Typo="Typo.h6" GutterBottom="true">اطلاعات سفارش</MudText> @if (!string.IsNullOrWhiteSpace(Order.UserFullName))
<MudGrid> {
<MudItem xs="6"> @Order.UserFullName
<MudText Typo="Typo.body2" Color="Color.Secondary">شماره سفارش:</MudText> }
<MudText Typo="Typo.body1"><strong>@Order.OrderNumber</strong></MudText> else
</MudItem> {
<MudItem xs="6"> <text>#@Order.UserId</text>
<MudText Typo="Typo.body2" Color="Color.Secondary">شناسه کاربر:</MudText> }
<MudText Typo="Typo.body1">@Order.UserId</MudText> @if (!string.IsNullOrWhiteSpace(DisplayMobile))
</MudItem> {
</MudGrid> <text> (@DisplayMobile)</text>
</MudPaper> }
</MudItem> </MudText>
<!-- Timeline وضعیت --> <MudPaper Class="pa-3">
<MudItem xs="12"> <MudStack Spacing="2">
<MudPaper Class="pa-4" Elevation="1"> <MudText Typo="Typo.subtitle2">خلاصه سفارش</MudText>
<MudText Typo="Typo.subtitle2" GutterBottom="true">Timeline وضعیت</MudText> <MudDivider />
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center" Justify="Justify.Center"> <MudText Typo="Typo.body2">مبلغ کل: @Order.TotalPrice.ToString("N0") تومان</MudText>
<MudText Typo="Typo.body2" Color="Color.Success">
از کیف پول اعتباری: @Order.DiscountBalanceUsed.ToString("N0") تومان
</MudText>
<MudText Typo="Typo.body2">
مبلغ پرداختی از درگاه: @Order.GatewayAmount.ToString("N0") تومان
</MudText>
@if (!string.IsNullOrEmpty(Order.TransactionId))
{
<MudText Typo="Typo.body2">شناسه پرداخت: @Order.TransactionId</MudText>
}
@if (Order.Created.HasValue)
{
<MudText Typo="Typo.body2">
تاریخ ثبت: @Order.Created.Value.MiladiToJalaliWithTime()
</MudText>
}
<MudText Typo="Typo.body2">
وضعیت پرداخت:
<MudChip T="string"
Color="@GetPaymentStatusColor(Order.PaymentStatusValue)"
Size="Size.Small"
Icon="@GetPaymentStatusIcon(Order.PaymentStatusValue)">
@GetPaymentStatusText(Order.PaymentStatusValue)
</MudChip>
</MudText>
@if (Order.VatAmount > 0)
{
<MudDivider Class="my-2" />
<MudText Typo="Typo.subtitle2">جزئیات مالیات بر ارزش افزوده</MudText>
<MudText Typo="Typo.body2">
مالیات: @Order.VatAmount.ToString("N0") تومان
</MudText>
<MudText Typo="Typo.body2">
مبلغ نهایی (شامل مالیات): @Order.TotalPrice.ToString("N0") تومان
</MudText>
}
<MudDivider Class="my-2" />
<MudText Typo="Typo.subtitle2">Timeline وضعیت</MudText>
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center">
@foreach (var step in _timelineSteps) @foreach (var step in _timelineSteps)
{ {
<MudStack Spacing="0" AlignItems="AlignItems.Center"> <MudStack Spacing="0" AlignItems="AlignItems.Center">
@@ -41,144 +77,10 @@
</MudStack> </MudStack>
} }
</MudStack> </MudStack>
</MudPaper>
</MudItem>
<!-- وضعیت سفارش --> <MudDivider Class="my-2" />
<MudItem xs="12" sm="6"> <MudText Typo="Typo.body2">
<MudPaper Class="pa-4" Elevation="1"> وضعیت ارسال:
<MudText Typo="Typo.h6" GutterBottom="true">وضعیت سفارش</MudText>
<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" <MudSelect T="OrderStatus"
Label="وضعیت ارسال" Label="وضعیت ارسال"
Dense="true" Dense="true"
@@ -189,18 +91,16 @@
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Processing">در حال آماده‌سازی</MudSelectItem> <MudSelectItem T="OrderStatus" Value="@OrderStatus.Processing">در حال آماده‌سازی</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem> <MudSelectItem T="OrderStatus" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem> <MudSelectItem T="OrderStatus" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.ReadyForOfficePickup">آماده تحویل در دفتر</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem> <MudSelectItem T="OrderStatus" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem> <MudSelectItem T="OrderStatus" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
</MudSelect> </MudSelect>
</MudItem> </MudText>
<MudItem xs="12" sm="6">
<MudTextField T="string" <MudTextField T="string"
Label="کد رهگیری" Label="کد رهگیری"
Variant="Variant.Outlined" Variant="Variant.Outlined"
Dense="true" Dense="true"
@bind-Value="_trackingCode" /> @bind-Value="_trackingCode" />
</MudItem>
<MudItem xs="12">
<MudTextField T="string" <MudTextField T="string"
Label="یادداشت ادمین" Label="یادداشت ادمین"
Variant="Variant.Outlined" Variant="Variant.Outlined"
@@ -208,51 +108,124 @@
Lines="3" Lines="3"
HelperText="دلیل تغییر وضعیت یا توضیحات اضافی" HelperText="دلیل تغییر وضعیت یا توضیحات اضافی"
@bind-Value="_adminNotes" /> @bind-Value="_adminNotes" />
</MudItem>
</MudGrid>
@if (_newStatus == OrderStatus.Cancelled || _newStatus == OrderStatus.Returned) @if (_newStatus == OrderStatus.Cancelled || _newStatus == OrderStatus.Returned)
{ {
<MudAlert Severity="Severity.Warning" Class="mt-2"> <MudAlert Severity="Severity.Warning" Dense="true">
توجه: در صورت لغو یا مرجوعی سفارش، موجودی محصولات به انبار بازگردانده می‌شود. توجه: در صورت لغو یا مرجوعی سفارش، موجودی محصولات به انبار بازگردانده می‌شود.
</MudAlert> </MudAlert>
} }
@if (_newStatus == OrderStatus.ReadyForOfficePickup)
{
<MudAlert Severity="Severity.Info" Dense="true">
سفارش برای تحویل حضوری در دفتر آماده است.
</MudAlert>
}
@if (_newStatus == OrderStatus.Shipped) @if (_newStatus == OrderStatus.Shipped)
{ {
<MudAlert Severity="Severity.Info" Class="mt-2"> <MudAlert Severity="Severity.Info" Dense="true">
پس از تغییر وضعیت به "ارسال شده"، اطلاع‌رسانی به کاربر ارسال خواهد شد. پس از تغییر وضعیت به «ارسال شده»، اطلاع‌رسانی به کاربر ارسال خواهد شد.
</MudAlert> </MudAlert>
} }
</MudStack>
</MudPaper> </MudPaper>
</MudItem>
<!-- یادداشت‌ها --> <MudPaper Class="pa-3">
@if (!string.IsNullOrEmpty(Order.Notes) || !string.IsNullOrEmpty(Order.AdminNotes)) <MudStack Spacing="1">
<MudText Typo="Typo.subtitle2">آدرس تحویل</MudText>
<MudDivider />
@if (Order.Address != null && !string.IsNullOrWhiteSpace(Order.Address.Address))
{ {
<MudItem xs="12"> @if (!string.IsNullOrWhiteSpace(Order.Address.Title))
<MudPaper Class="pa-4" Elevation="1"> {
<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)) @if (!string.IsNullOrEmpty(Order.Notes))
{ {
<MudText Typo="Typo.h6" GutterBottom="true"> <MudPaper Class="pa-3">
<MudIcon Icon="@Icons.Material.Filled.Comment" Size="Size.Small" /> <MudStack Spacing="1">
یادداشت کاربر <MudText Typo="Typo.subtitle2">یادداشت کاربر</MudText>
</MudText> <MudDivider />
<MudText Typo="Typo.body1" Class="mb-4">@Order.Notes</MudText> <MudText Typo="Typo.body2">@Order.Notes</MudText>
} </MudStack>
@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> </MudPaper>
</MudItem>
} }
</MudGrid> </MudStack>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<MudButton Variant="Variant.Outlined" Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.Print"
OnClick="PrintInvoiceAsync">
چاپ فاکتور
</MudButton>
<MudSpacer />
<MudButton Variant="Variant.Outlined" Color="Color.Default" OnClick="Close"> <MudButton Variant="Variant.Outlined" Color="Color.Default" OnClick="Close">
بستن بستن
</MudButton> </MudButton>
@@ -1,5 +1,7 @@
using BackOffice.Common.Utilities;
using BackOffice.Services.DiscountOrder; using BackOffice.Services.DiscountOrder;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor; using MudBlazor;
namespace BackOffice.Pages.DiscountShop.Components; namespace BackOffice.Pages.DiscountShop.Components;
@@ -8,6 +10,7 @@ public partial class OrderDetailsDialog
{ {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!; [CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!;
[Inject] public IDiscountOrderService OrderService { get; set; } = default!; [Inject] public IDiscountOrderService OrderService { get; set; } = default!;
[Inject] private IJSRuntime JsRuntime { get; set; } = default!;
[Parameter] public DiscountOrderDetailsDto Order { get; set; } = null!; [Parameter] public DiscountOrderDetailsDto Order { get; set; } = null!;
@@ -17,6 +20,9 @@ public partial class OrderDetailsDialog
private bool _isSaving; private bool _isSaving;
private List<TimelineStep> _timelineSteps = new(); private List<TimelineStep> _timelineSteps = new();
private string? DisplayMobile =>
!string.IsNullOrWhiteSpace(Order.UserMobile) ? Order.UserMobile : Order.Address?.Phone;
protected override void OnInitialized() protected override void OnInitialized()
{ {
_newStatus = Order.Status; _newStatus = Order.Status;
@@ -127,34 +133,69 @@ public partial class OrderDetailsDialog
private void Close() => MudDialog.Close(); private void Close() => MudDialog.Close();
private Color GetStatusColor(OrderStatus status) private Color GetPaymentStatusColor(int status) => status switch
{ {
return status switch 0 => Color.Warning,
{ 1 => Color.Success,
OrderStatus.Pending => Color.Warning, 2 => Color.Error,
OrderStatus.Paid => Color.Info, 3 => Color.Info,
OrderStatus.Processing => Color.Primary,
OrderStatus.Shipped => Color.Secondary,
OrderStatus.Delivered => Color.Success,
OrderStatus.Cancelled => Color.Error,
OrderStatus.Returned => Color.Dark,
_ => Color.Default _ => Color.Default
}; };
}
private string GetStatusText(OrderStatus status) private string GetPaymentStatusText(int status) => status switch
{ {
return status switch 0 => "در انتظار پرداخت",
{ 1 => "پرداخت شده",
OrderStatus.Pending => "در انتظار پرداخت", 2 => "ناموفق",
OrderStatus.Paid => "پرداخت شده", 3 => "بازپرداخت شده",
OrderStatus.Processing => "در حال آماده‌سازی",
OrderStatus.Shipped => "ارسال شده",
OrderStatus.Delivered => "تحویل داده شده",
OrderStatus.Cancelled => "لغو شده",
OrderStatus.Returned => "مرجوع شده",
_ => "نامشخص" _ => "نامشخص"
}; };
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
{
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 private enum StepState
@@ -70,7 +70,7 @@
Format="N0" Format="N0"
Immediate="true" Immediate="true"
Disabled="_loading" Disabled="_loading"
Label="قیمت (ریال) *" Label="قیمت (تومان) *"
Required="true" Required="true"
Variant="Variant.Outlined" Variant="Variant.Outlined"
Margin="Margin.Dense" /> Margin="Margin.Dense" />
@@ -20,14 +20,14 @@
<MudList T="string" Dense="true"> <MudList T="string" Dense="true">
<MudListItem T="string" <MudListItem T="string"
OnClick="@(() => OnTreeNodeSelected(null))" OnClick="@(() => OnTreeNodeSelected(null))"
Selected="@(_selectedParentId == null)"> Class="@(_selectedParentId == null ? "mud-selected-item" : "")">
<MudText Typo="Typo.body2">همه دسته‌بندی‌ها</MudText> <MudText Typo="Typo.body2">همه دسته‌بندی‌ها</MudText>
</MudListItem> </MudListItem>
@foreach (var node in _treeNodes) @foreach (var node in _treeNodes)
{ {
<MudListItem T="string" <MudListItem T="string"
OnClick="@(() => OnTreeNodeSelected(node.Id))" OnClick="@(() => OnTreeNodeSelected(node.Id))"
Selected="@(_selectedParentId == node.Id)"> Class="@(_selectedParentId == node.Id ? "mud-selected-item" : "")">
<MudStack Row="true" AlignItems="AlignItems.Center"> <MudStack Row="true" AlignItems="AlignItems.Center">
<div style="width:@(node.Depth * 16)px;"></div> <div style="width:@(node.Depth * 16)px;"></div>
<MudText Typo="Typo.body2">@node.Title</MudText> <MudText Typo="Typo.body2">@node.Title</MudText>
@@ -23,11 +23,11 @@
@bind-Value="@_trackingCodeFilter" Variant="Variant.Outlined" Margin="Margin.Dense"/> @bind-Value="@_trackingCodeFilter" Variant="Variant.Outlined" Margin="Margin.Dense"/>
</MudItem> </MudItem>
<MudItem xs="12" sm="6" md="3"> <MudItem xs="12" sm="6" md="3">
<MudDatePicker T="DateTime?" Label="از تاریخ" @bind-Date="_fromDateFilter" <MudDatePicker Label="از تاریخ" @bind-Date="_fromDateFilter"
Variant="Variant.Outlined" Margin="Margin.Dense"/> Variant="Variant.Outlined" Margin="Margin.Dense"/>
</MudItem> </MudItem>
<MudItem xs="12" sm="6" md="3"> <MudItem xs="12" sm="6" md="3">
<MudDatePicker T="DateTime?" Label="تا تاریخ" @bind-Date="_toDateFilter" <MudDatePicker Label="تا تاریخ" @bind-Date="_toDateFilter"
Variant="Variant.Outlined" Margin="Margin.Dense"/> Variant="Variant.Outlined" Margin="Margin.Dense"/>
</MudItem> </MudItem>
<MudItem xs="12" sm="6" md="3"> <MudItem xs="12" sm="6" md="3">
@@ -49,6 +49,8 @@
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem> <MudSelectItem T="OrderStatus?" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem> <MudSelectItem T="OrderStatus?" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem> <MudSelectItem T="OrderStatus?" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.ReadyForOfficePickup">آماده تحویل در دفتر</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
</MudSelect> </MudSelect>
</MudItem> </MudItem>
</Filters> </Filters>
@@ -97,20 +99,20 @@
<TemplateColumn Title="مبلغ کل"> <TemplateColumn Title="مبلغ کل">
<CellTemplate> <CellTemplate>
<MudText>@context.Item.TotalPrice.ToString("N0") ریال</MudText> <MudText>@context.Item.TotalPrice.ToString("N0") تومان</MudText>
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
<TemplateColumn Title="تخفیف کیفپول"> <TemplateColumn Title="کیف پول اعتباری">
<CellTemplate> <CellTemplate>
<MudText Color="Color.Success">@context.Item.DiscountBalanceUsed.ToString("N0") ریال</MudText> <MudText Color="Color.Success">@context.Item.DiscountBalanceUsed.ToString("N0") تومان</MudText>
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
<TemplateColumn Title="پرداخت درگاه"> <TemplateColumn Title="پرداخت درگاه">
<CellTemplate> <CellTemplate>
<MudText Typo="Typo.body2"> <MudText Typo="Typo.body2">
<strong>@context.Item.GatewayAmount.ToString("N0") ریال</strong> <strong>@context.Item.GatewayAmount.ToString("N0") تومان</strong>
</MudText> </MudText>
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
@@ -1,5 +1,6 @@
using System.Text; using System.Text;
using BackOffice.Common.BaseComponents; using BackOffice.Common.BaseComponents;
using BackOffice.Common.Utilities;
using BackOffice.Services.DiscountOrder; using BackOffice.Services.DiscountOrder;
using BackOffice.Pages.DiscountShop.Components; using BackOffice.Pages.DiscountShop.Components;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@@ -74,6 +75,15 @@ public partial class DiscountOrdersMainPage
return; return;
} }
// تکمیل فیلدهای فاکتور از ردیف لیست (جزئیات API نام/موبایل/VAT ندارد)
var listRow = _orders?.FirstOrDefault(o => o.Id == orderId);
if (listRow != null)
{
order.UserFullName = listRow.UserFullName;
order.UserMobile = listRow.UserMobile;
order.VatAmount = listRow.VatAmount;
}
var parameters = new DialogParameters<OrderDetailsDialog> { { x => x.Order, order } }; var parameters = new DialogParameters<OrderDetailsDialog> { { x => x.Order, order } };
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large, FullWidth = true }; var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large, FullWidth = true };
var dialog = await DialogService.ShowAsync<OrderDetailsDialog>("جزئیات سفارش", parameters, options); var dialog = await DialogService.ShowAsync<OrderDetailsDialog>("جزئیات سفارش", parameters, options);
@@ -178,8 +188,9 @@ public partial class DiscountOrdersMainPage
OrderStatus.Processing => Color.Primary, OrderStatus.Processing => Color.Primary,
OrderStatus.Shipped => Color.Secondary, OrderStatus.Shipped => Color.Secondary,
OrderStatus.Delivered => Color.Success, OrderStatus.Delivered => Color.Success,
OrderStatus.Cancelled => Color.Error, OrderStatus.Cancelled => Color.Dark,
OrderStatus.Returned => Color.Dark, OrderStatus.ReadyForOfficePickup => Color.Primary,
OrderStatus.Returned => Color.Error,
_ => Color.Default _ => Color.Default
}; };
} }
@@ -194,6 +205,7 @@ public partial class DiscountOrdersMainPage
OrderStatus.Shipped => "ارسال شده", OrderStatus.Shipped => "ارسال شده",
OrderStatus.Delivered => "تحویل داده شده", OrderStatus.Delivered => "تحویل داده شده",
OrderStatus.Cancelled => "لغو شده", OrderStatus.Cancelled => "لغو شده",
OrderStatus.ReadyForOfficePickup => "آماده تحویل در دفتر",
OrderStatus.Returned => "مرجوع شده", OrderStatus.Returned => "مرجوع شده",
_ => "نامشخص" _ => "نامشخص"
}; };
@@ -233,44 +245,36 @@ public partial class DiscountOrdersMainPage
try try
{ {
var sb = new StringBuilder(); var headers = new[]
sb.AppendLine("شناسه,نام کاربر,موبایل,مبلغ کل,تخفیف کیف‌پول,پرداخت درگاه,تعداد آیتم,وضعیت پرداخت,وضعیت ارسال,تاریخ پرداخت,تاریخ ثبت,آدرس"); {
"شناسه", "نام مشتری", "موبایل", "مبلغ کل", "کیف پول اعتباری", "پرداخت درگاه",
foreach (var o in _orders) "تعداد آیتم", "وضعیت پرداخت", "وضعیت ارسال", "تاریخ پرداخت", "تاریخ ثبت", "آدرس"
};
var rows = _orders.Select(o => new object?[]
{ {
sb.AppendLine(string.Join(",",
o.Id, o.Id,
EscapeCsv(o.UserFullName), CsvExportHelper.CustomerName(o.UserFullName, o.UserMobile, o.UserId),
EscapeCsv(o.UserMobile), o.UserMobile ?? "",
o.TotalPrice.ToString("N0"), o.TotalPrice,
o.DiscountBalanceUsed.ToString("N0"), o.DiscountBalanceUsed,
o.GatewayAmount.ToString("N0"), o.GatewayAmount,
o.ItemsCount, o.ItemsCount,
EscapeCsv(GetPaymentStatusText(o.PaymentStatusValue)), GetPaymentStatusText(o.PaymentStatusValue),
EscapeCsv(GetStatusText(o.Status)), GetStatusText(o.Status),
o.PaymentDate?.MiladiToJalaliWithTime() ?? "-", o.PaymentDate?.MiladiToJalaliWithTime() ?? "-",
o.Created?.MiladiToJalaliWithTime() ?? "-", o.Created?.MiladiToJalaliWithTime() ?? "-",
EscapeCsv(o.ShippingAddress ?? "-"))); o.ShippingAddress ?? "-"
} });
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray(); var filename = $"discount-orders-{DateTime.Now:yyyyMMddHHmmss}.xls";
var base64 = Convert.ToBase64String(bytes); await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
var filename = $"discount-orders-{DateTime.Now:yyyyMMddHHmmss}.csv"; CsvExportHelper.ToExcelXmlBase64("سفارش‌های اعتباری", headers, rows));
Snackbar.Add("خروجی Excel آماده شد.", Severity.Success);
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
} }
catch (Exception ex) catch (Exception ex)
{ {
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error); Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
} }
} }
private static string EscapeCsv(string value)
{
if (string.IsNullOrEmpty(value)) return "";
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
return $"\"{value.Replace("\"", "\"\"")}\"";
return value;
}
} }
@@ -116,7 +116,7 @@
<TemplateColumn Title="قیمت" CellStyle="text-wrap: nowrap;"> <TemplateColumn Title="قیمت" CellStyle="text-wrap: nowrap;">
<CellTemplate> <CellTemplate>
<MudText>@context.Item.Price.ToString("N0") ریال</MudText> <MudText>@context.Item.Price.ToString("N0") تومان</MudText>
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
@@ -48,6 +48,7 @@
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Processing">در حال پردازش</MudSelectItem> <MudSelectItem T="OrderStatus?" Value="OrderStatus.Processing">در حال پردازش</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Shipped">ارسال شده</MudSelectItem> <MudSelectItem T="OrderStatus?" Value="OrderStatus.Shipped">ارسال شده</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Delivered">تحویل شده</MudSelectItem> <MudSelectItem T="OrderStatus?" Value="OrderStatus.Delivered">تحویل شده</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="OrderStatus.ReadyForOfficePickup">آماده تحویل در دفتر</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Cancelled">لغو شده</MudSelectItem> <MudSelectItem T="OrderStatus?" Value="OrderStatus.Cancelled">لغو شده</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Returned">مرجوع شده</MudSelectItem> <MudSelectItem T="OrderStatus?" Value="OrderStatus.Returned">مرجوع شده</MudSelectItem>
</MudSelect> </MudSelect>
@@ -113,19 +114,19 @@
<MudItem xs="12" md="3"> <MudItem xs="12" md="3">
<MudPaper Elevation="1" Class="pa-4 text-center"> <MudPaper Elevation="1" Class="pa-4 text-center">
<MudText Typo="Typo.subtitle2" Color="Color.Success">مجموع فروش (مبلغ نهایی)</MudText> <MudText Typo="Typo.subtitle2" Color="Color.Success">مجموع فروش (مبلغ نهایی)</MudText>
<MudText Typo="Typo.h5">@_totalSales.ToString("N0") ریال</MudText> <MudText Typo="Typo.h5">@_totalSales.ToString("N0") تومان</MudText>
</MudPaper> </MudPaper>
</MudItem> </MudItem>
<MudItem xs="12" md="3"> <MudItem xs="12" md="3">
<MudPaper Elevation="1" Class="pa-4 text-center"> <MudPaper Elevation="1" Class="pa-4 text-center">
<MudText Typo="Typo.subtitle2" Color="Color.Warning">مجموع تخفیف</MudText> <MudText Typo="Typo.subtitle2" Color="Color.Warning">مجموع تخفیف</MudText>
<MudText Typo="Typo.h5">@_totalDiscount.ToString("N0") ریال</MudText> <MudText Typo="Typo.h5">@_totalDiscount.ToString("N0") تومان</MudText>
</MudPaper> </MudPaper>
</MudItem> </MudItem>
<MudItem xs="12" md="3"> <MudItem xs="12" md="3">
<MudPaper Elevation="1" Class="pa-4 text-center"> <MudPaper Elevation="1" Class="pa-4 text-center">
<MudText Typo="Typo.subtitle2" Color="Color.Info">میانگین مبلغ سفارش</MudText> <MudText Typo="Typo.subtitle2" Color="Color.Info">میانگین مبلغ سفارش</MudText>
<MudText Typo="Typo.h5">@_averageOrder.ToString("N0") ریال</MudText> <MudText Typo="Typo.h5">@_averageOrder.ToString("N0") تومان</MudText>
</MudPaper> </MudPaper>
</MudItem> </MudItem>
</MudGrid> </MudGrid>
@@ -242,12 +243,12 @@
</TemplateColumn> </TemplateColumn>
<TemplateColumn Title="مبلغ نهایی"> <TemplateColumn Title="مبلغ نهایی">
<CellTemplate> <CellTemplate>
@context.Item.GatewayAmount.ToString("N0") ریال @context.Item.GatewayAmount.ToString("N0") تومان
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
<TemplateColumn Title="تخفیف"> <TemplateColumn Title="تخفیف">
<CellTemplate> <CellTemplate>
@context.Item.DiscountBalanceUsed.ToString("N0") ریال @context.Item.DiscountBalanceUsed.ToString("N0") تومان
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
<TemplateColumn Title="وضعیت"> <TemplateColumn Title="وضعیت">
@@ -498,7 +499,8 @@
OrderStatus.Processing => Color.Info, OrderStatus.Processing => Color.Info,
OrderStatus.Shipped => Color.Info, OrderStatus.Shipped => Color.Info,
OrderStatus.Delivered => Color.Success, OrderStatus.Delivered => Color.Success,
OrderStatus.Cancelled => Color.Error, OrderStatus.ReadyForOfficePickup => Color.Primary,
OrderStatus.Cancelled => Color.Dark,
OrderStatus.Returned => Color.Error, OrderStatus.Returned => Color.Error,
_ => Color.Default _ => Color.Default
}; };
@@ -513,6 +515,7 @@
OrderStatus.Processing => "در حال پردازش", OrderStatus.Processing => "در حال پردازش",
OrderStatus.Shipped => "ارسال شده", OrderStatus.Shipped => "ارسال شده",
OrderStatus.Delivered => "تحویل شده", OrderStatus.Delivered => "تحویل شده",
OrderStatus.ReadyForOfficePickup => "آماده تحویل در دفتر",
OrderStatus.Cancelled => "لغو شده", OrderStatus.Cancelled => "لغو شده",
OrderStatus.Returned => "مرجوع شده", OrderStatus.Returned => "مرجوع شده",
_ => "نامشخص" _ => "نامشخص"
@@ -527,28 +530,31 @@
return; return;
} }
var sb = new StringBuilder(); var headers = new[]
sb.AppendLine("شناسه,نام کاربر,موبایل,تاریخ ثبت,تعداد آیتم,مبلغ نهایی,تخفیف,وضعیت"); {
"شناسه", "نام مشتری", "موبایل", "تاریخ ثبت", "تعداد آیتم", "مبلغ نهایی", "تخفیف", "وضعیت"
foreach (var o in _orders.Where(o => o.Created.HasValue).OrderBy(o => o.Created)) };
var rows = _orders
.OrderBy(o => o.Created ?? o.PaymentDate ?? DateTime.MaxValue)
.Select(o => new object?[]
{ {
sb.AppendLine(string.Join(",",
o.Id, o.Id,
EscapeCsv(o.UserFullName), CsvExportHelper.CustomerName(o.UserFullName, o.UserMobile, o.UserId),
EscapeCsv(o.UserMobile), o.UserMobile ?? "",
o.Created?.MiladiToJalaliWithTime() ?? "-", o.Created?.MiladiToJalaliWithTime()
?? o.PaymentDate?.MiladiToJalaliWithTime()
?? "-",
o.ItemsCount, o.ItemsCount,
o.GatewayAmount, o.GatewayAmount,
o.DiscountBalanceUsed, o.DiscountBalanceUsed,
GetStatusText(o.Status))); GetStatusText(o.Status)
} });
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray(); var filename = $"discount-sales-{DateTime.Now:yyyyMMddHHmmss}.xls";
var base64 = Convert.ToBase64String(bytes); await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
var filename = $"discount-sales-{DateTime.Now:yyyyMMddHHmmss}.csv"; CsvExportHelper.ToExcelXmlBase64("گزارش فروش اعتباری", headers, rows));
Snackbar.Add("خروجی Excel آماده شد.", Severity.Success);
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
} }
private async Task ExportToPdf() private async Task ExportToPdf()
@@ -566,18 +572,18 @@
sb.AppendLine("خلاصه:"); sb.AppendLine("خلاصه:");
sb.AppendLine($"تعداد سفارش‌ها: {_totalOrders}"); sb.AppendLine($"تعداد سفارش‌ها: {_totalOrders}");
sb.AppendLine($"مجموع فروش (مبلغ نهایی): {_totalSales:N0} ریال"); sb.AppendLine($"مجموع فروش (مبلغ نهایی): {_totalSales:N0} تومان");
sb.AppendLine($"مجموع تخفیف: {_totalDiscount:N0} ریال"); sb.AppendLine($"مجموع تخفیف: {_totalDiscount:N0} تومان");
sb.AppendLine($"میانگین مبلغ سفارش: {_averageOrder:N0} ریال"); sb.AppendLine($"میانگین مبلغ سفارش: {_averageOrder:N0} تومان");
sb.AppendLine(); sb.AppendLine();
sb.AppendLine("جزئیات سفارش‌ها:"); sb.AppendLine("جزئیات سفارش‌ها:");
sb.AppendLine("شناسه | نام کاربر | تاریخ | تعداد آیتم | مبلغ نهایی | تخفیف | وضعیت"); sb.AppendLine("شناسه | نام مشتری | تاریخ | تعداد آیتم | مبلغ نهایی | تخفیف | وضعیت");
foreach (var o in _orders.Where(o => o.Created.HasValue).OrderBy(o => o.Created)) foreach (var o in _orders.Where(o => o.Created.HasValue).OrderBy(o => o.Created))
{ {
sb.AppendLine( sb.AppendLine(
$"{o.Id} | {o.UserFullName} | {o.Created?.MiladiToJalaliWithTime() ?? "-"} | {o.ItemsCount} | {o.GatewayAmount:N0} | {o.DiscountBalanceUsed:N0} | {GetStatusText(o.Status)}"); $"{o.Id} | {CsvExportHelper.CustomerName(o.UserFullName, o.UserMobile, o.UserId)} | {o.Created?.MiladiToJalaliWithTime() ?? "-"} | {o.ItemsCount} | {o.GatewayAmount:N0} | {o.DiscountBalanceUsed:N0} | {GetStatusText(o.Status)}");
} }
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray(); var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
@@ -587,14 +593,4 @@
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64); await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
Snackbar.Add("خروجی PDF (نسخه متنی) آماده شد.", Severity.Success); Snackbar.Add("خروجی PDF (نسخه متنی) آماده شد.", Severity.Success);
} }
private static string EscapeCsv(string value)
{
if (string.IsNullOrEmpty(value))
return "";
var needsQuotes = value.Contains(',') || value.Contains('"') || value.Contains('\n');
var escaped = value.Replace("\"", "\"\"");
return needsQuotes ? $"\"{escaped}\"" : escaped;
}
} }
+1 -1
View File
@@ -108,7 +108,7 @@
<MudStack Spacing="0"> <MudStack Spacing="0">
<MudText Typo="Typo.caption" Color="Color.Secondary">مبلغ کل استخر</MudText> <MudText Typo="Typo.caption" Color="Color.Secondary">مبلغ کل استخر</MudText>
<MudText Typo="Typo.h6" Color="Color.Primary"> <MudText Typo="Typo.h6" Color="Color.Primary">
@(_currentPool?.TotalPoolAmount.ToString("N0") ?? "0") ریال @(_currentPool?.TotalPoolAmount.ToString("N0") ?? "0") تومان
</MudText> </MudText>
</MudStack> </MudStack>
</MudItem> </MudItem>
@@ -33,10 +33,8 @@
Min="0" /> Min="0" />
</MudItem> </MudItem>
<MudItem xs="12" md="2"> <MudItem xs="12" md="2">
<MudNumericField @bind-Value="_maxBalance" <PackageSelect Label="پکیج"
Label="حداکثر موجودی" @bind-SelectedPackageId="_filterPackageId" />
Variant="Variant.Outlined"
Min="0" />
</MudItem> </MudItem>
<MudItem xs="12" md="2" Class="d-flex align-center"> <MudItem xs="12" md="2" Class="d-flex align-center">
<MudButton Variant="Variant.Filled" <MudButton Variant="Variant.Filled"
@@ -72,6 +70,14 @@
<PropertyColumn Property="x => x.UserFullName" Title="نام کاربر" /> <PropertyColumn Property="x => x.UserFullName" Title="نام کاربر" />
<PropertyColumn Property="x => x.WeekDisplayName" Title="هفته" /> <PropertyColumn Property="x => x.WeekDisplayName" Title="هفته" />
<PropertyColumn Property="x => x.PackageTitle" Title="پکیج">
<CellTemplate>
<MudChip T="string" Color="Color.Tertiary" Size="Size.Small">
@(string.IsNullOrEmpty(context.Item.PackageTitle) ? "-" : context.Item.PackageTitle)
</MudChip>
</CellTemplate>
</PropertyColumn>
<TemplateColumn Title="چپ (جدید + قبلی = مجموع)"> <TemplateColumn Title="چپ (جدید + قبلی = مجموع)">
<CellTemplate> <CellTemplate>
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center"> <MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
@@ -129,7 +135,7 @@
</PropertyColumn> </PropertyColumn>
<PropertyColumn Property="x => x.PoolContribution" Title="سهم از استخر"> <PropertyColumn Property="x => x.PoolContribution" Title="سهم از استخر">
<CellTemplate> <CellTemplate>
@context.Item.PoolContribution.ToString("N0") ریال @context.Item.PoolContribution.ToString("N0") تومان
</CellTemplate> </CellTemplate>
</PropertyColumn> </PropertyColumn>
<PropertyColumn Property="x => x.IsExpired" Title="وضعیت"> <PropertyColumn Property="x => x.IsExpired" Title="وضعیت">
@@ -193,6 +199,7 @@
private MudDataGrid <UserWeeklyBalanceModel> _grid { get; set; } private MudDataGrid <UserWeeklyBalanceModel> _grid { get; set; }
private long? _filterUserId = null; private long? _filterUserId = null;
private long? _filterWeekDefinitionId = null; private long? _filterWeekDefinitionId = null;
private long? _filterPackageId = null;
private int? _minBalance = null; private int? _minBalance = null;
private int? _maxBalance = null; private int? _maxBalance = null;
private bool _onlyActive = false; private bool _onlyActive = false;
@@ -222,6 +229,11 @@ private MudDataGrid <UserWeeklyBalanceModel> _grid { get; set; }
request.WeekDefinitionId = _filterWeekDefinitionId.Value; request.WeekDefinitionId = _filterWeekDefinitionId.Value;
} }
if (_filterPackageId.HasValue && _filterPackageId.Value > 0)
{
request.PackageId = _filterPackageId.Value;
}
var response = await CommissionClient.GetUserWeeklyBalancesAsync(request); var response = await CommissionClient.GetUserWeeklyBalancesAsync(request);
var items = response.Models.Select(b => new UserWeeklyBalanceModel var items = response.Models.Select(b => new UserWeeklyBalanceModel
@@ -238,7 +250,8 @@ private MudDataGrid <UserWeeklyBalanceModel> _grid { get; set; }
MatchedBalance = b.TotalBalances, MatchedBalance = b.TotalBalances,
PoolContribution = b.WeeklyPoolContribution, PoolContribution = b.WeeklyPoolContribution,
CalculatedAt = b.CalculatedAt?.ToDateTime(), CalculatedAt = b.CalculatedAt?.ToDateTime(),
IsExpired = b.IsExpired IsExpired = b.IsExpired,
PackageTitle = b.PackageTitle
}).ToList(); }).ToList();
// Apply balance range filter if specified // Apply balance range filter if specified
@@ -308,5 +321,6 @@ private MudDataGrid <UserWeeklyBalanceModel> _grid { get; set; }
public long PoolContribution { get; set; } public long PoolContribution { get; set; }
public DateTime? CalculatedAt { get; set; } public DateTime? CalculatedAt { get; set; }
public bool IsExpired { get; set; } public bool IsExpired { get; set; }
public string PackageTitle { get; set; } = string.Empty;
} }
} }
@@ -0,0 +1,78 @@
@using CMSMicroservice.Protobuf.Protos.NetworkMembership
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.SwapHoriz" Class="ml-1" />
تغییر والد در شبکه
</MudText>
</TitleContent>
<DialogContent>
<MudAlert Severity="Severity.Warning" Dense="true" Class="mb-4">
⚠️ این عملیات حساس است و ساختار درخت شبکه را تغییر می‌دهد.
تاریخچه تغییر ثبت خواهد شد.
</MudAlert>
<MudForm @ref="_form">
<MudStack Spacing="3">
<MudTextField T="string"
Label="کاربر فعلی"
Value="@UserDisplayName"
ReadOnly="true"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Person" />
<MudNumericField T="long"
@bind-Value="_newParentId"
Label="شناسه والد جدید"
Required="true"
RequiredError="شناسه والد جدید الزامی است"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.PersonSearch"
Min="1" />
<MudSelect T="int"
@bind-Value="_newLeg"
Label="ساق جدید"
Required="true"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.AccountTree">
<MudSelectItem T="int" Value="0">چپ (تیم اول)</MudSelectItem>
<MudSelectItem T="int" Value="1">راست (تیم دوم)</MudSelectItem>
</MudSelect>
<MudTextField T="string"
@bind-Value="_reason"
Label="دلیل تغییر"
Required="true"
RequiredError="دلیل تغییر الزامی است"
Variant="Variant.Outlined"
Lines="3"
Placeholder="دلیل جابجایی کاربر در شبکه را وارد کنید..."
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Description" />
</MudStack>
</MudForm>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel" Color="Color.Default">انصراف</MudButton>
<MudButton OnClick="Submit"
Color="Color.Error"
Variant="Variant.Filled"
Disabled="_isSubmitting"
StartIcon="@Icons.Material.Filled.SwapHoriz">
@if (_isSubmitting)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="ml-2" />
<span>در حال انجام...</span>
}
else
{
<span>تغییر والد</span>
}
</MudButton>
</DialogActions>
</MudDialog>
@@ -0,0 +1,66 @@
using CMSMicroservice.Protobuf.Protos.NetworkMembership;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace BackOffice.Pages.Network.Components;
public partial class ChangeParentDialog
{
[Parameter] public long UserId { get; set; }
[Parameter] public string UserDisplayName { get; set; } = "";
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!;
[Inject] public NetworkMembershipContract.NetworkMembershipContractClient NetworkContract { get; set; } = default!;
private MudForm _form = default!;
private long _newParentId;
private int _newLeg;
private string _reason = "";
private bool _isSubmitting;
private async Task Submit()
{
await _form.Validate();
if (!_form.IsValid)
return;
if (_newParentId == UserId)
{
Snackbar.Add("والد جدید نمی‌تواند خود کاربر باشد.", Severity.Error);
return;
}
_isSubmitting = true;
StateHasChanged();
try
{
var request = new ChangeNetworkParentRequest
{
UserId = UserId,
NewParentId = _newParentId,
NewLeg = _newLeg,
Reason = _reason
};
await NetworkContract.ChangeNetworkParentAsync(request);
Snackbar.Add("والد کاربر با موفقیت تغییر کرد.", Severity.Success);
MudDialog.Close(DialogResult.Ok(true));
}
catch (Grpc.Core.RpcException ex)
{
Snackbar.Add($"خطا: {ex.Status.Detail}", Severity.Error);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در تغییر والد: {ex.Message}", Severity.Error);
}
finally
{
_isSubmitting = false;
StateHasChanged();
}
}
private void Cancel() => MudDialog.Cancel();
}
@@ -169,6 +169,42 @@
</CellTemplate> </CellTemplate>
</PropertyColumn> </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"> <TemplateColumn Title="عملیات" Sortable="false">
<CellTemplate> <CellTemplate>
<MudButtonGroup Size="Size.Small" Variant="Variant.Text"> <MudButtonGroup Size="Size.Small" Variant="Variant.Text">
@@ -337,7 +373,9 @@
isClubActive = n.IsClubActive, isClubActive = n.IsClubActive,
isActivatedInTargetWeek = n.IsActivatedInTargetWeek, isActivatedInTargetWeek = n.IsActivatedInTargetWeek,
activationWeekDefinitionId = n.ActivationWeekDefinitionId, activationWeekDefinitionId = n.ActivationWeekDefinitionId,
clubActivatedAt = n.ClubActivatedAt?.ToDateTime().ToLocalTime().MiladiToJalali() ?? "" clubActivatedAt = n.ClubActivatedAt?.ToDateTime().ToLocalTime().MiladiToJalali() ?? "",
isNewActivation = n.IsNewActivation,
packageName = n.PackageName ?? ""
}).ToArray(); }).ToArray();
var options = new { filterWeek = _activationWeekFilter }; var options = new { filterWeek = _activationWeekFilter };
@@ -254,7 +254,7 @@
<td><strong>کل کمیسیون کسب شده:</strong></td> <td><strong>کل کمیسیون کسب شده:</strong></td>
<td> <td>
<MudText Typo="Typo.body1" Color="Color.Primary"> <MudText Typo="Typo.body1" Color="Color.Primary">
<strong>@_userInfo.TotalEarnedCommission.ToString("N0") ریال</strong> <strong>@_userInfo.TotalEarnedCommission.ToString("N0") تومان</strong>
</MudText> </MudText>
</td> </td>
</tr> </tr>
@@ -262,7 +262,7 @@
<td><strong>کمیسیون پرداخت شده:</strong></td> <td><strong>کمیسیون پرداخت شده:</strong></td>
<td> <td>
<MudText Typo="Typo.body1" Color="Color.Success"> <MudText Typo="Typo.body1" Color="Color.Success">
@_userInfo.TotalPaidCommission.ToString("N0") ریال @_userInfo.TotalPaidCommission.ToString("N0") تومان
</MudText> </MudText>
</td> </td>
</tr> </tr>
@@ -270,7 +270,7 @@
<td><strong>کمیسیون در انتظار:</strong></td> <td><strong>کمیسیون در انتظار:</strong></td>
<td> <td>
<MudText Typo="Typo.body1" Color="Color.Warning"> <MudText Typo="Typo.body1" Color="Color.Warning">
@_userInfo.PendingCommission.ToString("N0") ریال @_userInfo.PendingCommission.ToString("N0") تومان
</MudText> </MudText>
</td> </td>
</tr> </tr>
@@ -300,9 +300,9 @@
<MudSimpleTable Dense="true"> <MudSimpleTable Dense="true">
<tbody> <tbody>
<tr> <tr>
<td><strong>پکیج طلایی:</strong></td> <td><strong>خرید پکیج:</strong></td>
<td> <td>
@if (_userInfo.HasPurchasedGoldenPackage) @if (_userInfo.HasPurchasedPackage)
{ {
<MudChip T="string" Color="Color.Success" Size="Size.Small" Icon="@Icons.Material.Filled.CheckCircle"> <MudChip T="string" Color="Color.Success" Size="Size.Small" Icon="@Icons.Material.Filled.CheckCircle">
خریداری شده خریداری شده
@@ -434,6 +434,12 @@
OnClick="@(() => NavigationManager.NavigateTo($"/commission/payouts?userId={UserId}"))"> OnClick="@(() => NavigationManager.NavigateTo($"/commission/payouts?userId={UserId}"))">
پرداخت‌های کاربر پرداخت‌های کاربر
</MudButton> </MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Warning"
StartIcon="@Icons.Material.Filled.SwapHoriz"
OnClick="OpenChangeParentDialog">
تغییر والد
</MudButton>
<MudButton Variant="Variant.Outlined" <MudButton Variant="Variant.Outlined"
Color="Color.Secondary" Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.Refresh" StartIcon="@Icons.Material.Filled.Refresh"
@@ -537,4 +543,28 @@
_ => "نامشخص" _ => "نامشخص"
}; };
} }
private async Task OpenChangeParentDialog()
{
var displayName = $"{_userInfo.UserName} (ID: {UserId})";
var parameters = new DialogParameters<Network.Components.ChangeParentDialog>
{
{ x => x.UserId, UserId },
{ x => x.UserDisplayName, displayName }
};
var options = new DialogOptions
{
MaxWidth = MaxWidth.Small,
FullWidth = true,
CloseOnEscapeKey = true
};
var dialog = await DialogService.ShowAsync<Network.Components.ChangeParentDialog>("تغییر والد شبکه", parameters, options);
var result = await dialog.Result;
if (result is { Canceled: false })
{
await LoadUserInfo();
await LoadNetworkHistory();
}
}
} }
@@ -1,4 +1,5 @@
@using CMSMicroservice.Protobuf.Protos.Package @using CMSMicroservice.Protobuf.Protos.Package
@using CMSMicroservice.Protobuf.Protos.Configuration
@using BackOffice.Common.BaseComponents @using BackOffice.Common.BaseComponents
@using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Forms
@using Tizzani.MudBlazor.HtmlEditor @using Tizzani.MudBlazor.HtmlEditor
@@ -41,12 +42,77 @@
<MudTextField T="string" @bind-Value="Model.Title" Disabled="_isLoading" Label="عنوان" Variant="Variant.Outlined" /> <MudTextField T="string" @bind-Value="Model.Title" Disabled="_isLoading" Label="عنوان" Variant="Variant.Outlined" />
</MudItem> </MudItem>
<MudItem xs="6"> <MudItem xs="6">
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.Price" Format="N0" Immediate="true" Disabled="_isLoading" Label="قیمت" Variant="Variant.Outlined" /> <MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.Price" Format="N0" Immediate="true" Disabled="_isLoading" Label="قیمت (ریال)" Variant="Variant.Outlined" />
</MudItem> </MudItem>
</MudStack> </MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudItem xs="4">
<MudNumericField T="int" HideSpinButtons="true" @bind-Value="Model.SortOrder" Disabled="_isLoading" Label="ترتیب نمایش" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="4">
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.ActivationFee" Format="N0" Immediate="true" Disabled="_isLoading" Label="هزینه فعال‌سازی (ریال)" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="4">
<MudNumericField T="double" HideSpinButtons="true" @bind-Value="Model.DiscountMultiplier" Disabled="_isLoading" Label="ضریب تخفیف" Variant="Variant.Outlined" />
</MudItem>
</MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudItem xs="4">
<MudNumericField T="double" HideSpinButtons="true" @bind-Value="Model.MagicWalletMultiplier" Disabled="_isLoading" Label="ضریب کیف پول جادویی" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="4">
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.MagicWalletMaxDeposit" Format="N0" Immediate="true" Disabled="_isLoading" Label="سقف واریز جادویی" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="4">
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.MagicWalletMaxCredit" Format="N0" Immediate="true" Disabled="_isLoading" Label="سقف اعتبار جادویی" Variant="Variant.Outlined" />
</MudItem>
</MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudItem xs="6">
<MudNumericField T="int" HideSpinButtons="true" @bind-Value="Model.MaxBalancesPerLeg" Disabled="_isLoading" Label="حداکثر تعادل هر پا" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="6">
<MudNumericField T="int" HideSpinButtons="true" @bind-Value="Model.MaxNetworkLevel" Disabled="_isLoading" Label="حداکثر سطح شبکه" Variant="Variant.Outlined" />
</MudItem>
</MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudItem xs="3">
<MudCheckBox T="bool" @bind-Value="Model.IsActive" Disabled="_isLoading" Label="فعال" Color="Color.Success" />
</MudItem>
<MudItem xs="3">
<MudCheckBox T="bool" @bind-Value="Model.IsBasePackage" Disabled="_isLoading" Label="پکیج پایه" Color="Color.Primary" />
</MudItem>
<MudItem xs="3">
<MudCheckBox T="bool" @bind-Value="Model.SupportsDirectPurchase" Disabled="_isLoading" Label="پرداخت مستقیم" Color="Color.Info" />
</MudItem>
<MudItem xs="3">
<MudCheckBox T="bool" @bind-Value="Model.SupportsDayaPurchase" Disabled="_isLoading" Label="اعتبار دایا" Color="Color.Warning" />
</MudItem>
</MudStack>
@if (_features.Any())
{
<MudText Typo="Typo.subtitle1" Class="mt-4 mb-2"><strong>ویژگی‌های پکیج</strong></MudText>
<MudStack Row="true" Wrap="Wrap.Wrap">
@foreach (var feature in _features)
{
<MudCheckBox T="bool"
Value="@(_selectedFeatureIds.Contains(feature.Id))"
ValueChanged="@((bool v) => ToggleFeature(feature.Id, v))"
Disabled="_isLoading"
Label="@feature.Title"
Color="Color.Primary" />
}
</MudStack>
}
<MudItem xs="12"> <MudItem xs="12">
<MudHtmlEditor @bind-Html="Model.Description" MinHeight="300px"> <MudHtmlEditor @bind-Html="Model.Description" MinHeight="300px">
<MudHtmlToolbarOptions InsertImage="false" /> <!-- This will exclude the "insert image" toolbar option --> <MudHtmlToolbarOptions InsertImage="false" />
</MudHtmlEditor> </MudHtmlEditor>
</MudItem> </MudItem>
File diff suppressed because one or more lines are too long
@@ -1,4 +1,5 @@
@using CMSMicroservice.Protobuf.Protos.Package @using CMSMicroservice.Protobuf.Protos.Package
@using CMSMicroservice.Protobuf.Protos.Configuration
@using BackOffice.Common.BaseComponents @using BackOffice.Common.BaseComponents
@using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Forms
@using Tizzani.MudBlazor.HtmlEditor @using Tizzani.MudBlazor.HtmlEditor
@@ -42,12 +43,77 @@
<MudTextField T="string" @bind-Value="Model.Title" Disabled="_isLoading" Label="عنوان" Variant="Variant.Outlined" /> <MudTextField T="string" @bind-Value="Model.Title" Disabled="_isLoading" Label="عنوان" Variant="Variant.Outlined" />
</MudItem> </MudItem>
<MudItem xs="6"> <MudItem xs="6">
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.Price" Disabled="_isLoading" Label="قیمت" Variant="Variant.Outlined" /> <MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.Price" Disabled="_isLoading" Label="قیمت (ریال)" Variant="Variant.Outlined" />
</MudItem> </MudItem>
</MudStack> </MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudItem xs="4">
<MudNumericField T="int" HideSpinButtons="true" @bind-Value="Model.SortOrder" Disabled="_isLoading" Label="ترتیب نمایش" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="4">
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.ActivationFee" Format="N0" Immediate="true" Disabled="_isLoading" Label="هزینه فعال‌سازی (ریال)" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="4">
<MudNumericField T="double" HideSpinButtons="true" @bind-Value="Model.DiscountMultiplier" Disabled="_isLoading" Label="ضریب تخفیف" Variant="Variant.Outlined" />
</MudItem>
</MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudItem xs="4">
<MudNumericField T="double" HideSpinButtons="true" @bind-Value="Model.MagicWalletMultiplier" Disabled="_isLoading" Label="ضریب کیف پول جادویی" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="4">
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.MagicWalletMaxDeposit" Format="N0" Immediate="true" Disabled="_isLoading" Label="سقف واریز جادویی" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="4">
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.MagicWalletMaxCredit" Format="N0" Immediate="true" Disabled="_isLoading" Label="سقف اعتبار جادویی" Variant="Variant.Outlined" />
</MudItem>
</MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudItem xs="6">
<MudNumericField T="int" HideSpinButtons="true" @bind-Value="Model.MaxBalancesPerLeg" Disabled="_isLoading" Label="حداکثر تعادل هر پا" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="6">
<MudNumericField T="int" HideSpinButtons="true" @bind-Value="Model.MaxNetworkLevel" Disabled="_isLoading" Label="حداکثر سطح شبکه" Variant="Variant.Outlined" />
</MudItem>
</MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudItem xs="3">
<MudCheckBox T="bool" @bind-Value="Model.IsActive" Disabled="_isLoading" Label="فعال" Color="Color.Success" />
</MudItem>
<MudItem xs="3">
<MudCheckBox T="bool" @bind-Value="Model.IsBasePackage" Disabled="_isLoading" Label="پکیج پایه" Color="Color.Primary" />
</MudItem>
<MudItem xs="3">
<MudCheckBox T="bool" @bind-Value="Model.SupportsDirectPurchase" Disabled="_isLoading" Label="پرداخت مستقیم" Color="Color.Info" />
</MudItem>
<MudItem xs="3">
<MudCheckBox T="bool" @bind-Value="Model.SupportsDayaPurchase" Disabled="_isLoading" Label="اعتبار دایا" Color="Color.Warning" />
</MudItem>
</MudStack>
@if (_features.Any())
{
<MudText Typo="Typo.subtitle1" Class="mt-4 mb-2"><strong>ویژگی‌های پکیج</strong></MudText>
<MudStack Row="true" Wrap="Wrap.Wrap">
@foreach (var feature in _features)
{
<MudCheckBox T="bool"
Value="@(_selectedFeatureIds.Contains(feature.Id))"
ValueChanged="@((bool v) => ToggleFeature(feature.Id, v))"
Disabled="_isLoading"
Label="@feature.Title"
Color="Color.Primary" />
}
</MudStack>
}
<MudItem xs="12"> <MudItem xs="12">
<MudHtmlEditor @bind-Html="Model.Description" MinHeight="300px"> <MudHtmlEditor @bind-Html="Model.Description" MinHeight="300px">
<MudHtmlToolbarOptions InsertImage="false" /> <!-- This will exclude the "insert image" toolbar option --> <MudHtmlToolbarOptions InsertImage="false" />
</MudHtmlEditor> </MudHtmlEditor>
</MudItem> </MudItem>
</MudStack> </MudStack>
File diff suppressed because one or more lines are too long
@@ -12,13 +12,21 @@
</MudItem> </MudItem>
</Filters> </Filters>
<Content> <Content>
@* ── G8: راهنمای مدیریت پکیج (Q28) ── *@
<MudAlert Severity="Severity.Warning" Variant="Variant.Text" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Warning">
تغییر قیمت روی کاربران فعلی اثر ندارد — فقط خریدهای جدید. هر پکیج Pool پورسانت مستقل دارد.
</MudAlert>
<MudDataGrid T="DataModel" ServerData="@(new Func<GridState<DataModel>, Task<GridData<DataModel>>>(ServerReload))" <MudDataGrid T="DataModel" ServerData="@(new Func<GridState<DataModel>, Task<GridData<DataModel>>>(ServerReload))"
Hover="true" @ref="_gridData" Height="calc(100vh - 240px)"> Hover="true" @ref="_gridData" Height="calc(100vh - 240px)">
<ColGroup> <ColGroup>
<col /> <col />
<col /> <col />
<col /> <col />
<col style="width: 58px;" /> <col />
<col />
<col />
<col />
<col style="width: 80px;" />
</ColGroup> </ColGroup>
<ToolBarContent> <ToolBarContent>
<MudText>مدیریت پکیج</MudText> <MudText>مدیریت پکیج</MudText>
@@ -51,31 +59,42 @@
</CellTemplate> </CellTemplate>
</PropertyColumn> </PropertyColumn>
<PropertyColumn Property="x => x.Description" Title="توضیحات" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;"> <PropertyColumn Property="x => x.Price" Title="قیمت" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
<CellTemplate> <CellTemplate>
@if (string.IsNullOrWhiteSpace(context.Item.Description)) <MudText Typo="Typo.inherit">@context.Item.Price.ToString("N0")</MudText>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.SortOrder" Title="ترتیب" />
<PropertyColumn Property="x => x.IsActive" Title="وضعیت" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
<CellTemplate>
@if (context.Item.IsActive)
{ {
<MudText Typo="Typo.inherit">-</MudText> <MudChip T="string" Color="Color.Success" Variant="Variant.Filled" Size="Size.Small">فعال</MudChip>
} }
else else
{ {
<MudChip T="string" Color="Color.Default" Variant="Variant.Filled" Size="Size.Small">غیرفعال</MudChip>
<MudTooltip Text="@(context.Item.Description.HtmlToText())" Arrow="true" Style="@($"{(context.Item.Description.Length < 70 ? string.Empty : "width:600px;")}")">
<MudText Typo="Typo.inherit" Style="text-wrap: nowrap;">
@(context.Item.Description.HtmlToText().Truncate(20, true))
</MudText>
</MudTooltip>
} }
</CellTemplate> </CellTemplate>
</PropertyColumn> </PropertyColumn>
<PropertyColumn Property="x => x.IsBasePackage" Title="نوع" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
<TemplateColumn StickyLeft="true" Title="عملیات" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
<CellTemplate> <CellTemplate>
<MudStack Row="true" AlignItems="AlignItems.Center"> @if (context.Item.IsBasePackage)
{
<MudChip T="string" Color="Color.Primary" Variant="Variant.Filled" Size="Size.Small">پایه</MudChip>
}
else
{
<MudChip T="string" Color="Color.Info" Variant="Variant.Outlined" Size="Size.Small">عادی</MudChip>
}
</CellTemplate>
</PropertyColumn>
<TemplateColumn Title="">
<CellTemplate>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudTooltip Text="ویرایش"> <MudTooltip Text="ویرایش">
<MudIconButton Icon="@Icons.Material.Filled.EditNote" Size="Size.Small" ButtonType="ButtonType.Button" OnClick="@(() => Update(context.Item.Adapt<DataModel>()))" /> <MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Small" ButtonType="ButtonType.Button" OnClick="@(() => Update(context.Item))" />
</MudTooltip> </MudTooltip>
<MudTooltip Text="آرشیو"> <MudTooltip Text="آرشیو">
<MudIconButton Icon="@Icons.Material.Filled.DeleteOutline" Size="Size.Small" ButtonType="ButtonType.Button" OnClick="@(() => OnDelete(context.Item))" /> <MudIconButton Icon="@Icons.Material.Filled.DeleteOutline" Size="Size.Small" ButtonType="ButtonType.Button" OnClick="@(() => OnDelete(context.Item))" />
</MudTooltip> </MudTooltip>
@@ -33,7 +33,7 @@ public partial class PackageMainPage
{ {
var parameters = new DialogParameters<UpdateDialog> { { x => x.Model, model.Adapt<UpdatePackageRequest>() } }; var parameters = new DialogParameters<UpdateDialog> { { x => x.Model, model.Adapt<UpdatePackageRequest>() } };
var dialog = await DialogService.ShowAsync<UpdateDialog>($"ویرایش پکیج", parameters, new DialogOptions() { CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.Small }); var dialog = await DialogService.ShowAsync<UpdateDialog>($"ویرایش پکیج", parameters, new DialogOptions() { CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.Medium });
var result = await dialog.Result; var result = await dialog.Result;
if (!result.Canceled) if (!result.Canceled)
@@ -70,7 +70,7 @@ public partial class PackageMainPage
public async Task CreateNew() public async Task CreateNew()
{ {
var dialog = await DialogService.ShowAsync<CreateDialog>($"افزودن پکیج", new DialogParameters<CreateDialog>() { { x => x.Model, new CreateNewPackageRequest() } }, new DialogOptions() { CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.Small }); var dialog = await DialogService.ShowAsync<CreateDialog>($"افزودن پکیج", new DialogParameters<CreateDialog>() { { x => x.Model, new CreateNewPackageRequest() { IsActive = true, SupportsDirectPurchase = true, DiscountMultiplier = 2.0, MagicWalletMultiplier = 2.5, MaxBalancesPerLeg = 300, MaxNetworkLevel = 15, MagicWalletMaxDeposit = 1_000_000_000, MagicWalletMaxCredit = 2_500_000_000 } } }, new DialogOptions() { CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.Medium });
var result = await dialog.Result; var result = await dialog.Result;
if (!result.Canceled) if (!result.Canceled)
{ {
@@ -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;
}
}
}
@@ -1,4 +1,5 @@
@using CMSMicroservice.Protobuf.Protos.ManualPayment @using CMSMicroservice.Protobuf.Protos.ManualPayment
@using CMSMicroservice.Protobuf.Protos.Package
@using BackOffice.Pages.Payment.Components @using BackOffice.Pages.Payment.Components
@using BackOffice.Pages.AutoComplete @using BackOffice.Pages.AutoComplete
@using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Forms
@@ -13,21 +14,36 @@
<UserAutoComplete Label="انتخاب کاربر" <UserAutoComplete Label="انتخاب کاربر"
@bind-SelectedUserId="_selectedUserId" /> @bind-SelectedUserId="_selectedUserId" />
<MudNumericField T="long" <MudSelect T="long?"
Label="مبلغ" Label="انتخاب پکیج"
Value="@_selectedPackageId"
ValueChanged="OnPackageChanged"
Variant="Variant.Outlined" Variant="Variant.Outlined"
Adornment="Adornment.End" Margin="Margin.Dense"
AdornmentText="تومان" Required="true"
Disabled="true" RequiredError="انتخاب پکیج الزامی است">
@bind-Value="_createModel.Amount" /> @foreach (var pkg in _packages)
{
<MudSelectItem T="long?" Value="@((long?)pkg.Id)">
@pkg.Title — @pkg.Price.ToString("N0") تومان
</MudSelectItem>
}
</MudSelect>
@if (_selectedPackagePrice.HasValue)
{
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text">
مبلغ پکیج: <strong>@_selectedPackagePrice.Value.ToString("N0") تومان</strong> — این مبلغ به کیف‌پول کاربر اضافه می‌شود.
</MudAlert>
}
<MudSelect T="int" <MudSelect T="int"
Label="نوع پرداخت" Label="نوع پرداخت"
Variant="Variant.Outlined" Variant="Variant.Outlined"
@bind-Value="_selectedType"> @bind-Value="_selectedType">
<MudSelectItem T="int" Value="1">واریز نقدی</MudSelectItem> <MudSelectItem T="int" Value="1">واریز نقدی</MudSelectItem>
<MudSelectItem T="int" Value="2">شارژ کیف پول تخفیف</MudSelectItem> <MudSelectItem T="int" Value="2">شارژ کیف پول اعتباری</MudSelectItem>
<MudSelectItem T="int" Value="3">شارژ کیف پول شبکه</MudSelectItem> <MudSelectItem T="int" Value="3">شارژ پاداش های دریافتی</MudSelectItem>
<MudSelectItem T="int" Value="4">تسویه حساب</MudSelectItem> <MudSelectItem T="int" Value="4">تسویه حساب</MudSelectItem>
<MudSelectItem T="int" Value="5">اصلاح خطا</MudSelectItem> <MudSelectItem T="int" Value="5">اصلاح خطا</MudSelectItem>
<MudSelectItem T="int" Value="6">بازپرداخت</MudSelectItem> <MudSelectItem T="int" Value="6">بازپرداخت</MudSelectItem>
@@ -83,7 +99,7 @@
<MudText Typo="Typo.h6">جزئیات فعالسازی دستی</MudText> <MudText Typo="Typo.h6">جزئیات فعالسازی دستی</MudText>
<MudText Typo="Typo.body2">شناسه: @Model.Id</MudText> <MudText Typo="Typo.body2">شناسه: @Model.Id</MudText>
<MudText Typo="Typo.body2">کاربر: @Model.UserFullName (@Model.UserId)</MudText> <MudText Typo="Typo.body2">کاربر: @Model.UserFullName (@Model.UserId)</MudText>
<MudText Typo="Typo.body2">مبلغ: @Model.Amount.ToString("N0") ریال</MudText> <MudText Typo="Typo.body2">مبلغ: @Model.Amount.ToString("N0") تومان</MudText>
<MudText Typo="Typo.body2">نوع: @Model.TypeDisplay</MudText> <MudText Typo="Typo.body2">نوع: @Model.TypeDisplay</MudText>
<MudText Typo="Typo.body2">وضعیت: @Model.StatusDisplay</MudText> <MudText Typo="Typo.body2">وضعیت: @Model.StatusDisplay</MudText>
@@ -1,4 +1,5 @@
using CMSMicroservice.Protobuf.Protos.ManualPayment; using CMSMicroservice.Protobuf.Protos.ManualPayment;
using CMSMicroservice.Protobuf.Protos.Package;
using Google.Protobuf; using Google.Protobuf;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Forms;
@@ -24,19 +25,23 @@ public partial class ManualPaymentDialog
{ {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!; [CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!;
[Inject] public ManualPaymentContract.ManualPaymentContractClient ManualPaymentClient { get; set; } = default!; [Inject] public ManualPaymentContract.ManualPaymentContractClient ManualPaymentClient { get; set; } = default!;
[Inject] public PackageContract.PackageContractClient PackageClient { get; set; } = default!;
// Snackbar is injected via _Imports.razor // Snackbar is injected via _Imports.razor
[Parameter] public ManualPaymentDialogMode Mode { get; set; } [Parameter] public ManualPaymentDialogMode Mode { get; set; }
[Parameter] public ManualPaymentModel? Model { get; set; } [Parameter] public ManualPaymentModel? Model { get; set; }
private ManualPaymentModel _createModel = new(){ private ManualPaymentModel _createModel = new(){ Amount = 0 };
Amount = 56000000,
};
private string? _adminNote; private string? _adminNote;
// User selection // User selection
private long? _selectedUserId; private long? _selectedUserId;
// Package selection
private long? _selectedPackageId;
private long? _selectedPackagePrice;
private List<GetAllPackageByFilterResponseModel> _packages = new();
// Type and Description // Type and Description
private int _selectedType = 1; // Default: CashDeposit private int _selectedType = 1; // Default: CashDeposit
private string _description = "عضویت دستی باشگاه مشتریان"; private string _description = "عضویت دستی باشگاه مشتریان";
@@ -48,6 +53,46 @@ public partial class ManualPaymentDialog
private bool _isSubmitting; private bool _isSubmitting;
protected override async Task OnInitializedAsync()
{
await LoadPackages();
}
private async Task LoadPackages()
{
try
{
var request = new GetAllPackageByFilterRequest
{
PaginationState = new() { PageNumber = 1, PageSize = 100 },
Filter = new()
};
var result = await PackageClient.GetAllPackageByFilterAsync(request);
_packages = result.Models?.OrderBy(p => p.SortOrder).ToList()
?? new List<GetAllPackageByFilterResponseModel>();
// اگر پکیج پایه وجود دارد، پیش‌فرض انتخاب کن
var basePackage = _packages.FirstOrDefault(p => p.IsBasePackage);
if (basePackage != null)
{
_selectedPackageId = basePackage.Id;
_selectedPackagePrice = basePackage.Price;
}
}
catch
{
_packages = new List<GetAllPackageByFilterResponseModel>();
}
}
private void OnPackageChanged(long? packageId)
{
_selectedPackageId = packageId;
var pkg = _packages.FirstOrDefault(p => p.Id == packageId);
_selectedPackagePrice = pkg?.Price;
}
private async Task OnImageFileSelected(IBrowserFile? file) private async Task OnImageFileSelected(IBrowserFile? file)
{ {
if (file == null) if (file == null)
@@ -81,6 +126,12 @@ public partial class ManualPaymentDialog
return; return;
} }
if (!_selectedPackageId.HasValue || _selectedPackageId.Value <= 0)
{
Snackbar.Add("لطفاً پکیج را انتخاب کنید.", Severity.Warning);
return;
}
if (_selectedType <= 0) if (_selectedType <= 0)
{ {
Snackbar.Add("لطفاً نوع پرداخت را انتخاب کنید.", Severity.Warning); Snackbar.Add("لطفاً نوع پرداخت را انتخاب کنید.", Severity.Warning);
@@ -101,7 +152,8 @@ public partial class ManualPaymentDialog
var request = new CreateManualPaymentRequest var request = new CreateManualPaymentRequest
{ {
UserId = _selectedUserId.Value, UserId = _selectedUserId.Value,
Amount = _createModel.Amount, Amount = 0, // deprecated — مبلغ از پکیج خوانده می‌شود
PackageId = _selectedPackageId.Value,
Type = (ManualPaymentType)_selectedType, Type = (ManualPaymentType)_selectedType,
Description = _description Description = _description
}; };
@@ -10,7 +10,7 @@
<MudText Typo="Typo.h6">جزئیات تراکنش</MudText> <MudText Typo="Typo.h6">جزئیات تراکنش</MudText>
<MudText Typo="Typo.body2">شناسه: @Model.Id</MudText> <MudText Typo="Typo.body2">شناسه: @Model.Id</MudText>
<MudText Typo="Typo.body2">RefId: @Model.RefId</MudText> <MudText Typo="Typo.body2">RefId: @Model.RefId</MudText>
<MudText Typo="Typo.body2">مبلغ: @Model.Amount.ToString("N0") ریال</MudText> <MudText Typo="Typo.body2">مبلغ: @Model.Amount.ToString("N0") تومان</MudText>
<MudText Typo="Typo.body2">وضعیت: @StatusText</MudText> <MudText Typo="Typo.body2">وضعیت: @StatusText</MudText>
<MudText Typo="Typo.body2">تاریخ پرداخت: @Model.PaymentDate.ToLocalTime().MiladiToJalaliWithTime()</MudText> <MudText Typo="Typo.body2">تاریخ پرداخت: @Model.PaymentDate.ToLocalTime().MiladiToJalaliWithTime()</MudText>
@@ -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();
}
}
@@ -6,6 +6,12 @@
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared"> <BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
<Filters> <Filters>
@* ── G10: هشدار پرداخت دستی (Q28) ── *@
<MudItem xs="12">
<MudAlert Severity="Severity.Warning" Variant="Variant.Text" Dense="true" Icon="@Icons.Material.Filled.Warning">
مبلغ پرداخت باید مطابق قیمت پکیج انتخابی باشد. پکیج‌های مختلف قیمت متفاوتی دارند.
</MudAlert>
</MudItem>
<MudItem xs="12" sm="6" md="3"> <MudItem xs="12" sm="6" md="3">
<UserAutoComplete Label="جستجوی کاربر" @bind-SelectedUserId="_userIdFilter" /> <UserAutoComplete Label="جستجوی کاربر" @bind-SelectedUserId="_userIdFilter" />
</MudItem> </MudItem>
@@ -26,8 +32,8 @@
<MudSelect T="int?" Clearable="true" Label="نوع" Variant="Variant.Outlined" <MudSelect T="int?" Clearable="true" Label="نوع" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_typeFilter"> Margin="Margin.Dense" @bind-Value="_typeFilter">
<MudSelectItem T="int?" Value="@(1)">واریز نقدی</MudSelectItem> <MudSelectItem T="int?" Value="@(1)">واریز نقدی</MudSelectItem>
<MudSelectItem T="int?" Value="@(2)">شارژ کیفپول تخفیف</MudSelectItem> <MudSelectItem T="int?" Value="@(2)">شارژ کیف پول اعتباری</MudSelectItem>
<MudSelectItem T="int?" Value="@(3)">شارژ کیف‌پول شبکه</MudSelectItem> <MudSelectItem T="int?" Value="@(3)">شارژ پاداش های دریافتی</MudSelectItem>
<MudSelectItem T="int?" Value="@(4)">تسویه حساب</MudSelectItem> <MudSelectItem T="int?" Value="@(4)">تسویه حساب</MudSelectItem>
<MudSelectItem T="int?" Value="@(5)">اصلاح خطا</MudSelectItem> <MudSelectItem T="int?" Value="@(5)">اصلاح خطا</MudSelectItem>
<MudSelectItem T="int?" Value="@(6)">بازگشت وجه</MudSelectItem> <MudSelectItem T="int?" Value="@(6)">بازگشت وجه</MudSelectItem>
@@ -219,8 +225,8 @@
return type switch return type switch
{ {
1 => "واریز نقدی", 1 => "واریز نقدی",
2 => "شارژ کیفپول تخفیف", 2 => "شارژ کیف پول اعتباری",
3 => "شارژ کیف‌پول شبکه", 3 => "شارژ پاداش های دریافتی",
4 => "تسویه حساب", 4 => "تسویه حساب",
5 => "اصلاح خطا", 5 => "اصلاح خطا",
6 => "بازگشت وجه", 6 => "بازگشت وجه",
@@ -257,7 +263,7 @@
{ {
bool? confirm = await DialogService.ShowMessageBox( bool? confirm = await DialogService.ShowMessageBox(
"تایید پرداخت", "تایید پرداخت",
$"آیا از تایید پرداخت دستی #{model.Id} به مبلغ {model.Amount:N0} ریال مطمئن هستید؟", $"آیا از تایید پرداخت دستی #{model.Id} به مبلغ {model.Amount:N0} تومان مطمئن هستید؟",
yesText: "تایید", cancelText: "لغو"); yesText: "تایید", cancelText: "لغو");
if (confirm == true) if (confirm == true)
{ {
@@ -37,7 +37,7 @@ public partial class ManualPayments
} }
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.AppendLine("شناسه,شناسه کاربر,نام کاربر,مبلغ,نوع,وضعیت,تاریخ ایجاد"); sb.AppendLine("شناسه,شناسه کاربر,نام کاربر,پکیج,مبلغ,نوع,وضعیت,تاریخ ایجاد");
foreach (var m in response.Models) foreach (var m in response.Models)
{ {
@@ -45,6 +45,7 @@ public partial class ManualPayments
m.Id, m.Id,
m.UserId, m.UserId,
EscapeCsv(m.UserFullName), EscapeCsv(m.UserFullName),
EscapeCsv(m.PackageName ?? "-"),
m.Amount.ToString("N0"), m.Amount.ToString("N0"),
EscapeCsv(GetTypeText((int)m.Type)), EscapeCsv(GetTypeText((int)m.Type)),
EscapeCsv(GetStatusText((int)m.Status)), EscapeCsv(GetStatusText((int)m.Status)),
+172 -51
View File
@@ -1,78 +1,115 @@
@page "/payment/transactions" @page "/payment/transactions"
@using BackOffice.Common.BaseComponents @using BackOffice.Common.BaseComponents
@using BackOffice.Pages.AutoComplete
@using CMSMicroservice.Protobuf.Protos.PaymentTransaction
@using Google.Protobuf.WellKnownTypes
@inject PaymentTransactionContract.PaymentTransactionContractClient PaymentTxClient
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared"> <BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
<Filters> <Filters>
<MudItem xs="12" sm="6" md="4"> <MudItem xs="12">
<BackOffice.Common.BaseComponents.DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه تاریخی" /> <MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true">
گزارش read-only تراکنش‌های درگاه پرداخت. برای ثبت پرداخت دستی به «پرداخت‌های دستی» مراجعه کنید.
</MudAlert>
</MudItem> </MudItem>
<MudItem xs="12" sm="6" md="4"> <MudItem xs="12" sm="6" md="3">
<MudSelect T="int?" Clearable="true" Label="وضعیت پرداخت" Variant="Variant.Outlined" <UserAutoComplete Label="کاربر" @bind-SelectedUserId="_userId" />
Margin="Margin.Dense" @bind-Value="_statusFilter"> </MudItem>
<MudSelectItem T="int?" Value="@(0)">در انتظار</MudSelectItem> <MudItem xs="12" sm="6" md="3">
<MudSelectItem T="int?" Value="@(1)">موفق</MudSelectItem> <MudTextField T="string" Label="موبایل" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_mobile" />
<MudSelectItem T="int?" Value="@(2)">ناموفق</MudSelectItem> </MudItem>
<MudSelectItem T="int?" Value="@(3)">بازگشت وجه</MudSelectItem> <MudItem xs="12" sm="6" md="3">
<MudTextField T="string" Label="Authority" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_authority" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudTextField T="string" Label="RefId" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_refId" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudTextField T="string" Label="OrderId" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_orderId" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudTextField T="string" Label="درگاه" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_gateway" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="bool?" Clearable="true" Label="وضعیت پرداخت" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_paymentStatus">
<MudSelectItem T="bool?" Value="@((bool?)true)">موفق</MudSelectItem>
<MudSelectItem T="bool?" Value="@((bool?)false)">ناموفق / در انتظار</MudSelectItem>
</MudSelect> </MudSelect>
</MudItem> </MudItem>
<MudItem xs="12" sm="6" md="4"> <MudItem xs="12" sm="6" md="4">
<MudSelect T="int?" Clearable="true" Label="نوع تراکنش" Variant="Variant.Outlined" <DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه تاریخ" />
Margin="Margin.Dense" @bind-Value="_typeFilter">
<MudSelectItem T="int?" Value="@(0)">نامشخص</MudSelectItem>
<MudSelectItem T="int?" Value="@(1)">خرید پکیج</MudSelectItem>
<MudSelectItem T="int?" Value="@(2)">پرداخت دستی</MudSelectItem>
<MudSelectItem T="int?" Value="@(3)">شارژ کیف‌پول</MudSelectItem>
<MudSelectItem T="int?" Value="@(4)">بازگشت وجه</MudSelectItem>
</MudSelect>
</MudItem> </MudItem>
</Filters> </Filters>
<Content> <Content>
<MudDataGrid @ref="_dataGrid" T="TransactionModel" <MudGrid Class="mb-3" Spacing="2">
ServerData="LoadData" <MudItem xs="6" sm="3">
Hover="true" Dense="true" <MudPaper Class="pa-3" Outlined="true">
Height="calc(100vh - 240px)"> <MudText Typo="Typo.caption" Color="Color.Secondary">تعداد</MudText>
<MudText Typo="Typo.h6">@_summaryTotalCount.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6" sm="3">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">جمع مبلغ (تومان)</MudText>
<MudText Typo="Typo.h6">@_summaryTotalAmount.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6" sm="3">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">موفق</MudText>
<MudText Typo="Typo.h6" Color="Color.Success">@_summarySuccessful.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6" sm="3">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">در انتظار</MudText>
<MudText Typo="Typo.h6" Color="Color.Warning">@_summaryPending.ToString("N0")</MudText>
</MudPaper>
</MudItem>
</MudGrid>
<MudDataGrid @ref="_dataGrid" T="PaymentTransactionModel"
ServerData="LoadData" ReadOnly="true"
Hover="true" Dense="true" Height="calc(100vh - 380px)">
<ToolBarContent>
<MudText Typo="Typo.subtitle1">گزارش تراکنش درگاه</MudText>
<MudSpacer />
<MudButton Variant="Variant.Outlined" Color="Color.Info" Size="Size.Small"
StartIcon="@Icons.Material.Filled.FileDownload" OnClick="ExportCsv">CSV</MudButton>
</ToolBarContent>
<Columns> <Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" /> <PropertyColumn Property="x => x.Id" Title="شناسه" />
<PropertyColumn Property="x => x.RefId" Title="شماره مرجع" /> <PropertyColumn Property="x => x.UserId" Title="کاربر" />
<PropertyColumn Property="x => x.Amount" Title="مبلغ"> <PropertyColumn Property="x => x.Mobile" Title="موبایل" />
<CellTemplate> <PropertyColumn Property="x => x.GatewayProvider" Title="درگاه" />
@context.Item.Amount.ToString("N0") <TemplateColumn Title="مبلغ (تومان)">
</CellTemplate> <CellTemplate>@context.Item.Amount.ToString("N0")</CellTemplate>
</PropertyColumn> </TemplateColumn>
<PropertyColumn Property="x => x.Authority" Title="Authority" />
<PropertyColumn Property="x => x.RefId" Title="RefId" />
<PropertyColumn Property="x => x.CardPan" Title="کارت" />
<TemplateColumn Title="وضعیت"> <TemplateColumn Title="وضعیت">
<CellTemplate> <CellTemplate>
<MudChip T="string" <MudChip T="string" Size="Size.Small" Color="@(context.Item.PaymentStatus ? Color.Success : Color.Warning)">
Size="Size.Small" @ReportLabels.GetPaymentStatusLabel(context.Item.PaymentStatus)
Color="@GetStatusColor(context.Item.PaymentStatus)">
@GetStatusText(context.Item.PaymentStatus)
</MudChip> </MudChip>
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
<TemplateColumn Title="تاریخ پرداخت"> <PropertyColumn Property="x => x.VerificationStatusCode" Title="کد Verify" />
<CellTemplate> <PropertyColumn Property="x => x.OrderId" Title="OrderId" />
@context.Item.PaymentDate.ToLocalTime().MiladiToJalaliWithTime() <TemplateColumn Title="تاریخ">
</CellTemplate> <CellTemplate>@context.Item.CreatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="نوع">
<CellTemplate>
@GetTypeText(context.Item.Type)
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="عملیات">
<CellTemplate>
<MudTooltip Text="جزئیات">
<MudIconButton Icon="@Icons.Material.Filled.Info"
Size="Size.Small"
OnClick="@(() => OpenDetails(context.Item))" />
</MudTooltip>
</CellTemplate>
</TemplateColumn> </TemplateColumn>
</Columns> </Columns>
<NoRecordsContent> <NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert> <MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">
با فیلتر فعلی موردی یافت نشد — بازه تاریخ یا کاربر را تغییر دهید.
</MudAlert>
</NoRecordsContent> </NoRecordsContent>
<PagerContent> <PagerContent>
<MudDataGridPager T="TransactionModel" PageSizeOptions="@(new int[] { 20, 50, 100 })" <MudDataGridPager T="PaymentTransactionModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}" InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد در صفحه" /> RowsPerPageString="تعداد در صفحه" />
</PagerContent> </PagerContent>
@@ -80,3 +117,87 @@
</Content> </Content>
</BasePageComponent> </BasePageComponent>
@code {
private BasePageComponent? _basePage;
private MudDataGrid<PaymentTransactionModel>? _dataGrid;
private long? _userId;
private string? _mobile, _authority, _refId, _orderId, _gateway;
private bool? _paymentStatus;
private DateTime? _fromDate, _toDate;
private long _summaryTotalCount, _summaryTotalAmount, _summarySuccessful, _summaryPending;
private GetAllPaymentTransactionByFilterFilter BuildFilter() => new()
{
UserId = _userId > 0 ? _userId : null,
Mobile = string.IsNullOrWhiteSpace(_mobile) ? null : _mobile,
Authority = string.IsNullOrWhiteSpace(_authority) ? null : _authority,
RefId = string.IsNullOrWhiteSpace(_refId) ? null : _refId,
OrderId = string.IsNullOrWhiteSpace(_orderId) ? null : _orderId,
GatewayProvider = string.IsNullOrWhiteSpace(_gateway) ? null : _gateway,
PaymentStatus = _paymentStatus,
CreatedFrom = _fromDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_fromDate.Value, DateTimeKind.Utc)) : null,
CreatedTo = _toDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_toDate.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc)) : null
};
private async Task LoadSummary()
{
try
{
var s = await PaymentTxClient.GetPaymentTransactionSummaryAsync(new GetPaymentTransactionSummaryRequest { Filter = BuildFilter() });
_summaryTotalCount = s.TotalCount;
_summaryTotalAmount = s.TotalAmount;
_summarySuccessful = s.SuccessfulCount;
_summaryPending = s.PendingCount;
}
catch { }
}
private async Task<GridData<PaymentTransactionModel>> LoadData(GridState<PaymentTransactionModel> state)
{
try
{
await LoadSummary();
var result = await PaymentTxClient.GetAllPaymentTransactionByFilterAsync(new GetAllPaymentTransactionByFilterRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize },
Filter = BuildFilter()
});
return new GridData<PaymentTransactionModel>
{
Items = result?.Models?.ToList() ?? new(),
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
return new GridData<PaymentTransactionModel>();
}
}
private async Task OnFilterSubmit() { if (_dataGrid != null) await _dataGrid.ReloadServerData(); }
private async Task OnFilterCleared()
{
_userId = null; _mobile = _authority = _refId = _orderId = _gateway = null;
_paymentStatus = null; _fromDate = _toDate = null;
if (_dataGrid != null) await _dataGrid.ReloadServerData();
}
private async Task ExportCsv()
{
try
{
var result = await PaymentTxClient.GetAllPaymentTransactionByFilterAsync(new GetAllPaymentTransactionByFilterRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = 1, PageSize = 5000 },
Filter = BuildFilter()
});
var sb = new System.Text.StringBuilder();
sb.AppendLine("Id,UserId,Mobile,Gateway,Amount,Authority,RefId,Status,OrderId,Created");
foreach (var r in result.Models)
sb.AppendLine($"{r.Id},{r.UserId},{r.Mobile},{r.GatewayProvider},{r.Amount},{r.Authority},{r.RefId},{r.PaymentStatus},{r.OrderId},{r.CreatedAt.ToDateTime():yyyy-MM-dd HH:mm}");
await jsRuntime.InvokeVoidAsync("jsSaveAsFile", "payment-transactions.csv", sb.ToString());
}
catch (Exception ex) { Snackbar.Add($"خطا در export: {ex.Message}", Severity.Error); }
}
}
@@ -1,95 +0,0 @@
using BackOffice.Common.BaseComponents;
using MudBlazor;
namespace BackOffice.Pages.Payment;
public partial class Transactions
{
private BasePageComponent? _basePage;
private MudDataGrid<TransactionModel>? _dataGrid;
private DateTime? _fromDate;
private DateTime? _toDate;
private int? _statusFilter;
private int? _typeFilter;
private async Task<GridData<TransactionModel>> LoadData(GridState<TransactionModel> state)
{
// TODO: Connect to BackOffice.BFF Transactions when API is ready
await Task.CompletedTask;
return new GridData<TransactionModel>
{
Items = Array.Empty<TransactionModel>(),
TotalItems = 0
};
}
private async Task OnFilterSubmit()
{
if (_dataGrid != null)
await _dataGrid.ReloadServerData();
}
private async Task OnFilterCleared()
{
_fromDate = null;
_toDate = null;
_statusFilter = null;
_typeFilter = null;
if (_dataGrid != null)
await _dataGrid.ReloadServerData();
}
private void OpenDetails(TransactionModel model)
{
// TODO: Open TransactionDetailsDialog with model
}
private static string GetStatusText(int status)
{
return status switch
{
0 => "در انتظار",
1 => "موفق",
2 => "ناموفق",
3 => "بازگشت وجه",
_ => "نامشخص"
};
}
private static Color GetStatusColor(int status)
{
return status switch
{
0 => Color.Warning,
1 => Color.Success,
2 => Color.Error,
3 => Color.Info,
_ => Color.Default
};
}
private static string GetTypeText(int type)
{
return type switch
{
1 => "خرید پکیج",
2 => "پرداخت دستی",
3 => "شارژ کیف‌پول",
4 => "بازگشت وجه",
_ => "نامشخص"
};
}
private class TransactionModel
{
public long Id { get; set; }
public string? RefId { get; set; }
public long Amount { get; set; }
public int PaymentStatus { get; set; }
public DateTime PaymentDate { get; set; }
public int Type { get; set; }
}
}
+1 -1
View File
@@ -95,7 +95,7 @@
<MudNumericField T="long?" <MudNumericField T="long?"
HideSpinButtons="true" HideSpinButtons="true"
Label="قیمت جدید (ریال)" Label="قیمت جدید (تومان)"
@bind-Value="_newPrice" @bind-Value="_newPrice"
Variant="Variant.Outlined" Variant="Variant.Outlined"
Margin="Margin.Dense" /> Margin="Margin.Dense" />
@@ -51,7 +51,7 @@
<MudTextField T="string" @bind-Value="Model.Title" Disabled="_isLoading" Label="عنوان" Variant="Variant.Outlined" Margin="Margin.Dense" /> <MudTextField T="string" @bind-Value="Model.Title" Disabled="_isLoading" Label="عنوان" Variant="Variant.Outlined" Margin="Margin.Dense" />
</MudItem> </MudItem>
<MudItem xs="6"> <MudItem xs="6">
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.Price" Format="N0" Immediate="true" Disabled="_isLoading" Label="قیمت (ریال)" Variant="Variant.Outlined" Margin="Margin.Dense" /> <MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.Price" Format="N0" Immediate="true" Disabled="_isLoading" Label="قیمت (تومان)" Variant="Variant.Outlined" Margin="Margin.Dense" />
</MudItem> </MudItem>
</MudStack> </MudStack>
@@ -37,7 +37,7 @@
<MudTextField T="string" @bind-Value="Model.Title" Disabled="_isLoading" Label="عنوان" Variant="Variant.Outlined" Margin="Margin.Dense" /> <MudTextField T="string" @bind-Value="Model.Title" Disabled="_isLoading" Label="عنوان" Variant="Variant.Outlined" Margin="Margin.Dense" />
</MudItem> </MudItem>
<MudItem xs="6"> <MudItem xs="6">
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.Price" Format="N0" Immediate="true" Disabled="_isLoading" Label="قیمت (ریال)" Variant="Variant.Outlined" Margin="Margin.Dense" /> <MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.Price" Format="N0" Immediate="true" Disabled="_isLoading" Label="قیمت (تومان)" Variant="Variant.Outlined" Margin="Margin.Dense" />
</MudItem> </MudItem>
</MudStack> </MudStack>
@@ -128,7 +128,7 @@
<TemplateColumn Title="قیمت" CellStyle="text-wrap: nowrap;"> <TemplateColumn Title="قیمت" CellStyle="text-wrap: nowrap;">
<CellTemplate> <CellTemplate>
@context.Item.Price.ToString("N0") ریال @context.Item.Price.ToString("N0") تومان
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
@@ -174,8 +174,7 @@
<MudItem xs="12"> <MudItem xs="12">
<MudSwitch T="bool" @bind-Value="_networkConfig.SpilloverEnabled" <MudSwitch T="bool" @bind-Value="_networkConfig.SpilloverEnabled"
Color="Color.Primary" Color="Color.Primary"
Label="Spillover فعال باشد" Label="Spillover فعال باشد — اعضای اضافی به پایین درخت منتقل شوند" />
HelperText="اعضای اضافی به پایین درخت منتقل شوند" />
</MudItem> </MudItem>
</MudGrid> </MudGrid>
@@ -340,8 +339,7 @@
<MudItem xs="12"> <MudItem xs="12">
<MudSwitch @bind-Value="_systemConfig.MaintenanceMode" <MudSwitch @bind-Value="_systemConfig.MaintenanceMode"
Color="Color.Warning" Color="Color.Warning"
Label="حالت تعمیر و نگهداری" Label="حالت تعمیر و نگهداری — غیرفعال کردن موقت سیستم برای به‌روزرسانی" />
HelperText="غیرفعال کردن موقت سیستم برای به‌روزرسانی" />
</MudItem> </MudItem>
<MudItem xs="12"> <MudItem xs="12">
@@ -8,12 +8,12 @@
</MudText> </MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary"> <MudText Typo="Typo.caption" Color="Color.Secondary">
مبلغ تخفیف به‌صورت ریالی وارد شود. مبلغ تخفیف به‌صورت تومانی وارد شود.
</MudText> </MudText>
<MudNumericField T="long?" <MudNumericField T="long?"
@bind-Value="_discountAmount" @bind-Value="_discountAmount"
Label="مبلغ تخفیف (ریال)" Label="مبلغ تخفیف (تومان)"
Variant="Variant.Outlined" Variant="Variant.Outlined"
HideSpinButtons="true" /> HideSpinButtons="true" />
@@ -40,7 +40,7 @@ public partial class ApplyDiscountDialog
if (response.Success) if (response.Success)
{ {
Snackbar.Add($"تخفیف با موفقیت اعمال شد. مبلغ نهایی: {response.FinalAmount.ToString("N0")} ریال", Severity.Success); Snackbar.Add($"تخفیف با موفقیت اعمال شد. مبلغ نهایی: {response.FinalAmount.ToString("N0")} تومان", Severity.Success);
MudDialog.Close(DialogResult.Ok(true)); MudDialog.Close(DialogResult.Ok(true));
} }
else else
@@ -27,6 +27,8 @@
<MudSelectItem T="int" Value="2">تحویل پست</MudSelectItem> <MudSelectItem T="int" Value="2">تحویل پست</MudSelectItem>
<MudSelectItem T="int" Value="3">تحویل به مشتری</MudSelectItem> <MudSelectItem T="int" Value="3">تحویل به مشتری</MudSelectItem>
<MudSelectItem T="int" Value="4">مرجوع شده</MudSelectItem> <MudSelectItem T="int" Value="4">مرجوع شده</MudSelectItem>
<MudSelectItem T="int" Value="5">لغو شده</MudSelectItem>
<MudSelectItem T="int" Value="6">آماده تحویل در دفتر</MudSelectItem>
</MudSelect> </MudSelect>
</MudItem> </MudItem>
@@ -56,6 +58,24 @@
</MudItem> </MudItem>
} }
@if (_newStatus == 5)
{
<MudItem xs="12">
<MudAlert Severity="Severity.Warning">
توجه: وضعیت «لغو شده» یعنی ارسال این سفارش متوقف شده است.
</MudAlert>
</MudItem>
}
@if (_newStatus == 6)
{
<MudItem xs="12">
<MudAlert Severity="Severity.Info">
سفارش برای تحویل حضوری در دفتر آماده است.
</MudAlert>
</MudItem>
}
@if (_newStatus == 2) @if (_newStatus == 2)
{ {
<MudItem xs="12"> <MudItem xs="12">
@@ -37,6 +37,8 @@ public partial class ChangeOrderStatusDialog
2 => "تحویل پست", 2 => "تحویل پست",
3 => "تحویل به مشتری", 3 => "تحویل به مشتری",
4 => "مرجوع شده", 4 => "مرجوع شده",
5 => "لغو شده",
6 => "آماده تحویل در دفتر",
_ => "بدون ارسال / نامشخص" _ => "بدون ارسال / نامشخص"
}; };
} }
@@ -49,6 +51,8 @@ public partial class ChangeOrderStatusDialog
2 => Color.Info, 2 => Color.Info,
3 => Color.Success, 3 => Color.Success,
4 => Color.Error, 4 => Color.Error,
5 => Color.Dark,
6 => Color.Primary,
_ => Color.Default _ => Color.Default
}; };
} }
@@ -95,6 +95,8 @@
<MudSelectItem T="int" Value="2">تحویل پست</MudSelectItem> <MudSelectItem T="int" Value="2">تحویل پست</MudSelectItem>
<MudSelectItem T="int" Value="3">تحویل به مشتری</MudSelectItem> <MudSelectItem T="int" Value="3">تحویل به مشتری</MudSelectItem>
<MudSelectItem T="int" Value="4">مرجوع شده</MudSelectItem> <MudSelectItem T="int" Value="4">مرجوع شده</MudSelectItem>
<MudSelectItem T="int" Value="5">لغو شده</MudSelectItem>
<MudSelectItem T="int" Value="6">آماده تحویل در دفتر</MudSelectItem>
</MudSelect> </MudSelect>
</MudText> </MudText>
<MudTextField T="string" <MudTextField T="string"
@@ -123,6 +125,14 @@
{ {
<MudText Typo="Typo.caption">آدرسی ثبت نشده است.</MudText> <MudText Typo="Typo.caption">آدرسی ثبت نشده است.</MudText>
} }
@if (!string.IsNullOrWhiteSpace(_model.PostalCode))
{
<MudText Typo="Typo.body2">کد پستی: @_model.PostalCode</MudText>
}
@if (!string.IsNullOrWhiteSpace(_model.UserMobile))
{
<MudText Typo="Typo.body2">تلفن سفارش‌دهنده: @_model.UserMobile</MudText>
}
</MudStack> </MudStack>
</MudPaper> </MudPaper>
@@ -162,6 +172,13 @@
</MudStack> </MudStack>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<MudButton Variant="Variant.Outlined" Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.Print"
OnClick="PrintInvoiceAsync"
Disabled="_model is null || _isLoading">
چاپ فاکتور
</MudButton>
<MudSpacer />
<MudButton Variant="Variant.Outlined" Color="Color.Default" OnClick="Close"> <MudButton Variant="Variant.Outlined" Color="Color.Default" OnClick="Close">
بستن بستن
</MudButton> </MudButton>
@@ -1,6 +1,8 @@
using BackOffice.Common.Utilities;
using CMSMicroservice.Protobuf.Protos; using CMSMicroservice.Protobuf.Protos;
using CMSMicroservice.Protobuf.Protos.UserOrder; using CMSMicroservice.Protobuf.Protos.UserOrder;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor; using MudBlazor;
namespace BackOffice.Pages.UserOrder.Components; namespace BackOffice.Pages.UserOrder.Components;
@@ -9,6 +11,7 @@ public partial class UserOrderDetailsDialog
{ {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!; [CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!;
[Inject] public UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } = default!; [Inject] public UserOrderContract.UserOrderContractClient UserOrderContract { get; set; } = default!;
[Inject] private IJSRuntime JsRuntime { get; set; } = default!;
[Parameter] public long OrderId { get; set; } [Parameter] public long OrderId { get; set; }
@@ -56,6 +59,8 @@ public partial class UserOrderDetailsDialog
2 => Color.Info, // InTransit 2 => Color.Info, // InTransit
3 => Color.Success, // Delivered 3 => Color.Success, // Delivered
4 => Color.Error, // Returned 4 => Color.Error, // Returned
5 => Color.Dark, // Cancelled
6 => Color.Primary, // ReadyForOfficePickup
_ => Color.Default // None / Unknown _ => Color.Default // None / Unknown
}; };
} }
@@ -68,6 +73,8 @@ public partial class UserOrderDetailsDialog
2 => "تحویل پست", 2 => "تحویل پست",
3 => "تحویل به مشتری", 3 => "تحویل به مشتری",
4 => "مرجوع شده", 4 => "مرجوع شده",
5 => "لغو شده",
6 => "آماده تحویل در دفتر",
_ => "بدون ارسال / نامشخص" _ => "بدون ارسال / نامشخص"
}; };
} }
@@ -178,5 +185,61 @@ public partial class UserOrderDetailsDialog
MudDialog.Close(DialogResult.Ok(true)); MudDialog.Close(DialogResult.Ok(true));
} }
private async Task PrintInvoiceAsync()
{
if (_model is null) return;
var vat = _model.VatInfo;
var lines = _model.FactorDetails.Select(f =>
{
var unit = f.UnitPrice ?? 0;
var count = f.Count ?? 0;
var discountUnit = f.UnitDiscountPrice ?? 0;
var lineDiscount = Math.Max(0, (unit - discountUnit) * count);
var lineTotal = discountUnit > 0 ? discountUnit * count : unit * count;
return new OrderInvoicePrint.InvoiceLine
{
Title = f.ProductTitle ?? "",
Quantity = count,
UnitPrice = unit,
Discount = lineDiscount,
LineTotal = lineTotal
};
}).ToList();
var subtotal = lines.Sum(l => l.UnitPrice * l.Quantity);
var discountTotal = lines.Sum(l => l.Discount);
var total = vat is { TotalAmount: > 0 } ? vat.TotalAmount : _model.Amount;
var html = OrderInvoicePrint.BuildHtml(new OrderInvoicePrint.InvoiceModel
{
ShopTitle = "فروشگاه عادی — کارا بازار سلامت",
OrderId = _model.Id.ToString(),
CustomerName = _model.UserFullName,
CustomerMobile = _model.UserMobile,
CustomerNationalCode = _model.UserNationalCode,
Address = _model.UserAddressText,
PostalCode = _model.PostalCode,
PaymentStatus = _model.PaymentStatus switch
{
PaymentStatus.Success => "پرداخت شده",
PaymentStatus.Pending => "در انتظار پرداخت",
_ => "پرداخت نشده"
},
PaymentMethod = GetPaymentMethodText(_model.PaymentMethod.GetHashCode()),
TransactionId = _model.TransactionId?.ToString(),
TrackingCode = _trackingCode ?? _model.TrackingCode,
Created = null,
PaymentDate = _model.PaymentDate?.ToDateTime(),
Subtotal = subtotal,
DiscountTotal = discountTotal,
VatAmount = vat?.VatAmount ?? 0,
TotalAmount = total,
Lines = lines
});
await JsRuntime.InvokeVoidAsync("jsHtmlToPdf", html, $"فاکتور سفارش {_model.Id}");
}
private void Close() => MudDialog.Close(); private void Close() => MudDialog.Close();
} }
@@ -46,6 +46,7 @@
<MudSelectItem T="int?" Value="@(3)">تحویل به مشتری</MudSelectItem> <MudSelectItem T="int?" Value="@(3)">تحویل به مشتری</MudSelectItem>
<MudSelectItem T="int?" Value="@(4)">مرجوع شده</MudSelectItem> <MudSelectItem T="int?" Value="@(4)">مرجوع شده</MudSelectItem>
<MudSelectItem T="int?" Value="@(5)">لغو شده</MudSelectItem> <MudSelectItem T="int?" Value="@(5)">لغو شده</MudSelectItem>
<MudSelectItem T="int?" Value="@(6)">آماده تحویل در دفتر</MudSelectItem>
</MudSelect> </MudSelect>
</MudItem> </MudItem>
<MudItem xs="12" sm="6" md="3"> <MudItem xs="12" sm="6" md="3">
@@ -113,13 +114,13 @@
<MudItem xs="12" md="3"> <MudItem xs="12" md="3">
<MudPaper Elevation="1" Class="pa-4 text-center"> <MudPaper Elevation="1" Class="pa-4 text-center">
<MudText Typo="Typo.subtitle2" Color="Color.Success">مجموع فروش</MudText> <MudText Typo="Typo.subtitle2" Color="Color.Success">مجموع فروش</MudText>
<MudText Typo="Typo.h5">@_totalSales.ToString("N0") ریال</MudText> <MudText Typo="Typo.h5">@_totalSales.ToString("N0") تومان</MudText>
</MudPaper> </MudPaper>
</MudItem> </MudItem>
<MudItem xs="12" md="3"> <MudItem xs="12" md="3">
<MudPaper Elevation="1" Class="pa-4 text-center"> <MudPaper Elevation="1" Class="pa-4 text-center">
<MudText Typo="Typo.subtitle2" Color="Color.Info">میانگین مبلغ سفارش</MudText> <MudText Typo="Typo.subtitle2" Color="Color.Info">میانگین مبلغ سفارش</MudText>
<MudText Typo="Typo.h5">@_averageOrder.ToString("N0") ریال</MudText> <MudText Typo="Typo.h5">@_averageOrder.ToString("N0") تومان</MudText>
</MudPaper> </MudPaper>
</MudItem> </MudItem>
<MudItem xs="12" md="3"> <MudItem xs="12" md="3">
@@ -237,7 +238,7 @@
</TemplateColumn> </TemplateColumn>
<TemplateColumn Title="مبلغ"> <TemplateColumn Title="مبلغ">
<CellTemplate> <CellTemplate>
@context.Item.Amount.ToString("N0") ریال @context.Item.Amount.ToString("N0") تومان
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
<TemplateColumn Title="وضعیت پرداخت"> <TemplateColumn Title="وضعیت پرداخت">
@@ -1,4 +1,5 @@
using System.Text; using System.Text;
using BackOffice.Common.Utilities;
using CMSMicroservice.Protobuf.Protos; using CMSMicroservice.Protobuf.Protos;
using CMSMicroservice.Protobuf.Protos.UserOrder; using CMSMicroservice.Protobuf.Protos.UserOrder;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@@ -215,7 +216,8 @@ public partial class OrderSalesReports
2 => Color.Info, 2 => Color.Info,
3 => Color.Success, 3 => Color.Success,
4 => Color.Error, 4 => Color.Error,
5 => Color.Error, 5 => Color.Dark,
6 => Color.Primary,
_ => Color.Default _ => Color.Default
}; };
} }
@@ -229,6 +231,7 @@ public partial class OrderSalesReports
3 => "تحویل به مشتری", 3 => "تحویل به مشتری",
4 => "مرجوع شده", 4 => "مرجوع شده",
5 => "لغو شده", 5 => "لغو شده",
6 => "آماده تحویل در دفتر",
_ => "بدون ارسال / نامشخص" _ => "بدون ارسال / نامشخص"
}; };
} }
@@ -241,27 +244,26 @@ public partial class OrderSalesReports
return; return;
} }
var sb = new StringBuilder(); var headers = new[]
sb.AppendLine("شناسه,کاربر,نام کاربر,مبلغ,وضعیت پرداخت,وضعیت ارسال,تاریخ پرداخت"); {
"شناسه", "کاربر", "نام مشتری", "مبلغ", "وضعیت پرداخت", "وضعیت ارسال", "تاریخ پرداخت"
foreach (var o in _orders) };
var rows = _orders.Select(o => new object?[]
{ {
sb.AppendLine(string.Join(",",
o.Id, o.Id,
o.UserId, o.UserId,
EscapeCsv(o.UserFullName), CsvExportHelper.CustomerName(o.UserFullName, userId: o.UserId),
o.Amount, o.Amount,
EscapeCsv(o.PaymentStatus == PaymentStatus.Success ? "پرداخت شده" : "پرداخت نشده"), o.PaymentStatus == PaymentStatus.Success ? "پرداخت شده" : "پرداخت نشده",
EscapeCsv(GetDeliveryStatusText(o.DeliveryStatus.GetHashCode())), GetDeliveryStatusText(o.DeliveryStatus.GetHashCode()),
o.PaymentDate != null ? o.PaymentDate.ToDateTime().MiladiToJalaliWithTime() : "-")); o.PaymentDate != null ? o.PaymentDate.ToDateTime().MiladiToJalaliWithTime() : "-"
} });
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray(); var filename = $"store-sales-{DateTime.Now:yyyyMMddHHmmss}.xls";
var base64 = Convert.ToBase64String(bytes); await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
var filename = $"store-sales-{DateTime.Now:yyyyMMddHHmmss}.csv"; CsvExportHelper.ToExcelXmlBase64("گزارش فروش", headers, rows));
Snackbar.Add("خروجی Excel آماده شد.", Severity.Success);
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
} }
protected async Task ExportToPdf() protected async Task ExportToPdf()
@@ -279,8 +281,8 @@ public partial class OrderSalesReports
sb.AppendLine("خلاصه:"); sb.AppendLine("خلاصه:");
sb.AppendLine($"تعداد سفارش‌ها: {_totalOrders}"); sb.AppendLine($"تعداد سفارش‌ها: {_totalOrders}");
sb.AppendLine($"مجموع فروش: {_totalSales:N0} ریال"); sb.AppendLine($"مجموع فروش: {_totalSales:N0} تومان");
sb.AppendLine($"میانگین مبلغ سفارش: {_averageOrder:N0} ریال"); sb.AppendLine($"میانگین مبلغ سفارش: {_averageOrder:N0} تومان");
sb.AppendLine($"پرداخت شده: {_paidCount}"); sb.AppendLine($"پرداخت شده: {_paidCount}");
sb.AppendLine(); sb.AppendLine();
@@ -290,7 +292,7 @@ public partial class OrderSalesReports
foreach (var o in _orders) foreach (var o in _orders)
{ {
sb.AppendLine( sb.AppendLine(
$"{o.Id} | {o.UserFullName} | {o.Amount:N0} | {(o.PaymentStatus == PaymentStatus.Success ? "پرداخت شده" : "پرداخت نشده")} | {GetDeliveryStatusText(o.DeliveryStatus.GetHashCode())} | {(o.PaymentDate != null ? o.PaymentDate.ToDateTime().MiladiToJalaliWithTime() : "-")}"); $"{o.Id} | {CsvExportHelper.CustomerName(o.UserFullName, userId: o.UserId)} | {o.Amount:N0} | {(o.PaymentStatus == PaymentStatus.Success ? "پرداخت شده" : "پرداخت نشده")} | {GetDeliveryStatusText(o.DeliveryStatus.GetHashCode())} | {(o.PaymentDate != null ? o.PaymentDate.ToDateTime().MiladiToJalaliWithTime() : "-")}");
} }
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray(); var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
@@ -349,12 +351,4 @@ public partial class OrderSalesReports
} }
}; };
} }
private static string EscapeCsv(string value)
{
if (string.IsNullOrEmpty(value)) return "";
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
return $"\"{value.Replace("\"", "\"\"")}\"";
return value;
}
} }
@@ -27,11 +27,11 @@
@bind-Value="@_request.Filter.Amount" Variant="Variant.Outlined" Margin="Margin.Dense"/> @bind-Value="@_request.Filter.Amount" Variant="Variant.Outlined" Margin="Margin.Dense"/>
</MudItem> </MudItem>
<MudItem xs="12" sm="6" md="3"> <MudItem xs="12" sm="6" md="3">
<MudDatePicker T="DateTime?" Label="از تاریخ پرداخت" @bind-Date="_paymentDateFrom" <MudDatePicker Label="از تاریخ پرداخت" @bind-Date="_paymentDateFrom"
Variant="Variant.Outlined" Margin="Margin.Dense"/> Variant="Variant.Outlined" Margin="Margin.Dense"/>
</MudItem> </MudItem>
<MudItem xs="12" sm="6" md="3"> <MudItem xs="12" sm="6" md="3">
<MudDatePicker T="DateTime?" Label="تا تاریخ پرداخت" @bind-Date="_paymentDateTo" <MudDatePicker Label="تا تاریخ پرداخت" @bind-Date="_paymentDateTo"
Variant="Variant.Outlined" Margin="Margin.Dense"/> Variant="Variant.Outlined" Margin="Margin.Dense"/>
</MudItem> </MudItem>
<MudItem xs="12" sm="6" md="3"> <MudItem xs="12" sm="6" md="3">
@@ -56,6 +56,7 @@
<MudSelectItem T="int?" Value="@(3)">تحویل به مشتری</MudSelectItem> <MudSelectItem T="int?" Value="@(3)">تحویل به مشتری</MudSelectItem>
<MudSelectItem T="int?" Value="@(4)">مرجوع شده</MudSelectItem> <MudSelectItem T="int?" Value="@(4)">مرجوع شده</MudSelectItem>
<MudSelectItem T="int?" Value="@(5)">لغو شده</MudSelectItem> <MudSelectItem T="int?" Value="@(5)">لغو شده</MudSelectItem>
<MudSelectItem T="int?" Value="@(6)">آماده تحویل در دفتر</MudSelectItem>
</MudSelect> </MudSelect>
</MudItem> </MudItem>
</Filters> </Filters>
@@ -1,8 +1,9 @@
using System.Text.Json; using System.Text;
using System.Text; using System.Text.Json;
using BackOffice.Common.BaseComponents;
using BackOffice.Common.Utilities;
using CMSMicroservice.Protobuf.Protos; using CMSMicroservice.Protobuf.Protos;
using CMSMicroservice.Protobuf.Protos.UserOrder; using CMSMicroservice.Protobuf.Protos.UserOrder;
using BackOffice.Common.BaseComponents;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using MudBlazor; using MudBlazor;
@@ -213,6 +214,8 @@ public partial class UserOrderMainPage
2 => Color.Info, // InTransit 2 => Color.Info, // InTransit
3 => Color.Success, // Delivered 3 => Color.Success, // Delivered
4 => Color.Error, // Returned 4 => Color.Error, // Returned
5 => Color.Dark, // Cancelled
6 => Color.Primary, // ReadyForOfficePickup
_ => Color.Default // None / Unknown _ => Color.Default // None / Unknown
}; };
} }
@@ -225,6 +228,8 @@ public partial class UserOrderMainPage
2 => "تحویل پست", 2 => "تحویل پست",
3 => "تحویل به مشتری", 3 => "تحویل به مشتری",
4 => "مرجوع شده", 4 => "مرجوع شده",
5 => "لغو شده",
6 => "آماده تحویل در دفتر",
_ => "بدون ارسال / نامشخص" _ => "بدون ارسال / نامشخص"
}; };
} }
@@ -238,6 +243,15 @@ public partial class UserOrderMainPage
}; };
} }
// Domain PaymentStatus: Success=0, Reject=1, Pending=2
private static string GetPaymentStatusText(int status) => status switch
{
0 => "پرداخت شده",
1 => "پرداخت ناموفق",
2 => "در انتظار پرداخت",
_ => "نامشخص"
};
private async Task OpenChangeStatus(DataModel model) private async Task OpenChangeStatus(DataModel model)
{ {
var parameters = new DialogParameters var parameters = new DialogParameters
@@ -315,40 +329,31 @@ public partial class UserOrderMainPage
return; return;
} }
var sb = new StringBuilder(); var headers = new[]
sb.AppendLine("شناسه,کاربر,نام کاربر,مبلغ,وضعیت پرداخت,وضعیت ارسال,روش پرداخت,تاریخ پرداخت"); {
"شناسه", "کاربر", "نام مشتری", "مبلغ", "وضعیت پرداخت", "وضعیت ارسال", "روش پرداخت", "تاریخ پرداخت"
foreach (var o in result.Models) };
var rows = result.Models.Select(o => new object?[]
{ {
sb.AppendLine(string.Join(",",
o.Id, o.Id,
o.UserId, o.UserId,
EscapeCsv(o.UserFullName), CsvExportHelper.CustomerName(o.UserFullName, userId: o.UserId),
o.Amount, o.Amount,
EscapeCsv(o.PaymentStatus.ToString()), GetPaymentStatusText(o.PaymentStatus.GetHashCode()),
EscapeCsv(GetDeliveryStatusText(o.DeliveryStatus.GetHashCode())), GetDeliveryStatusText(o.DeliveryStatus.GetHashCode()),
EscapeCsv(GetPaymentMethodText(o.PaymentMethod.GetHashCode())), GetPaymentMethodText(o.PaymentMethod.GetHashCode()),
o.PaymentDate?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-")); o.PaymentDate?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-"
} });
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray(); var filename = $"user-orders-{DateTime.Now:yyyyMMddHHmmss}.xls";
var base64 = Convert.ToBase64String(bytes); await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
var filename = $"user-orders-{DateTime.Now:yyyyMMddHHmmss}.csv"; CsvExportHelper.ToExcelXmlBase64("سفارش‌ها", headers, rows));
Snackbar.Add("خروجی Excel آماده شد.", Severity.Success);
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
} }
catch (Exception ex) catch (Exception ex)
{ {
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error); Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
} }
} }
private static string EscapeCsv(string value)
{
if (string.IsNullOrEmpty(value)) return "";
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
return $"\"{value.Replace("\"", "\"\"")}\"";
return value;
}
} }
@@ -2,11 +2,11 @@
@attribute [Authorize(Roles = "Administrator")] @attribute [Authorize(Roles = "Administrator")]
@using CMSMicroservice.Protobuf.Protos.UserWallet @using CMSMicroservice.Protobuf.Protos.UserWallet
@using CMSMicroservice.Protobuf.Protos.UserWalletChangeLog @using CMSMicroservice.Protobuf.Protos.UserWalletHistory
@using BackOffice.Pages.AutoComplete @using BackOffice.Pages.AutoComplete
@inject UserWalletContract.UserWalletContractClient WalletClient @inject UserWalletContract.UserWalletContractClient WalletClient
@inject UserWalletChangeLogContract.UserWalletChangeLogContractClient ChangeLogClient @inject UserWalletHistoryContract.UserWalletHistoryContractClient ChangeLogClient
<div> <div>
<MudText Typo="Typo.h5" Class="mb-3">مدیریت کیف‌پول کاربران</MudText> <MudText Typo="Typo.h5" Class="mb-3">مدیریت کیف‌پول کاربران</MudText>
@@ -30,15 +30,27 @@
</ToolBarContent> </ToolBarContent>
<Columns> <Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" /> <PropertyColumn Property="x => x.Id" Title="شناسه" />
<PropertyColumn Property="x => x.UserId" Title="کاربر" /> <PropertyColumn Property="x => x.UserId" Title="کاربر">
<CellTemplate>
<MudStack Spacing="0">
<MudText Typo="Typo.body2"><strong>@context.Item.UserName</strong></MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">ID: @context.Item.UserId</MudText>
</MudStack>
</CellTemplate>
</PropertyColumn>
<TemplateColumn Title="موجودی اصلی"> <TemplateColumn Title="موجودی اصلی">
<CellTemplate> <CellTemplate>
<MudText Color="Color.Primary"><strong>@context.Item.Balance.ToString("N0")</strong> ریال</MudText> <MudText Color="Color.Primary"><strong>@context.Item.Balance.ToString("N0")</strong> تومان</MudText>
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
<TemplateColumn Title="موجودی شبکه"> <TemplateColumn Title="پاداش های دریافتی">
<CellTemplate> <CellTemplate>
<MudText Color="Color.Success">@context.Item.NetworkBalance.ToString("N0") ریال</MudText> <MudText Color="Color.Success">@context.Item.NetworkBalance.ToString("N0") تومان</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="موجودی اعتباری">
<CellTemplate>
<MudText Color="Color.Tertiary">@context.Item.DiscountBalance.ToString("N0") تومان</MudText>
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
<TemplateColumn Title="عملیات" Sortable="false"> <TemplateColumn Title="عملیات" Sortable="false">
@@ -78,7 +90,7 @@
OnClick="ApplyLogFilter">جستجو</MudButton> OnClick="ApplyLogFilter">جستجو</MudButton>
</MudStack> </MudStack>
<MudDataGrid T="GetAllUserWalletChangeLogByFilterResponseModel" @ref="_logGrid" <MudDataGrid T="GetAllUserWalletHistoryByFilterResponseModel" @ref="_logGrid"
ServerData="LoadChangeLogs" ServerData="LoadChangeLogs"
Hover="true" Dense="true" Hover="true" Dense="true"
Height="calc(100vh - 340px)"> Height="calc(100vh - 340px)">
@@ -103,16 +115,26 @@
<MudText Color="Color.Primary">@context.Item.CurrentBalance.ToString("N0")</MudText> <MudText Color="Color.Primary">@context.Item.CurrentBalance.ToString("N0")</MudText>
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
<TemplateColumn Title="تغییر شبکه"> <TemplateColumn Title="تغییر پاداش های دریافتی">
<CellTemplate> <CellTemplate>
<MudText>@context.Item.ChangeNerworkValue.ToString("N0")</MudText> <MudText>@context.Item.ChangeNerworkValue.ToString("N0")</MudText>
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
<TemplateColumn Title="موجودی شبکه فعلی"> <TemplateColumn Title="پاداش های دریافتی فعلی">
<CellTemplate> <CellTemplate>
<MudText Color="Color.Success">@context.Item.CurrentNetworkBalance.ToString("N0")</MudText> <MudText Color="Color.Success">@context.Item.CurrentNetworkBalance.ToString("N0")</MudText>
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
<TemplateColumn Title="تغییر موجودی اعتباری">
<CellTemplate>
<MudText>@context.Item.ChangeDiscountValue.ToString("N0")</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="موجودی اعتباری">
<CellTemplate>
<MudText Color="Color.Tertiary">@context.Item.CurrentDiscountBalance.ToString("N0")</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="شناسه مرجع"> <TemplateColumn Title="شناسه مرجع">
<CellTemplate> <CellTemplate>
@(context.Item.RefrenceId?.ToString() ?? "—") @(context.Item.RefrenceId?.ToString() ?? "—")
@@ -128,7 +150,7 @@
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert> <MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent> </NoRecordsContent>
<PagerContent> <PagerContent>
<MudDataGridPager T="GetAllUserWalletChangeLogByFilterResponseModel" PageSizeOptions="@(new int[] { 20, 50, 100 })" <MudDataGridPager T="GetAllUserWalletHistoryByFilterResponseModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}" InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد در صفحه" /> RowsPerPageString="تعداد در صفحه" />
</PagerContent> </PagerContent>
@@ -140,7 +162,7 @@
@code { @code {
private int _activeTab; private int _activeTab;
private MudDataGrid<GetAllUserWalletByFilterResponseModel>? _walletGrid; private MudDataGrid<GetAllUserWalletByFilterResponseModel>? _walletGrid;
private MudDataGrid<GetAllUserWalletChangeLogByFilterResponseModel>? _logGrid; private MudDataGrid<GetAllUserWalletHistoryByFilterResponseModel>? _logGrid;
// Wallet filters // Wallet filters
private long? _filterUserId; private long? _filterUserId;
@@ -176,14 +198,14 @@
} }
} }
private async Task<GridData<GetAllUserWalletChangeLogByFilterResponseModel>> LoadChangeLogs(GridState<GetAllUserWalletChangeLogByFilterResponseModel> state) private async Task<GridData<GetAllUserWalletHistoryByFilterResponseModel>> LoadChangeLogs(GridState<GetAllUserWalletHistoryByFilterResponseModel> state)
{ {
try try
{ {
var request = new GetAllUserWalletChangeLogByFilterRequest var request = new GetAllUserWalletHistoryByFilterRequest
{ {
PaginationState = new() { PageNumber = state.Page + 1, PageSize = state.PageSize }, PaginationState = new() { PageNumber = state.Page + 1, PageSize = state.PageSize },
Filter = new GetAllUserWalletChangeLogByFilterFilter() Filter = new GetAllUserWalletHistoryByFilterFilter()
}; };
if (_logFilterUserId.HasValue && _logFilterUserId.Value > 0) if (_logFilterUserId.HasValue && _logFilterUserId.Value > 0)
@@ -192,8 +214,8 @@
if (_logFilterIsIncrease.HasValue) if (_logFilterIsIncrease.HasValue)
request.Filter.IsIncrease = _logFilterIsIncrease.Value; request.Filter.IsIncrease = _logFilterIsIncrease.Value;
var result = await ChangeLogClient.GetAllUserWalletChangeLogByFilterAsync(request); var result = await ChangeLogClient.GetAllUserWalletHistoryByFilterAsync(request);
return new GridData<GetAllUserWalletChangeLogByFilterResponseModel> return new GridData<GetAllUserWalletHistoryByFilterResponseModel>
{ {
Items = result?.Models?.ToList() ?? new(), Items = result?.Models?.ToList() ?? new(),
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0) TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
@@ -202,7 +224,7 @@
catch (Exception ex) catch (Exception ex)
{ {
Snackbar.Add($"خطا: {ex.Message}", Severity.Error); Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
return new GridData<GetAllUserWalletChangeLogByFilterResponseModel>(); return new GridData<GetAllUserWalletHistoryByFilterResponseModel>();
} }
} }
@@ -102,6 +102,7 @@ public class DiscountOrderService : IDiscountOrderService
DiscountBalanceUsed = response.DiscountBalanceUsed, DiscountBalanceUsed = response.DiscountBalanceUsed,
GatewayAmount = response.GatewayAmount, GatewayAmount = response.GatewayAmount,
PaymentCompleted = response.PaymentCompleted, PaymentCompleted = response.PaymentCompleted,
PaymentStatusValue = (int)response.PaymentStatus,
TransactionId = response.TransactionId, TransactionId = response.TransactionId,
Status = MapFromProtoStatus(response.DeliveryStatus, response.PaymentCompleted), Status = MapFromProtoStatus(response.DeliveryStatus, response.PaymentCompleted),
TrackingCode = response.TrackingCode, TrackingCode = response.TrackingCode,
@@ -200,12 +201,13 @@ public class DiscountOrderService : IDiscountOrderService
return status switch return status switch
{ {
OrderStatus.Pending => ProtoDeliveryStatus.DeliveryPending, OrderStatus.Pending => ProtoDeliveryStatus.DeliveryPending,
OrderStatus.Paid => ProtoDeliveryStatus.DeliveryPending, // Paid but not processed yet OrderStatus.Paid => ProtoDeliveryStatus.DeliveryPending,
OrderStatus.Processing => ProtoDeliveryStatus.DeliveryProcessing, OrderStatus.Processing => ProtoDeliveryStatus.DeliveryProcessing,
OrderStatus.Shipped => ProtoDeliveryStatus.DeliveryShipped, OrderStatus.Shipped => ProtoDeliveryStatus.DeliveryShipped,
OrderStatus.Delivered => ProtoDeliveryStatus.DeliveryDelivered, OrderStatus.Delivered => ProtoDeliveryStatus.DeliveryDelivered,
OrderStatus.Cancelled => ProtoDeliveryStatus.DeliveryCancelled, OrderStatus.Cancelled => ProtoDeliveryStatus.DeliveryCancelled,
OrderStatus.Returned => ProtoDeliveryStatus.DeliveryCancelled, // Treat as cancelled OrderStatus.ReadyForOfficePickup => ProtoDeliveryStatus.DeliveryReadyForOffice,
OrderStatus.Returned => ProtoDeliveryStatus.DeliveryReturned,
_ => ProtoDeliveryStatus.DeliveryPending _ => ProtoDeliveryStatus.DeliveryPending
}; };
} }
@@ -221,6 +223,8 @@ public class DiscountOrderService : IDiscountOrderService
ProtoDeliveryStatus.DeliveryShipped => OrderStatus.Shipped, ProtoDeliveryStatus.DeliveryShipped => OrderStatus.Shipped,
ProtoDeliveryStatus.DeliveryDelivered => OrderStatus.Delivered, ProtoDeliveryStatus.DeliveryDelivered => OrderStatus.Delivered,
ProtoDeliveryStatus.DeliveryCancelled => OrderStatus.Cancelled, ProtoDeliveryStatus.DeliveryCancelled => OrderStatus.Cancelled,
ProtoDeliveryStatus.DeliveryReadyForOffice => OrderStatus.ReadyForOfficePickup,
ProtoDeliveryStatus.DeliveryReturned => OrderStatus.Returned,
_ => OrderStatus.Pending _ => OrderStatus.Pending
}; };
} }
@@ -235,6 +239,8 @@ public class DiscountOrderService : IDiscountOrderService
ProtoDeliveryStatus.DeliveryShipped => OrderStatus.Shipped, ProtoDeliveryStatus.DeliveryShipped => OrderStatus.Shipped,
ProtoDeliveryStatus.DeliveryDelivered => OrderStatus.Delivered, ProtoDeliveryStatus.DeliveryDelivered => OrderStatus.Delivered,
ProtoDeliveryStatus.DeliveryCancelled => OrderStatus.Cancelled, ProtoDeliveryStatus.DeliveryCancelled => OrderStatus.Cancelled,
ProtoDeliveryStatus.DeliveryReadyForOffice => OrderStatus.ReadyForOfficePickup,
ProtoDeliveryStatus.DeliveryReturned => OrderStatus.Returned,
_ => OrderStatus.Pending _ => OrderStatus.Pending
}; };
} }
@@ -33,7 +33,8 @@ public enum OrderStatus
Shipped = 3, // DELIVERY_SHIPPED Shipped = 3, // DELIVERY_SHIPPED
Delivered = 4, // DELIVERY_DELIVERED Delivered = 4, // DELIVERY_DELIVERED
Cancelled = 5, // DELIVERY_CANCELLED Cancelled = 5, // DELIVERY_CANCELLED
Returned = 6 // Not in proto - handle in service Returned = 6, // DELIVERY_RETURNED
ReadyForOfficePickup = 7 // DELIVERY_READY_FOR_OFFICE
} }
public class DiscountOrderDto public class DiscountOrderDto
@@ -62,11 +63,17 @@ public class DiscountOrderDetailsDto
public long Id { get; set; } public long Id { get; set; }
public long UserId { get; set; } public long UserId { get; set; }
public string OrderNumber { get; set; } = string.Empty; public string OrderNumber { get; set; } = string.Empty;
/// <summary>از ردیف لیست پر می‌شود (GetOrderById این فیلدها را ندارد)</summary>
public string UserFullName { get; set; } = string.Empty;
public string UserMobile { get; set; } = string.Empty;
public AddressInfoDto? Address { get; set; } public AddressInfoDto? Address { get; set; }
public long TotalPrice { get; set; } public long TotalPrice { get; set; }
public long DiscountBalanceUsed { get; set; } public long DiscountBalanceUsed { get; set; }
public long GatewayAmount { get; set; } public long GatewayAmount { get; set; }
public long VatAmount { get; set; }
public bool PaymentCompleted { get; set; } public bool PaymentCompleted { get; set; }
/// <summary>مقدار proto PaymentStatus: 0 Pending, 1 Completed, 2 Failed, 3 Refunded</summary>
public int PaymentStatusValue { get; set; }
public string? TransactionId { get; set; } public string? TransactionId { get; set; }
public OrderStatus Status { get; set; } public OrderStatus Status { get; set; }
public string? TrackingCode { get; set; } public string? TrackingCode { get; set; }
@@ -96,6 +96,7 @@
// Payment // Payment
"payment" => "پرداخت", "payment" => "پرداخت",
"transactions" => "تراکنش‌ها", "transactions" => "تراکنش‌ها",
"ledger-transactions" => "دفتر تراکنش‌های مالی",
"manual-payments" => "پرداخت دستی", "manual-payments" => "پرداخت دستی",
// Network // Network
+24 -1
View File
@@ -75,7 +75,12 @@
<MudNavLink Match="NavLinkMatch.Prefix" <MudNavLink Match="NavLinkMatch.Prefix"
Href="/club/members" Href="/club/members"
Icon="@Icons.Material.Filled.Groups"> Icon="@Icons.Material.Filled.Groups">
اعضا و آمار اعضا و گزارش‌ها
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/club/customer-packages"
Icon="@Icons.Material.Filled.PeopleAlt">
گزارش مشتریان پکیج
</MudNavLink> </MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix" <MudNavLink Match="NavLinkMatch.Prefix"
Href="/club/features" Href="/club/features"
@@ -200,6 +205,24 @@
</MudNavLink> </MudNavLink>
} }
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/payment/manual-credit-charges"
Icon="@Icons.Material.Filled.AddCard">
شارژ دستی کیف پول
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/payment/transactions"
Icon="@Icons.Material.Filled.ReceiptLong">
گزارش تراکنش درگاه
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix"
Href="/payment/ledger-transactions"
Icon="@Icons.Material.Filled.AccountBalance">
دفتر تراکنش‌های مالی
</MudNavLink>
<MudNavLink Match="NavLinkMatch.Prefix" <MudNavLink Match="NavLinkMatch.Prefix"
Href="/wallets" Href="/wallets"
Icon="@Icons.Material.Filled.AccountBalanceWallet"> Icon="@Icons.Material.Filled.AccountBalanceWallet">
+30
View File
@@ -0,0 +1,30 @@
#!/bin/sh
# Generate nginx config with blazor-environment header
# BLAZOR_ENVIRONMENT defaults to "Staging" if not set
BLAZOR_ENV="${BLAZOR_ENVIRONMENT:-Staging}"
cat > /etc/nginx/conf.d/default.conf <<NGINX_CONF
server {
listen 80;
server_name _;
location / {
root /usr/share/nginx/html;
try_files \$uri \$uri/ /index.html;
add_header blazor-environment $BLAZOR_ENV;
}
}
NGINX_CONF
# Inject GW_URL into all appsettings JSON files
# This overrides GwUrl regardless of which environment Blazor detects,
# which is critical because the service worker cache strips the
# blazor-environment header and Blazor falls back to Production.
WWWROOT="/usr/share/nginx/html"
if [ -n "$GW_URL" ]; then
echo "[entrypoint] Setting GwUrl to $GW_URL in all appsettings files"
for f in "$WWWROOT"/appsettings*.json; do
[ -f "$f" ] && sed -i "s|\"GwUrl\":\s*\"[^\"]*\"|\"GwUrl\": \"$GW_URL\"|g" "$f"
done
fi
exec nginx -g 'daemon off;'
+47 -4
View File
@@ -94,8 +94,9 @@
.admin-node-card .node-meta { .admin-node-card .node-meta {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 4px;
margin-top: 2px; margin-top: 3px;
flex-wrap: wrap;
} }
.admin-node-card .level-badge { .admin-node-card .level-badge {
@@ -124,8 +125,8 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-top: 6px; margin-top: 5px;
padding-top: 6px; padding-top: 5px;
border-top: 1px solid #f0f0f0; border-top: 1px solid #f0f0f0;
font-size: 10px; font-size: 10px;
} }
@@ -204,6 +205,48 @@
animation: pulse-highlight 1.5s ease-in-out infinite; 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 { @keyframes pulse-highlight {
0%, 100% { transform: scale(1); opacity: 1; } 0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.2); opacity: 0.8; } 50% { transform: scale(1.2); opacity: 0.8; }
+23 -7
View File
@@ -39,8 +39,8 @@ window.AdminOrgChart = {
this.chart = new d3.OrgChart() this.chart = new d3.OrgChart()
.container('#' + containerId) .container('#' + containerId)
.data(data) .data(data)
.nodeWidth((d) => 160) .nodeWidth((d) => 178)
.nodeHeight((d) => 80) .nodeHeight((d) => 92)
.childrenMargin((d) => 60) .childrenMargin((d) => 60)
.compactMarginBetween((d) => 20) .compactMarginBetween((d) => 20)
.compactMarginPair((d) => 20) .compactMarginPair((d) => 20)
@@ -83,12 +83,19 @@ window.AdminOrgChart = {
</div>`; </div>`;
} }
// Highlight badge for target week matches // نوع فعالسازی - inline pill در meta row
let highlightBadge = ''; let activationTypePill = '';
if (weekFilterActive && isTargetWeek) { if (data.isNewActivation === true) {
highlightBadge = '<div class="target-week-highlight">✨</div>'; 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 // Avatar - first letter of name
const firstChar = data.userName ? data.userName.charAt(0).toUpperCase() : '?'; const firstChar = data.userName ? data.userName.charAt(0).toUpperCase() : '?';
@@ -114,6 +121,14 @@ window.AdminOrgChart = {
if (data.activationWeekDefinitionId) { if (data.activationWeekDefinitionId) {
tooltipLines.push(`📆 هفته: ${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) { if (weekFilterActive) {
tooltipLines.push(isTargetWeek ? '🎯 فعال در هفته انتخابی' : '⚪ خارج از هفته انتخابی'); tooltipLines.push(isTargetWeek ? '🎯 فعال در هفته انتخابی' : '⚪ خارج از هفته انتخابی');
} }
@@ -125,14 +140,15 @@ window.AdminOrgChart = {
data-user-id="${data.userId}" data-user-id="${data.userId}"
title="${tooltipText}"> title="${tooltipText}">
${weekIndicator} ${weekIndicator}
${highlightBadge}
<div class="node-header"> <div class="node-header">
<div class="node-avatar">${firstChar}</div> <div class="node-avatar">${firstChar}</div>
<div class="node-main-info"> <div class="node-main-info">
<div class="node-name">${data.userName || 'کاربر ' + data.userId}</div> <div class="node-name">${data.userName || 'کاربر ' + data.userId}</div>
${packageBadge}
<div class="node-meta"> <div class="node-meta">
${levelBadge} ${levelBadge}
${legText ? `<span class="${legClass}">${legText}</span>` : ''} ${legText ? `<span class="${legClass}">${legText}</span>` : ''}
${activationTypePill}
</div> </div>
</div> </div>
</div> </div>
@@ -9,7 +9,7 @@ self.addEventListener('fetch', event => event.respondWith(onFetch(event)));
const cacheNamePrefix = 'offline-cache-'; const cacheNamePrefix = 'offline-cache-';
const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`; const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`;
const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ]; const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ];
const offlineAssetsExclude = [ /^service-worker\.js$/ ]; const offlineAssetsExclude = [ /^service-worker\.js$/, /^appsettings.*\.json$/ ];
async function onInstall(event) { async function onInstall(event) {
console.info('Service worker: Install'); console.info('Service worker: Install');
+6
View File
@@ -0,0 +1,6 @@
{
"sdk": {
"version": "9.0.306",
"rollForward": "latestPatch"
}
}