Compare commits

...

126 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
masoodafar-web e541aec115 merge: kub-stage → production (discount autocomplete + smart AddStockDialog + site pages admin)
Build and Deploy to Production / build-and-deploy (push) Successful in 8m9s
2026-02-18 01:01:29 +03:30
masoodafar-web c9affe62bd feat: smart product autocomplete for inventory dialogs
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m14s
- DiscountProductsAutoComplete: new autocomplete component for discount
  products with search/debounce (mirrors ProductsAutoComplete pattern)
- AddStockDialog: replace manual product ID input with smart autocomplete
  - When 'regular product' selected → shows ProductsAutoComplete
  - When 'discount product' selected → shows DiscountProductsAutoComplete
  - Admin never has to type product IDs manually
  - Product type selector controls which autocomplete is shown
- Validates product selection before submit
2026-02-18 00:43:12 +03:30
masoodafar-web 6147499f9c feat: replace raw JSON editor with Shopify-style typed settings editors
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m7s
- Create PageSettingsModels.cs: typed DTOs for landing, about, contact, licenses
- LandingSettingsEditor: accordion sections for hero buttons, trust badges, steps,
  features, stats, testimonials, FAQs, CTA — all with add/remove list management
- AboutSettingsEditor: vision/mission text + icon fields + section titles
- ContactSettingsEditor: address, phone, email, working hours + social media links
- LicensesSettingsEditor: page description + display style selector
- PageSettingsEditDialog: auto-switch editor by PageKey, JSON↔Model serialize/deserialize
- Fallback to raw JSON textarea for unknown page keys
2026-02-17 23:50:34 +03:30
masoodafar-web 3409133757 feat: Add SitePageSettings management UI
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 8m21s
- ISitePageSettingsService + SitePageSettingsService (gRPC client)
- PageSettingsManagementPage: 4 cards for fixed pages
- PageSettingsEditDialog: edit title, hero, meta, settingsJson
- PageImagesDialog: CRUD for page images (licenses, team, values)
- PageImageEditDialog: add/edit single image with upload
- Proto NuGet updated to 0.0.180
- NavMenu: added link to page settings
2026-02-17 22:34:59 +03:30
masoodafar-web af51c02d3d chore: clean up commented-out GwUrl lines in appsettings
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 7m16s
2026-02-17 02:56:07 +03:30
masoodafar-web b9171d548b docs: فاز ۱۵.۴-۱۵.۶ — K8S_SERVER, appsettings.Production, nginx image fix
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 7m42s
2026-02-17 02:11:49 +03:30
masoodafar-web 743403e4d4 fix: nginx image path library/nginx:alpine → nginx:alpine on production
Build and Deploy to Production / build-and-deploy (push) Successful in 7m20s
2026-02-17 02:09:03 +03:30
masoodafar-web 44fd5f29d9 merge: kub-stage into production — fix prod workflow + appsettings GwUrl=cms.kbs2.ir
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2026-02-17 02:00:29 +03:30
masoodafar-web e7bed65149 fix: production deployment — appsettings.Production.json + fix K8S_SERVER + set image
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
- appsettings.Production.json: GwUrl=cms.kbs1.ir
- prod-deploy.yml: K8S_SERVER 194.5.195.53 → 45.149.79.127
- prod-deploy.yml: kubectl set image instead of rollout restart
2026-02-17 01:58:36 +03:30
masoodafar-web 6fc00e134f docs: اضافه شدن فاز ۱۵ به CHANGELOG — Production Deploy, فیکس cross-deploy, Proto v0.0.179 2026-02-17 01:44:25 +03:30
masoodafar-web fac3d2c85e Fix production workflow: use SERVER_PASSWORD, add docker login, use set image for deploy
Build and Deploy to Production / build-and-deploy (push) Failing after 6m25s
2026-02-17 01:24:32 +03:30
masoodafar-web 51c20116a7 Remove staging workflow files from production branch to prevent cross-deployment 2026-02-17 01:16:13 +03:30
masoodafar-web 4454b8dd3a Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Failing after 6s
Features merged:
- Discount shop management (CRUD, inventory, bulk edit)
- Blog and content management improvements
- Docker build fixes (NuGet package reference)
- UI improvements across multiple components

Production workflow and Dockerfile preserved.
2026-02-17 00:57:14 +03:30
masoodafar-web aea614e095 docs: add Phase 14 — Docker build + deployment fixes
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 7m36s
2026-02-16 02:23:42 +03:30
masoodafar-web 6ec221cb41 fix: use local registry for nginx:alpine image in Dockerfile
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 7m16s
- Changed FROM nginx:alpine to 194.5.195.53:32082/nginx:alpine
- Fixes TLS handshake timeout when pulling from Docker Hub
2026-02-16 02:02:47 +03:30
masoodafar-web 05af68bae1 ci: re-trigger deploy (added SERVER_PASSWORD secret)
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 7m9s
2026-02-16 01:46:04 +03:30
masoodafar-web 4fd039b250 fix: switch to NuGet package reference for CMS Protobuf (Docker build fix)
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
- Changed from ProjectReference to PackageReference (v0.0.178)
- Removed COPY libs/ from Dockerfile (no longer needed)
- Docker build context (src/) cannot resolve ../../../CMS path
- NuGet package restored from Nexus registry
2026-02-16 01:42:25 +03:30
masoodafar-web 49017027d0 feat: full BackOffice updates - discount shop, blog, content, UI improvements
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m20s
- Blog management pages (posts, categories)
- Content management pages (site pages, sections)
- Contract management pages
- Discount shop improvements (order details, product form, categories)
- Wallet management pages
- Commission withdrawal details/reject dialogs
- Orders hub, sales reports
- System hub, global search, breadcrumb
- Club features page, club hub
- UI/UX improvements across all pages
- AppBreadcrumb, GlobalSearch components
- NavMenu updates and styling
2026-02-16 00:56:39 +03:30
masoodafar-web d84dfe3bc5 refactor: enhance Docker daemon startup and registry login in deployment workflows
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m19s
2026-02-11 00:41:32 +03:30
masoodafar-web 8d9942ddc2 Refactor: Update Protobuf references to CMSMicroservice across User, UserAddress, UserOrder, UserRole, and related services
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 3m9s
- Changed Protobuf imports from BackOffice.BFF to CMSMicroservice for User, UserAddress, UserOrder, UserRole components and services.
- Updated appsettings for direct connection to CMS, removing BFF references.
- Adjusted logic in services to accommodate new Protobuf structure and fields.
- Moved documentation to totalDoc directory.
2026-02-10 22:05:13 +03:30
masoodafar-web 2bbb1ba4c7 chore: Update production workflow to use Nexus registry (no proxy needed)
Build and Deploy to Production / build-and-deploy (push) Failing after 0s
- Changed Dockerfile base images to use 194.5.195.53:32082 (Nexus)
- Updated workflow to use docker-sshpass image from Nexus
- Removed HTTP_PROXY/HTTPS_PROXY dependencies
- Changed deploy method to SSH instead of kubeconfig
- Registry: 194.5.195.53:30080 (Gitea internal)
- K8S Server: 45.149.79.127 (Production)
2026-01-29 22:04:31 +03:30
masoodafar-web 7a27347142 fix: increase Docker daemon startup timeout to 3 minutes
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 0s
2026-01-29 17:13:49 +03:30
masoodafar-web 5d563aa405 trigger: test offline CI/CD pipeline
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
2026-01-29 07:03:31 +03:30
masoodafar-web b05f92195a refactor: use docker-sshpass image (fully offline)
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
2026-01-29 06:49:39 +03:30
masoodafar-web fc5d81425a refactor: simplified pipeline with offline Nexus + auto deploy
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
2026-01-29 06:12:15 +03:30
masoodafar-web 66884d8216 Revert: use only Nexus (no fallback)
Build and Deploy / build (push) Failing after 39s
2026-01-29 03:44:39 +03:30
masoodafar-web ad47f1f495 Add nuget.org as fallback source
Build and Deploy / build (push) Has been cancelled
2026-01-29 03:41:17 +03:30
masoodafar-web 1a5a71a27f Remove afrino NuGet sources - use only Nexus
Build and Deploy / build (push) Failing after 32s
2026-01-29 03:14:39 +03:30
masoodafar-web 8a285e485b chore: تنظیم Nexus برای NuGet و Docker offline
Build and Deploy / build (push) Failing after 49s
- Dockerfile: استفاده از Nexus Docker (194.5.195.53:32082)
- NuGet.config: استفاده از Nexus NuGet با HTTP و allowInsecureConnections
- workflow: حذف kubectl dependency، اضافه کردن 32082 به insecure-registries
2026-01-29 02:56:20 +03:30
masoodafar-web 21ee004869 feat: enhance category selection in ProductFormDialog with multi-selection text and improved error handling
Build and Deploy / build (push) Failing after 16s
2026-01-05 21:51:39 +03:30
masoodafar-web 31226399ae feat: replace CategoryMultiSelectCombo with MudSelect for category selection; implement category loading and flattening logic
Build and Deploy / build (push) Failing after 16s
2026-01-05 02:01:05 +03:30
masoodafar-web 53f56636b3 feat: add image and thumbnail upload support in DiscountProduct forms and services
Build and Deploy / build (push) Failing after 17s
2026-01-05 01:23:49 +03:30
masoodafar-web a2c496c16f refactor: remove stock management fields from BulkEdit and related logic; adjust layout in ProductFormDialog and CreateDialog
Build and Deploy / build (push) Failing after 15s
2026-01-04 23:51:42 +03:30
masoodafar-web d28d826a4b Merge branch 'kub-stage' of https://git.se.kbs1.ir/admin/BackOffice into kub-stage
Build and Deploy / build (push) Failing after 27s
2026-01-04 23:05:54 +03:30
masoodafar-web b7543fc10c feat: implement Proto DLL migration and update domain references 2026-01-04 23:05:46 +03:30
Admin 3e9f40bd41 fix: Remove domain from insecure-registries, use IP only
Build and Deploy / build (push) Failing after 15s
2026-01-03 15:15:27 +00:00
Admin 229aa6aec7 fix: Update registry to IP address for CI/CD
Build and Deploy / build (push) Failing after 16s
2026-01-03 15:03:23 +00:00
masoodafar-web 910a1ebfcc feat: update external registry URLs to new domain
Build and Deploy / build (push) Failing after 23s
2026-01-03 17:30:30 +03:30
masoodafar-web c062bd358c Refactor code structure for improved readability and maintainability
Build and Deploy / build (push) Failing after 16s
2026-01-03 15:38:50 +03:30
masoodafar-web 2d833605b4 feat: Implement Inventory Management Pages
Build and Deploy / build (push) Successful in 2m18s
- Added InventoryMainPage for managing inventory items with filtering options.
- Created LowStockPage to display low stock items with filtering capabilities.
- Developed MovementsPage to track inventory movements with detailed filtering.
- Introduced WarehousesPage for managing warehouses, including creation and viewing inventory.
- Enhanced backend integration with InventoryBFFContract for data retrieval.
- Implemented user-friendly UI components using MudBlazor for better user experience.
- Added necessary dialog components for adding stock, adjusting inventory, and viewing movements.
2026-01-03 08:18:55 +03:30
masoodafar-web 978475360c Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 2m23s
2025-12-29 00:43:04 +03:30
masoodafar-web 81ee23e313 feat(BalancesReport): enhance balance display with new member counts and totals
Build and Deploy / build (push) Successful in 2m9s
2025-12-28 21:20:43 +03:30
masoodafar-web 230453e491 feat(config): add production configuration for gateway URL and authentication
Build and Deploy to Production / build-and-deploy (push) Successful in 4m5s
2025-12-27 07:46:54 +03:30
masoodafar-web 1a65babb9d feat(UserPayouts): update status options and colors for user payouts
Build and Deploy / build (push) Successful in 2m8s
2025-12-27 07:34:38 +03:30
masoodafar-web bbe783dc83 feat(UserPayouts): implement withdrawal approval and rejection functionality
Build and Deploy / build (push) Successful in 2m6s
2025-12-27 06:22:11 +03:30
masoodafar-web 6f5bfba0a5 feat(AppVersionEditDialog): replace ReleaseNotes text field with HTML editor for enhanced formatting
Build and Deploy / build (push) Successful in 2m9s
2025-12-27 05:08:14 +03:30
masoodafar-web 98a4ba9e99 feat(config): Add staging configuration for backend URL and authentication
Build and Deploy / build (push) Has been cancelled
2025-12-27 05:07:50 +03:30
masoodafar-web ece29956b2 feat(app-versions): Add functionality to create new app versions and update UI components
Build and Deploy / build (push) Successful in 2m26s
2025-12-27 02:45:47 +03:30
307 changed files with 18548 additions and 7681 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
-46
View File
@@ -1,46 +0,0 @@
name: Push nuget and docker image Actions Workflow
on:
push:
branches:
- stage-new
jobs:
Deploy:
runs-on: windows
steps:
- name: Checkout
uses: https://git.afrino.co/actions/checkout@v3
- name: Setup dotnet
uses: https://git.afrino.co/actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
- name: Remove Package Source
run: dotnet nuget remove source FourSat
continue-on-error: true
- name: Add Package Source
run: dotnet nuget add source --name FourSat --username systemuser --password sZSA7PTiv3pUSQZ https://git.afrino.co/api/packages/FourSat/nuget/index.json --store-password-in-clear-text
- name: Install dependencies
run: dotnet restore ".\src\BackOffice\BackOffice.csproj"
- name: Build
run: dotnet build ".\src\BackOffice\BackOffice.csproj" --configuration Release --no-restore
- name: Test
run: dotnet test ".\src\BackOffice\BackOffice.csproj" --no-restore --verbosity normal
- name: Recycle Apppool
run: |
& "C:\Windows\System32\inetsrv\appcmd.exe" recycle apppool /apppool.name:admin.kbs1.ir
shell: powershell
- name: Stop Website
run: |
& "C:\Windows\System32\inetsrv\appcmd.exe" stop site /site.name:admin.kbs1.ir
shell: powershell
- name: Publish
run: dotnet publish ".\src\BackOffice\BackOffice.csproj" -c Release -o publish
- name: Copy Publish To IIS Directory
run: Get-ChildItem -Path "publish\*" | Copy-Item -Destination "E:\kbs1.ir\admin.kbs1.ir\" -Recurse -Force
- name: Start Website
run: |
& "C:\Windows\System32\inetsrv\appcmd.exe" start site /site.name:admin.kbs1.ir
shell: powershell
+43 -52
View File
@@ -1,4 +1,4 @@
name: Build and Deploy
name: Build and Deploy to Kubernetes
on:
push:
@@ -6,85 +6,76 @@ on:
- kub-stage
env:
REGISTRY: gitea-svc:3000
REGISTRY: 194.5.195.53:30080
IMAGE_NAME: admin/backoffice
K8S_SERVER: 194.5.195.53
jobs:
build:
build-and-deploy:
runs-on: ubuntu-latest
container:
image: docker:latest
image: 194.5.195.53:32082/docker-sshpass:latest
options: --privileged
env:
HTTP_PROXY: http://proxyuser:87zH26nbqT2@46.249.98.211:3128
HTTPS_PROXY: http://proxyuser:87zH26nbqT2@46.249.98.211:3128
NO_PROXY: localhost,127.0.0.1,gitea-svc,194.5.195.53,10.0.0.0/8
steps:
- name: Install dependencies
run: |
apk add --no-cache git curl
# Install kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
mv kubectl /usr/local/bin/
- name: Start Docker daemon with insecure registry
- name: Start Docker daemon
run: |
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'DAEMON'
{
"insecure-registries": ["git.foursat.afrino.co", "gitea-svc:3000"]
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
}
DAEMON
mkdir -p ~/.docker
cat > ~/.docker/config.json << 'CONF'
{
"proxies": {
"default": {
"httpProxy": "http://proxyuser:87zH26nbqT2@46.249.98.211:3128",
"httpsProxy": "http://proxyuser:87zH26nbqT2@46.249.98.211:3128",
"noProxy": "localhost,127.0.0.1,gitea-svc,194.5.195.53,10.0.0.0/8"
}
}
}
CONF
dockerd &
for i in $(seq 1 30); do
docker info >/dev/null 2>&1 && break || sleep 2
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
docker info
# 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 .
git log -1 --format="%H %s"
- 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 build -f BackOffice/Dockerfile \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
--build-arg HTTP_PROXY=http://proxyuser:87zH26nbqT2@46.249.98.211:3128 \
--build-arg HTTPS_PROXY=http://proxyuser:87zH26nbqT2@46.249.98.211:3128 \
.
DOCKER_BUILDKIT=0 docker build --network host -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest -f BackOffice/Dockerfile .
- name: Push to Registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- name: Deploy to Kubernetes
run: |
# Setup kubeconfig
mkdir -p ~/.kube
echo "${{ secrets.KUBECONFIG }}" | base64 -d > ~/.kube/config
export SSHPASS="${{ secrets.SERVER_PASSWORD }}"
# Restart deployment to pull new image
kubectl rollout restart deployment/backoffice || echo "Deployment doesn't exist yet"
# Copy K8s manifests to server
sshpass -e scp -o StrictHostKeyChecking=no k8s/staging/backoffice-deployment.yaml root@${{ env.K8S_SERVER }}:/tmp/backoffice-deployment.yaml
# Wait for rollout to complete
kubectl rollout status deployment/backoffice --timeout=5m || echo "Deployment rollout pending"
# 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!"
+34 -41
View File
@@ -6,78 +6,71 @@ on:
- production
env:
REGISTRY: gitea-svc:3000
EXTERNAL_REGISTRY: git.foursat.afrino.co
REGISTRY: 194.5.195.53:30080
IMAGE_NAME: admin/backoffice
K8S_SERVER: 45.149.79.127
jobs:
build-and-deploy:
runs-on: ubuntu-latest
container:
image: docker:latest
image: 194.5.195.53:32082/docker-sshpass:latest
options: --privileged
env:
HTTP_PROXY: http://proxyuser:87zH26nbqT2@46.249.98.211:3128
HTTPS_PROXY: http://proxyuser:87zH26nbqT2@46.249.98.211:3128
NO_PROXY: localhost,127.0.0.1,gitea-svc,45.149.79.127,194.5.195.53,10.0.0.0/8
steps:
- name: Install dependencies
run: |
apk add --no-cache git curl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
mv kubectl /usr/local/bin/
- name: Start Docker daemon
run: |
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'DAEMON'
{
"insecure-registries": ["git.foursat.afrino.co", "gitea-svc:3000"]
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
}
DAEMON
mkdir -p ~/.docker
cat > ~/.docker/config.json << 'CONF'
{
"proxies": {
"default": {
"httpProxy": "http://proxyuser:87zH26nbqT2@46.249.98.211:3128",
"httpsProxy": "http://proxyuser:87zH26nbqT2@46.249.98.211:3128",
"noProxy": "localhost,127.0.0.1,gitea-svc,194.5.195.53,10.0.0.0/8"
}
}
}
CONF
dockerd &
for i in $(seq 1 30); do docker info >/dev/null 2>&1 && break || sleep 2; done
echo "🚀 Starting Docker daemon..."
dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs &
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
if ! docker info >/dev/null 2>&1; then
echo "❌ Docker daemon failed to start"
exit 1
fi
- name: Checkout code
run: |
git clone --depth 1 --branch production http://gitea-svc:3000/admin/BackOffice.git .
git log -1 --format="%H %s"
- 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 build -f BackOffice/Dockerfile \
DOCKER_BUILDKIT=0 docker build --network host -f BackOffice/Dockerfile \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod \
-t ${{ env.EXTERNAL_REGISTRY }}/${{ env.IMAGE_NAME }}:prod \
--build-arg HTTP_PROXY=http://proxyuser:87zH26nbqT2@46.249.98.211:3128 \
--build-arg HTTPS_PROXY=http://proxyuser:87zH26nbqT2@46.249.98.211:3128 \
.
- name: Push to Registry
run: |
echo "87zH26nbqT" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod
echo "87zH26nbqT" | docker login ${{ env.EXTERNAL_REGISTRY }} -u admin --password-stdin
docker push ${{ env.EXTERNAL_REGISTRY }}/${{ env.IMAGE_NAME }}:prod
- name: Deploy to Production
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBECONFIG_PROD }}" | base64 -d > ~/.kube/config
kubectl rollout restart deployment/backoffice || echo "Deployment not found"
kubectl rollout status deployment/backoffice --timeout=5m || echo "Rollout pending"
export SSHPASS="${{ secrets.SERVER_PASSWORD }}"
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
kubectl set image deployment/backoffice backoffice=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
kubectl rollout status deployment/backoffice --timeout=300s
"
echo "✅ Deployed to Production!"
Executable
+79
View File
@@ -0,0 +1,79 @@
#!/bin/bash
# Build all BFF proto dependencies and copy DLLs to libs folder
# This script is automatically run before build via Directory.Build.targets
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LIBS_DIR="$SCRIPT_DIR/libs"
BFF_PROTOS_DIR="$SCRIPT_DIR/../BackOffice.BFF/src/Protobufs"
echo "🔧 Building all proto dependencies..."
# Create libs directory
mkdir -p "$LIBS_DIR"
# All proto projects
PROTO_PROJECTS=(
"BackOffice.BFF.Common.Protobuf"
"BackOffice.BFF.Category.Protobuf"
"BackOffice.BFF.ClubMembership.Protobuf"
"BackOffice.BFF.Commission.Protobuf"
"BackOffice.BFF.Configuration.Protobuf"
"BackOffice.BFF.DiscountCategory.Protobuf"
"BackOffice.BFF.DiscountOrder.Protobuf"
"BackOffice.BFF.DiscountProduct.Protobuf"
"BackOffice.BFF.DiscountShoppingCart.Protobuf"
"BackOffice.BFF.Health.Protobuf"
"BackOffice.BFF.Inventory.Protobuf"
"BackOffice.BFF.ManualPayment.Protobuf"
"BackOffice.BFF.NetworkMembership.Protobuf"
"BackOffice.BFF.Otp.Protobuf"
"BackOffice.BFF.Package.Protobuf"
"BackOffice.BFF.Products.Protobuf"
"BackOffice.BFF.ProductTag.Protobuf"
"BackOffice.BFF.PublicMessage.Protobuf"
"BackOffice.BFF.Role.Protobuf"
"BackOffice.BFF.Tag.Protobuf"
"BackOffice.BFF.User.Protobuf"
"BackOffice.BFF.UserAddress.Protobuf"
"BackOffice.BFF.UserOrder.Protobuf"
"BackOffice.BFF.UserRole.Protobuf"
)
BUILT_COUNT=0
FAILED_COUNT=0
for PROJECT in "${PROTO_PROJECTS[@]}"; do
PROJECT_DIR="$BFF_PROTOS_DIR/$PROJECT"
if [ -d "$PROJECT_DIR" ]; then
echo "📦 Building $PROJECT..."
if dotnet build "$PROJECT_DIR" -c Release --verbosity quiet 2>/dev/null; then
# Copy DLL and PDB - check both net9.0 and net8.0
if [ -f "$PROJECT_DIR/bin/Release/net9.0/$PROJECT.dll" ]; then
cp "$PROJECT_DIR/bin/Release/net9.0/$PROJECT.dll" "$LIBS_DIR/" && ((BUILT_COUNT++))
cp "$PROJECT_DIR/bin/Release/net9.0/$PROJECT.pdb" "$LIBS_DIR/" 2>/dev/null || true
elif [ -f "$PROJECT_DIR/bin/Release/net8.0/$PROJECT.dll" ]; then
cp "$PROJECT_DIR/bin/Release/net8.0/$PROJECT.dll" "$LIBS_DIR/" && ((BUILT_COUNT++))
cp "$PROJECT_DIR/bin/Release/net8.0/$PROJECT.pdb" "$LIBS_DIR/" 2>/dev/null || true
else
echo "⚠️ DLL not found for: $PROJECT"
((FAILED_COUNT++))
fi
else
echo "⚠️ Failed to build: $PROJECT"
((FAILED_COUNT++))
fi
else
echo "⚠️ Project not found: $PROJECT"
((FAILED_COUNT++))
fi
done
echo ""
echo "✅ Build complete!"
echo " Built: $BUILT_COUNT projects"
echo " Failed: $FAILED_COUNT projects"
echo " Output: $LIBS_DIR"
echo ""
ls -la "$LIBS_DIR"/*.dll 2>/dev/null | wc -l | xargs -I {} echo " Total DLLs: {}"
+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
+4
View File
@@ -0,0 +1,4 @@
# Docs moved to totalDoc
See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation.
+3
View File
@@ -0,0 +1,3 @@
# Docs moved to totalDoc
See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation.
-501
View File
@@ -1,501 +0,0 @@
# BackOffice Build Fix Status
> آخرین بروزرسانی: December 20, 2025
## وضعیت فعلی
**Build Status**: ✅ SUCCESS - 0 Error
### BackOffice.BFF Solution:
- **Build**: ✅ موفق - 0 Error
- **Proto Projects فعال**:
- ✅ BackOffice.BFF.Tag.Protobuf
- ✅ BackOffice.BFF.ProductTag.Protobuf
- ✅ BackOffice.BFF.DiscountProduct.Protobuf
- ✅ BackOffice.BFF.DiscountCategory.Protobuf
- ✅ BackOffice.BFF.DiscountOrder.Protobuf
- ✅ BackOffice.BFF.DiscountShoppingCart.Protobuf
- ✅ BackOffice.BFF.PublicMessage.Protobuf
- ✅ BackOffice.BFF.ManualPayment.Protobuf
- ✅ BackOffice.BFF.ClubMembership.Protobuf
- ✅ BackOffice.BFF.Commission.Protobuf
### BackOffice UI:
- **Build**: ✅ موفق - 0 Error
- **Framework**: Blazor WebAssembly .NET 9.0
- **UI Library**: MudBlazor 8.14.0
### CMS Microservice:
- **Build**: ✅ موفق - 0 Error
**پیشرفت کلی**: از 60+ خطا به 0 خطا رسیدیم ✨
---
## ⚠️ ملاحظات مهم Proto Packages
> **هشدار مهم**: هر تغییری در Proto files نیاز به این 3 مرحله دارد:
### چک‌لیست اجباری بعد از تغییر Proto:
1. **افزایش Version** در `.csproj`:
```xml
<Version>0.0.142</Version> → <Version>0.0.143</Version>
```
2. **Pack کردن** Proto project:
```bash
cd path/to/proto/project
dotnet pack -c Release
# ✅ خودکار push می‌شه به GitLab Registry
```
3. **Update Version** در پروژه‌های وابسته (لایه بالاتر):
```xml
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.143" />
```
**مثال**: تغییر در CMS Proto → Pack → Update در BFF Protos → Pack → Update در UI
**⚠️ فراموش کردن این مراحل = Build Error یا Runtime Bug**
---
## ماژول‌های فعال شده (Enabled Modules)
### ✅ کاملاً فعال و تست شده:
1. **DiscountShop Module** (فروشگاه تخفیفی)
- ✅ DiscountProductsMainPage - مدیریت محصولات تخفیفی
- ✅ DiscountCategoriesMainPage - مدیریت دسته‌بندی‌ها (با MudDataGrid)
- ✅ DiscountOrdersMainPage - مدیریت سفارشات
- ✅ SalesReports - گزارش فروش
- ✅ ProductImageGallery - گالری تصاویر (با MudBlazor 8 fixes)
- Services: IDiscountProductService, IDiscountCategoryService, IDiscountOrderService
2. **PublicMessages Module** (پیام‌های عمومی)
- ✅ PublicMessagesMainPage - مدیریت پیام‌ها
- ✅ MessageFormDialog - فرم ایجاد/ویرایش
- ✅ MessageViewDialog - نمایش جزئیات
- ✅ MessageTemplatesDialog - قالب‌های آماده
- Services: IPublicMessageService
- Proto: BackOffice.BFF.PublicMessage.Protobuf
3. **ManualPayment Module** (پرداخت‌های دستی)
- ✅ ManualPayments - صفحه اصلی مدیریت
- ✅ ManualPaymentDialog - فرم ایجاد و تایید/رد
- Services: Direct gRPC to ManualPaymentContract
- Proto: BackOffice.BFF.ManualPayment.Protobuf
4. **Tag Module** (برچسب‌ها)
- ✅ TagManagementPage - مدیریت تگ‌ها
- ✅ TagEditDialog - ویرایش تگ
- Services: ITagService, IProductTagService
- Proto: BackOffice.BFF.Tag.Protobuf, BackOffice.BFF.ProductTag.Protobuf
5. **Dashboard Widgets**
- ✅ DiscountShopWidget - آمار فروشگاه تخفیفی (7 روز اخیر)
6. **Payment Pages**
- ✅ Transactions - صفحه تراکنش‌ها
7. **DragDrop Pages**
- ✅ CategoryProductsDragDropPage - مدیریت محصولات دسته
- ✅ ProductCategoriesDragDropPage - مدیریت دسته‌های محصول
8. **BulkEdit Module**
- ✅ BulkEdit - ویرایش گروهی محصولات (قیمت، موجودی، وضعیت)
- Proto: BackOffice.BFF.Products.Protobuf (BulkUpdateProductPrices, BulkUpdateProductStock, ToggleProductStatus)
- Note: استفاده از `BackOffice.BFF.Protobuf.Common.PaginationState` با using alias
9. **Product Image Management** - ✅ FULLY OPERATIONAL
- ✅ GalleryDialog - گالری تصاویر محصول
- ✅ CreateDialog - ایجاد محصول با آپلود تصویر
- ✅ UpdateDialog - ویرایش محصول با آپلود تصویر
- ✅ Proto: GetProductGallery, AddProductImage, RemoveProductImage
- ✅ Messages: ImageFileModel, ProductGalleryItem
- ✅ Backend: ProductsService methods uncommented and active
- ✅ CQRS Handlers: AddProductImageCommandHandler, GetProductGalleryQueryHandler, RemoveProductImageCommandHandler
- ✅ CMS Integration: ProductGalleries microservice connected
- ✅ Image Optimization: SixLabors.ImageSharp (1200x1200 + 300x300 thumbnail)
---
## ماژول‌های Exclude شده (نیاز به کار اضافی)
**هیچ فایلی Exclude نیست!** ✅
تمامی صفحات و کامپوننت‌ها build می‌شوند. فقط Backend implementation برای Image Upload لازمه.
---
## تغییرات مهم MudBlazor 8
### Breaking Changes برطرف شده:
1. **MudDialogInstance → IMudDialogInstance**
```csharp
// قبلی:
[CascadingParameter] MudDialogInstance MudDialog { get; set; }
// جدید:
[CascadingParameter] IMudDialogInstance MudDialog { get; set; }
```
2. **MudSwitch نیاز به T parameter**
```razor
<!-- قبلی: -->
<MudSwitch @bind-Checked="Model.IsActive" />
<!-- جدید: -->
<MudSwitch T="bool" @bind-Value="Model.IsActive" />
```
3. **MudChip نیاز به T parameter**
```razor
<!-- قبلی: -->
<MudChip>Text</MudChip>
<!-- جدید: -->
<MudChip T="string">Text</MudChip>
```
4. **MudTreeView تغییر API**
- راه‌حل: جایگزینی با `MudDataGrid` در DiscountCategoriesMainPage
5. **MudFileUpload تغییر signature**
```csharp
// FilesChanged حالا IBrowserFile می‌گیرد نه IReadOnlyList
<MudFileUpload T="IReadOnlyList<IBrowserFile>" FilesChanged="OnFilesSelected" />
```
6. **DragEventArgs.PreventDefault() حذف شد**
```razor
<!-- استفاده از directive attribute: -->
@ondragover:preventDefault
```
---
## تغییرات Proto
### 1. Google.Protobuf.WellKnownTypes Simplification
در همه جا از wrapper به مقدار مستقیم تغییر یافت:
```csharp
// قبلی (اشتباه):
request.UserId = new Google.Protobuf.WellKnownTypes.Int64Value { Value = userId };
request.Status = new Google.Protobuf.WellKnownTypes.Int32Value { Value = status };
request.ReferenceNumber = new Google.Protobuf.WellKnownTypes.StringValue { Value = refNum };
// جدید (صحیح):
request.UserId = userId;
request.Status = status;
request.ReferenceNumber = refNum;
```
### 2. Timestamp to DateTime Conversion
```csharp
// Proto Timestamp به DateTime تبدیل می‌شود:
var dateTime = timestamp.ToDateTime(); // به جای ToLocalTime()
```
---
## تغییرات معماری
### BasePageComponent Pattern
صفحات با فیلتر از `BasePageComponent` استفاده می‌کنند ولی `ReloadAsync()` ندارد.
راه‌حل: استفاده مستقیم از `MudDataGrid.ReloadServerData()`:
```csharp
private MudDataGrid<ModelType>? _dataGrid;
private async Task OnFilterSubmit()
{
if (_dataGrid != null)
await _dataGrid.ReloadServerData();
}
```
---
- `ProductGalleryImage`
- `GetCategoriesRequest/Response`
- `UpdateProductCategoriesRequest`
- `GetProductsForCategoryRequest/Response`
- `UpdateCategoryProductsRequest`
### 3. تغییرات csproj
**Products از NuGet به ProjectReference تغییر کرد**:
```xml
<!-- قبلی: -->
<PackageReference Include="Foursat.BackOffice.BFF.Products.Protobuf" Version="0.0.8" />
<!-- جدید: -->
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Products.Protobuf/BackOffice.BFF.Products.Protobuf.csproj" />
```
### 4. فیکس‌های MudBlazor
**MudSwitch T parameter**:
- `Pages/Settings/UserSettings.razor`
- `Pages/Club/ClubMembers.razor`
- `Pages/Configuration/Configuration.razor`
```razor
<!-- قبلی: -->
<MudSwitch @bind-Value="..." />
<!-- جدید: -->
<MudSwitch T="bool" @bind-Value="..." />
```
### 5. فیکس Snackbar Duplicate
در فایل‌های زیر `[Inject] ISnackbar Snackbar` حذف شد (چون در `_Imports.razor` inject شده):
- `ApplyDiscountDialog.razor.cs`
- `CancelOrderDialog.razor.cs`
- `ChangeOrderStatusDialog.razor.cs`
### 6. فیکس ConfigureService.cs
Using های زیر comment شدند:
```csharp
// using BackOffice.Services.DiscountProduct;
// using BackOffice.Services.DiscountCategory;
// using BackOffice.Services.DiscountOrder;
// using BackOffice.Services.Tag;
// using BackOffice.Services.ProductTag;
// using BackOffice.Services.PublicMessage;
```
---
## کارهای باقیمانده (TODO)
### فوری - نیاز به Proto Methods:
#### 1. Product Image Management
**فایل‌های Excluded**:
- `Pages/Products/Components/GalleryDialog.razor`
- `Pages/Products/Components/CreateDialog.razor`
- `Pages/Products/Components/UpdateDialog.razor`
**Proto Methods مورد نیاز در `products.proto`**:
```protobuf
service ProductsContract {
// برای GalleryDialog:
rpc AddProductImage(AddProductImageRequest) returns (AddProductImageResponse);
rpc RemoveProductImage(RemoveProductImageRequest) returns (google.protobuf.Empty);
// برای Create/Update Dialogs:
rpc CreateProductWithImage(CreateProductWithImageRequest) returns (CreateProductResponse);
rpc UpdateProductWithImage(UpdateProductWithImageRequest) returns (google.protobuf.Empty);
}
message ImageFileModel {
bytes file = 1;
string mime = 2;
string file_name = 3;
}
message AddProductImageRequest {
int64 product_id = 1;
string title = 2;
ImageFileModel image_file = 3;
}
message AddProductImageResponse {
int64 product_gallery_id = 1;
}
message RemoveProductImageRequest {
int64 product_gallery_id = 1;
}
message CreateProductWithImageRequest {
// ... سایر فیلدهای محصول
ImageFileModel image_file = 1;
ImageFileModel thumbnail_file = 2;
}
message UpdateProductWithImageRequest {
int64 id = 1;
// ... سایر فیلدها
ImageFileModel image_file = 2;
ImageFileModel thumbnail_file = 3;
}
```
**وضعیت**: 🔴 نیاز به پیاده‌سازی در Backend
---
#### 2. BulkEdit Refactoring
**فایل Excluded**: `Pages/Products/BulkEdit.razor`
**مشکل**: استفاده مستقیم از `CMSMicroservice.Protobuf.Protos`
**راه‌حل**:
1. حذف dependency به `CMSMicroservice.Protobuf`
2. افزودن bulk update methods به `products.proto`:
```protobuf
service ProductsContract {
rpc BulkUpdateProducts(BulkUpdateProductsRequest) returns (BulkUpdateProductsResponse);
}
message BulkUpdateProductsRequest {
repeated int64 product_ids = 1;
google.protobuf.Int64Value new_price = 2;
google.protobuf.Int32Value new_discount = 3;
google.protobuf.Int32Value new_club_discount_percent = 4;
StockUpdateOperation stock_operation = 5;
google.protobuf.BoolValue status_enable = 6;
}
enum StockUpdateOperation {
STOCK_NO_CHANGE = 0;
STOCK_SET = 1;
STOCK_ADD = 2;
STOCK_SUBTRACT = 3;
}
message BulkUpdateProductsResponse {
int32 updated_count = 1;
repeated int64 failed_product_ids = 2;
}
```
**وضعیت**: 🔴 نیاز به پیاده‌سازی در Backend
---
### اختیاری - بهبودها:
#### 3. Transactions API Implementation
**فایل**: `Pages/Payment/Transactions.razor`
**وضعیت فعلی**: ✅ Enabled ولی متد `LoadData` فقط `TODO` دارد
**نیاز**: پیاده‌سازی Transaction API در Backend
---
## آمار نهایی
### ماژول‌های فعال: 7 ✅
1. DiscountShop (Products, Categories, Orders, Reports)
2. PublicMessages
3. ManualPayments
4. Tag Management
5. Dashboard DiscountShopWidget
6. Transactions Page
7. DragDrop Pages (Category ↔ Products)
### ماژول‌های Excluded: 3 ❌
1. GalleryDialog (نیاز به Image Upload API)
2. CreateDialog/UpdateDialog (نیاز به Image Upload API)
3. BulkEdit (نیاز به Refactoring + Bulk API)
### Build Errors: 0 🎉
### Proto Projects: 14 فعال
### صفحات فعال: ~30+
### کامپوننت‌های فعال: ~50+
---
---
## Handler های موقتاً Exclude شده در BackOffice.BFF.Application
### فایل‌های Exclude شده:
```xml
<Compile Remove="DiscountOrderCQ/**/*.cs" />
<Compile Remove="DiscountShoppingCartCQ/**/*.cs" />
<Compile Remove="ManualPaymentCQ/**/*.cs" />
<Compile Remove="ConfigurationCQ/**/*.cs" />
<Compile Remove="CommissionCQ/Commands/ProcessWithdrawal/**/*.cs" />
```
### دلیل Exclude:
این Handler ها فیلدهای متفاوتی با proto های CMS دارند و نیاز به بازنویسی دارند.
### مثال عدم تطابق DiscountOrder:
**Handler انتظار دارد:**
- Request: `UserId`, `AddressId`, `DiscountBalanceAmount`, `GatewayAmount`
- Response: `OrderId`, `TrackingCode`, `RequiresGatewayPayment`, `GatewayPayableAmount`
**Proto CMS دارد:**
- Request: `user_id`, `user_address_id`, `discount_balance_to_use`, `notes`
- Response: `success`, `message`, `order_id`, `gateway_amount`, `payment_url`
---
## Proto Update های مورد نیاز
### UserOrder.Protobuf
متدهای زیر باید اضافه شوند:
- `CancelOrderAsync(CancelOrderRequest)`
- `ApplyDiscountToOrderAsync(ApplyDiscountToOrderRequest)`
- `UpdateOrderStatusAsync(UpdateOrderStatusRequest)`
فیلدهای زیر باید اضافه شوند:
- `VatAmount`
- `VatPercentage`
- `VatBaseAmount`
- `VatTotalAmount`
- `PaymentStatus.None`
### Products.Protobuf
متدهای زیر باید اضافه شوند:
- `AddProductImageAsync`
- `RemoveProductImageAsync`
فیلدهای زیر باید اضافه شوند:
- `ImageFile` (bytes)
- `ThumbnailFile` (bytes)
- `ImageFileModel` message
---
## دستورات برای ادامه کار
### 1. اجرای build برای دیدن خطاهای فعلی:
```bash
cd /home/masoud/Apps/project/FourSat/BackOffice/src/BackOffice
dotnet build 2>&1 | grep -E "error CS|Error"
```
### 2. فایل‌های مهم برای بررسی:
- `BackOffice.csproj` - لیست exclude ها و references
- `ConfigureService.cs` - DI registrations
- `_Imports.razor` - global using و inject ها
### 3. Proto فایل‌های مهم:
- `BackOffice.BFF/src/Protobufs/BackOffice.BFF.Products.Protobuf/Protos/products.proto`
- `BackOffice.BFF/src/Protobufs/BackOffice.BFF.UserOrder.Protobuf/Protos/userorder.proto`
---
## چک‌لیست برای chat جدید
- [ ] خطاهای build رو چک کن
- [ ] `PaginationState` namespace رو فیکس کن
- [ ] `WithdrawalReports` binding رو فیکس کن
- [ ] `OpenGalleryDialog` رو comment کن در `ProductsMainPage`
- [ ] `DiscountShopWidget` رو از `SystemOverview` حذف کن
- [ ] تست build موفق
---
## نکات مهم
1. **هیچ فایلی حذف نشده** - فقط از build exclude شدند
2. **Proto های local** از ProjectReference استفاده می‌کنند نه NuGet
3. **MudBlazor 8.14.0** نیاز به `T` parameter برای generic components دارد
4. **Snackbar** در `_Imports.razor` inject شده، نباید در component ها duplicate بشه
-118
View File
@@ -1,118 +0,0 @@
# BackOffice Changelog
> تاریخچه تغییرات پروژه BackOffice
---
## December 20, 2025
### 🐛 Bug Fixes
#### 1. صفحه `/network/balances` - ValidationException
**مشکل**: خطای ValidationException هنگام لود صفحه
**راه‌حل**: اضافه کردن Mapster mapping در `CommissionProfile.cs`:
```csharp
config.NewConfig<GetUserWeeklyBalancesRequest, GetUserWeeklyBalancesQuery>()
.Map(dest => dest.PaginationState, src => src.PaginationState);
```
---
#### 2. صفحه `/club/members` - داده‌ها لود نمی‌شدند
**مشکل**: صفحه خالی بود و داده‌ای نمایش نمی‌داد
**راه‌حل**: ایجاد `ClubMembershipProfile.cs` در CMS و BFF با mappings کامل:
- `GetAllClubMembershipsRequest``GetAllClubMembershipsQuery`
- `GetAllClubMembershipsResponseDto``GetAllClubMembershipsResponse`
**فایل‌های جدید**:
- `CMS/WebApi/Common/Mappings/ClubMembershipProfile.cs`
- `BackOffice.BFF/WebApi/Common/Mappings/ClubMembershipProfile.cs` (بازنویسی)
---
#### 3. صفحه `/club/statistics` - Unimplemented Error
**مشکل**: خطای `Status(StatusCode="Unimplemented")`
**راه‌حل**:
1. اضافه کردن override `GetClubStatistics` در `ClubMembershipService.cs`
2. اضافه کردن mappings برای Statistics در هر دو Profile
---
### ✨ New Features
#### 4. فعال‌سازی قابلیت‌های Products
**قبل**: همه دکمه‌ها "در حال توسعه" نشان می‌دادند
**بعد**: همه قابلیت‌ها فعال شدند:
- ✅ ایجاد محصول جدید (CreateDialog)
- ✅ ویرایش محصول (UpdateDialog)
- ✅ گالری تصاویر (GalleryDialog)
- ✅ مدیریت تگ‌ها (AssignTagsDialog)
**فایل**: `ProductsMainPage.razor.cs`
---
#### 5. فیلد "تعداد موجودی" در Products
**اضافات**:
- فیلد موجودی در فرم ایجاد محصول
- فیلد موجودی در فرم ویرایش محصول
- ستون موجودی در لیست با رنگ‌بندی:
- 🔴 ناموجود (0 یا کمتر)
- 🟡 کم موجود (کمتر از 10)
- 🟢 موجود (10 یا بیشتر)
**فایل‌های تغییر یافته**:
- `CreateDialog.razor`
- `UpdateDialog.razor`
- `ProductsMainPage.razor`
- `CreateNewProductsCommand.cs` (BFF)
- `UpdateProductsCommand.cs` (BFF)
---
## December 6, 2025
### ✅ Major Milestones
- Build Errors: 60+ → 0
- MudBlazor 8 Migration Complete
- All Product Image Management APIs Implemented
- BulkEdit Module Enabled
- All Files Unexcluded
### 🔧 Technical Changes
- `IMudDialogInstance` جایگزین `MudDialogInstance`
- `MudSwitch T="bool"` اضافه شد
- `MudChip T="string"` اضافه شد
- Products از NuGet به ProjectReference تغییر کرد
---
## December 1, 2025
### ✅ Network & Commission System
- Commission Dashboard Complete
- Network Members Page Complete
- Club Members Page Complete
- Weekly Pool Management
- Withdrawal System
- Payout System
---
## November 29, 2025
### ✅ Initial Setup
- SystemConfigurations Table Created
- Base Configuration Values Added:
- `Network.MaxDepth`: 10
- `Club.DefaultMembershipDurationMonths`: 12
- `Commission.MinimumPayoutAmount`: 100000
- `System.MaintenanceMode`: false
+1
View File
@@ -0,0 +1 @@
Docs moved to /totalDoc — see totalDoc/INDEX.md
-28
View File
@@ -1,28 +0,0 @@
# ⚠️ توجه: مستندات اصلی منتقل شده
مستندات اصلی پروژه در فولدر زیر قرار دارند:
```
/home/masoud/Apps/project/FourSat/totalDoc/
```
## 🗂️ ساختار اصلی مستندات:
- **00-INDEX.md** - فهرست جامع مستندات
- **QUICK-REFERENCE.md** - مرجع سریع
- **FINAL-STATUS.md** - وضعیت نهایی پروژه
- **CHANGELOG-2025-12-XX.md** - لاگ تغییرات روزانه
- **01-BUSINESS/** - منطق تجاری
- **02-ARCHITECTURE/** - معماری سیستم
- **03-BACKEND/** - مستندات Backend (CMS, BFF)
- **04-FRONTEND/** - مستندات Frontend (BackOffice, FrontOffice)
- **05-TASKS/** - کارهای جاری
- **06-DEPLOYMENT/** - راهنمای استقرار
## 📝 این پوشه:
فایل‌های این پوشه (`BackOffice/docs/`) برای مرجع محلی نگه داشته شده‌اند ولی **مستندات اصلی و به‌روز** در `totalDoc` قرار دارند.
---
**تاریخ**: ۳۰ آذر ۱۴۰۴ (20 December 2025)
-34
View File
@@ -1,34 +0,0 @@
# BackOffice Documentation - README
> آخرین بروزرسانی: **December 20, 2025**
## فایل‌های این پوشه
| فایل | شرح |
|------|-----|
| `STATUS.md` | وضعیت کلی پروژه و Build Status |
| `CHANGELOG.md` | تاریخچه تغییرات به ترتیب تاریخ |
| `TECHNICAL-NOTES.md` | نکات فنی، Mapster، MudBlazor، Proto |
| `development-plan.md` | برنامه توسعه (قدیمی - برای مرجع) |
---
## وضعیت فعلی
```
✅ Build Status: SUCCESS (0 Errors)
✅ Proto Projects: 24 فعال
✅ صفحات فعال: 40+
✅ Excluded Files: 0
```
## دستورات سریع
```bash
# Build همه
cd /home/masoud/Apps/project/FourSat/BackOffice/src
dotnet build BackOffice.sln
# فقط UI
dotnet build BackOffice/BackOffice.csproj
```
-346
View File
@@ -1,346 +0,0 @@
# کارهای باقیمانده - BackOffice
> آخرین بروزرسانی: December 20, 2025
## وضعیت کلی
**Build Status**: ✅ SUCCESS (0 Errors)
**Enabled Modules**: 10+ ماژول کامل
**Remaining Tasks**: فقط Backend Implementation
---
## ✅ کارهای انجام شده - Session December 20, 2025
### 1. صفحه `/network/balances` - ✅ FIXED
**مشکل**: ValidationException هنگام لود صفحه
**راه‌حل**: اضافه کردن Mapster mapping برای `GetUserWeeklyBalancesRequest``GetUserWeeklyBalancesQuery`
**فایل تغییر یافته**:
- `BackOffice.BFF/WebApi/Common/Mappings/CommissionProfile.cs`
```csharp
config.NewConfig<GetUserWeeklyBalancesRequest, GetUserWeeklyBalancesQuery>()
.Map(dest => dest.PaginationState, src => src.PaginationState);
```
### 2. صفحه `/club/members` - ✅ FIXED
**مشکل**: داده‌ها لود نمی‌شدند (Mapster mapping نداشت)
**راه‌حل**: ایجاد ClubMembershipProfile در CMS و BFF
**فایل‌های جدید**:
- `CMS/WebApi/Common/Mappings/ClubMembershipProfile.cs` (NEW)
- `BackOffice.BFF/WebApi/Common/Mappings/ClubMembershipProfile.cs` (REWRITTEN)
**Mappings اضافه شده**:
- `GetAllClubMembershipsRequest``GetAllClubMembershipsQuery`
- `GetAllClubMembershipsResponseDto``GetAllClubMembershipsResponse`
### 3. صفحه `/club/statistics` - ✅ FIXED
**مشکل**: خطای `Status(StatusCode="Unimplemented")`
**راه‌حل**: پیاده‌سازی متد gRPC در BFF و اضافه کردن mappings
**فایل‌های تغییر یافته**:
- `BackOffice.BFF/WebApi/Services/ClubMembershipService.cs` - اضافه شدن `GetClubStatistics` override
- `CMS/WebApi/Common/Mappings/ClubMembershipProfile.cs` - اضافه شدن mappings
- `BackOffice.BFF/WebApi/Common/Mappings/ClubMembershipProfile.cs` - اضافه شدن mappings
**Mappings اضافه شده**:
- `GetClubStatisticsRequest``GetClubStatisticsQuery`
- `GetClubStatisticsResponseDto``GetClubStatisticsResponse`
- PackageDistribution و MonthlyTrend mappings
### 4. صفحه Products - ✅ ALL FEATURES ENABLED
**مشکل**: همه قابلیت‌ها disabled بودند و "در حال توسعه" نشان می‌دادند
**راه‌حل**: Uncomment کردن کدهای دیالوگ‌ها
**فایل تغییر یافته**:
- `BackOffice/Pages/Products/ProductsMainPage.razor.cs`
**قابلیت‌های فعال شده**:
-`CreateNew()` - ایجاد محصول جدید
-`Update()` - ویرایش محصول
-`OpenGallery()` - گالری تصاویر
-`OpenTagAssignment()` - اختصاص تگ
### 5. فیلد "تعداد موجودی" در Products - ✅ ADDED
**مشکل**: فیلد RemainingCount در فرم‌ها و لیست نبود
**راه‌حل**: اضافه کردن فیلد به همه لایه‌ها
**فایل‌های تغییر یافته**:
- `BackOffice/Pages/Products/Components/CreateDialog.razor` - اضافه شدن فیلد موجودی
- `BackOffice/Pages/Products/Components/UpdateDialog.razor` - اضافه شدن فیلد موجودی
- `BackOffice/Pages/Products/ProductsMainPage.razor` - اضافه شدن ستون موجودی با رنگ‌بندی
- `BackOffice.BFF.Application/ProductsCQ/Commands/CreateNewProducts/CreateNewProductsCommand.cs` - اضافه شدن `RemainingCount`
- `BackOffice.BFF.Application/ProductsCQ/Commands/UpdateProducts/UpdateProductsCommand.cs` - اضافه شدن `RemainingCount`
**نمایش موجودی در لیست**:
- 🔴 **ناموجود** - اگر موجودی `0` یا کمتر (Chip قرمز)
- 🟡 **عدد** - اگر موجودی کمتر از `10` (Chip زرد - هشدار)
- 🟢 **عدد** - اگر موجودی `10` یا بیشتر (Chip سبز)
---
## ✅ کارهای انجام شده قبلی
### 1. BulkEdit Module - COMPLETED ✅
- ✅ حذف dependency به CMSMicroservice
- ✅ استفاده از BackOffice.BFF.Products.Protobuf
- ✅ تصحیح PaginationState namespace issue
- ✅ فایل فعال شد و build موفق
### 2. Product Image Management - Proto COMPLETED ✅
- ✅ تعریف ImageFileModel message
- ✅ اضافه کردن GetProductGallery RPC
- ✅ اضافه کردن AddProductImage RPC
- ✅ اضافه کردن RemoveProductImage RPC
- ✅ اضافه کردن ImageFile و ThumbnailFile به Create/Update requests
- ✅ هر 3 دیالوگ فعال شدند و build موفق
**فایل‌های Enabled**:
- `Pages/Products/Components/GalleryDialog.razor`
- `Pages/Products/Components/CreateDialog.razor`
- `Pages/Products/Components/UpdateDialog.razor`
---
## 🔴 کارهای باقیمانده (Backend Only)
### 1. Product Image Management - Backend Implementation
**اولویت**: بالا
**وضعیت**: ✅ COMPLETED - همه چیز آماده!
**آخرین تغییرات**:
- ✅ ProductsService.cs: همه methods فعال شدند (AddProductImage, GetProductGallery, RemoveProductImage)
- ✅ Application Layer: CQRS handlers از قبل پیاده‌سازی شده‌اند
- ✅ CMS Integration: ProductGalleries microservice متصل است
- ✅ Image Optimization: 1200x1200 main + 300x300 thumbnail ready
#### Proto Messages (✅ Ready):
```protobuf
// Image file model
message ImageFileModel {
bytes file = 1;
string mime = 2;
string file_name = 3;
}
// Get Product Gallery
rpc GetProductGallery(GetProductGalleryRequest) returns (GetProductGalleryResponse);
message GetProductGalleryRequest {
int64 product_id = 1;
}
message ProductGalleryItem {
int64 product_gallery_id = 1;
int64 product_image_id = 2;
string title = 3;
string image_path = 4;
string image_thumbnail_path = 5;
}
message GetProductGalleryResponse {
repeated ProductGalleryItem items = 1;
}
// Add Product Image
rpc AddProductImage(AddProductImageRequest) returns (AddProductImageResponse);
message AddProductImageRequest {
int64 product_id = 1;
string title = 2;
ImageFileModel image_file = 3;
}
message AddProductImageResponse {
int64 product_gallery_id = 1;
int64 product_image_id = 2;
string title = 3;
string image_path = 4;
string image_thumbnail_path = 5;
}
// Remove Product Image
rpc RemoveProductImage(RemoveProductImageRequest) returns (google.protobuf.Empty);
message RemoveProductImageRequest {
int64 product_gallery_id = 1;
}
```
#### Backend Implementation Steps:
1. **افزودن Messages به Proto** ✅ (فقط تعریف)
2. **پیاده‌سازی RPCs در Backend**:
- AddProductImage: دریافت فایل، ذخیره در storage، ثبت در DB
- RemoveProductImage: حذف فایل از storage و DB
- CreateProductWithImage: ایجاد محصول + آپلود تصاویر
- UpdateProductWithImage: ویرایش محصول + آپلود تصاویر (اختیاری)
3. **File Storage**:
- پیشنهاد: MinIO, Azure Blob, یا local file system
- ذخیره تصویر اصلی و thumbnail
- برگرداندن URL های قابل دسترسی
4. **تست و Enable فایل‌ها در UI**
**زمان تخمینی**: 2-3 روز کاری
---
### 2. BulkEdit Backend Implementation (اختیاری)
**اولویت**: پایین
**وضعیت**: ✅ UI کامل، Backend موجود و کار می‌کند
**نکته**: BulkEdit از RPCهای موجود استفاده می‌کند:
- `BulkUpdateProductPricesAsync`
- `BulkUpdateProductStockAsync`
- `ToggleProductStatusAsync`
همه چیز آماده و کار می‌کند! فقط نیاز به تست دارد.
---
### 3. Transactions API Implementation
**اولویت**: پایین
**وضعیت**: UI آماده، API نیاز به پیاده‌سازی
#### فایل:
- `Pages/Payment/Transactions.razor` - ✅ Enabled اما TODO
#### وضعیت فعلی:
```csharp
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
};
}
```
#### نیاز:
- ایجاد Transaction proto در BackOffice.BFF
- پیاده‌سازی GetTransactions RPC
- اتصال UI به API
**زمان تخمینی**: 1 روز کاری
---
## 📊 آمار پیشرفت
### Modules Status:
| Module | Status | Files | Notes |
|--------|--------|-------|-------|
| DiscountShop | ✅ Complete | 10+ | Products, Categories, Orders, Reports |
| PublicMessages | ✅ Complete | 4 | CRUD + Templates |
| ManualPayments | ✅ Complete | 2 | Create, Approve, Reject |
| Tag Management | ✅ Complete | 3 | CRUD Tags |
| Dashboard Widget | ✅ Complete | 1 | DiscountShop Stats |
| Transactions | ⚠️ Partial | 1 | UI ready, API TODO |
| DragDrop Pages | ✅ Complete | 2 | Category ↔ Products |
| **BulkEdit** | ✅ Complete | 1 | Fully working! |
| **Product Images** | ✅ Complete | 3 | Backend FULLY implemented! |
### Overall Progress:
- **Enabled**: 38+ صفحه و کامپوننت ✅
- **Blocked**: 0 فایل ✅
- **Proto Projects**: 14 فعال
- **Build Errors**: 0 ✅
- **UI Completion**: 100% 🎉
- **Backend Implementation**: 100% ✅✅✅
- **System Status**: FULLY OPERATIONAL 🚀
---
## 🎯 Next Steps
### ✅ ALL TASKS COMPLETED!
**BackOffice System Status**: **PRODUCTION READY** 🚀
**آماده برای استفاده**:
---
## 📝 نکات مهم
### ⚠️ CRITICAL: Proto Package Management
**هر بار که Proto تغییر می‌کند (در هر سرویسی):**
```bash
# 1. افزایش Version در csproj
<Version>X.Y.Z</Version> → <Version>X.Y.Z+1</Version>
# 2. Pack کردن
cd path/to/proto/project
dotnet pack -c Release # Auto-push به GitLab
# 3. Update در لایه بالاتر
<PackageReference Include="PackageName" Version="NEW_VERSION" />
```
**این قانون برای همه سرویس‌ها صادق است:**
- CMS → BFF ها
- BackOffice.BFF → BackOffice UI
- FrontOffice.BFF → FrontOffice UI
**⚠️ عدم رعایت = ساعت‌ها Debug بیهوده!**
---
### برای Backend Developer:
1. **Image Upload**:
- استفاده از streaming برای فایل‌های بزرگ
- اعتبارسنجی نوع و سایز فایل
- تولید thumbnail خودکار
- مدیریت storage (MinIO recommended)
2. **Bulk Update**:
- استفاده از Transaction برای atomicity
- مدیریت concurrent updates
- Logging تغییرات برای audit
3. **Security**:
- اعتبارسنجی سمت سرور
- محدودیت سایز فایل
- sanitize file names
### برای Frontend Developer:
1. **Image Upload**:
- Progress indicator
- Preview قبل از upload
- مدیریت خطاها
- Retry mechanism
2. **BulkEdit**:
- Confirmation قبل از تغییرات
- نمایش نتایج
- Undo capability (آینده)
---
## 🔗 Related Docs
- [BUILD-FIX-STATUS.md](./BUILD-FIX-STATUS.md) - وضعیت کلی build
- [EXCLUDED-FILES.md](./EXCLUDED-FILES.md) - لیست فایل‌های exclude
- [PROTO-DEPENDENCIES.md](./PROTO-DEPENDENCIES.md) - وابستگی‌های proto
---
**Last Updated**: December 6, 2025
**By**: GitHub Copilot (Claude Sonnet 4.5)
-311
View File
@@ -1,311 +0,0 @@
# Session Log - December 20, 2025
## خلاصه Session
این session شامل رفع چندین باگ در صفحات BackOffice و فعال‌سازی قابلیت‌های Products بود.
---
## 1. فیکس صفحه `/network/balances`
### مشکل
```
ValidationException هنگام لود صفحه بالانس‌های هفتگی
```
### علت
Mapster mapping برای تبدیل `GetUserWeeklyBalancesRequest` به `GetUserWeeklyBalancesQuery` وجود نداشت.
### راه‌حل
اضافه کردن mapping در `CommissionProfile.cs`:
```csharp
// File: BackOffice.BFF/src/BackOffice.BFF.WebApi/Common/Mappings/CommissionProfile.cs
config.NewConfig<GetUserWeeklyBalancesRequest, GetUserWeeklyBalancesQuery>()
.Map(dest => dest.PaginationState, src => src.PaginationState);
```
---
## 2. فیکس صفحه `/club/members`
### مشکل
```
صفحه لود می‌شد ولی هیچ داده‌ای نمایش نمی‌داد
```
### علت
Mapster mappings در CMS و BFF برای `GetAllClubMemberships` وجود نداشتند.
### راه‌حل
ایجاد `ClubMembershipProfile.cs` در هر دو لایه:
**CMS/src/CMSMicroservice.WebApi/Common/Mappings/ClubMembershipProfile.cs** (NEW):
```csharp
public class ClubMembershipProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
// GetAllClubMemberships mappings
config.NewConfig<GetAllClubMembershipsRequest, GetAllClubMembershipsQuery>()
.Map(dest => dest.PaginationState, src => src.PaginationState)
.Map(dest => dest.Filter, src => src.Filter);
config.NewConfig<GetAllClubMembershipsResponseDto, GetAllClubMembershipsResponse>()
.MapWith(src => new GetAllClubMembershipsResponse
{
MetaData = src.MetaData != null ? new CMSMicroservice.Protobuf.Common.MetaData
{
PageIndex = src.MetaData.PageIndex,
TotalPages = src.MetaData.TotalPages,
TotalCount = src.MetaData.TotalCount
} : null,
Models = { src.Models?.Select(...) ?? Enumerable.Empty<...>() }
});
}
}
```
**BackOffice.BFF/src/BackOffice.BFF.WebApi/Common/Mappings/ClubMembershipProfile.cs** (REWRITTEN):
- Mapping از BFF Proto به Query
- Mapping از CMS Response به BFF Proto Response
- استفاده از alias imports برای disambiguation
---
## 3. فیکس صفحه `/club/statistics`
### مشکل
```
Status(StatusCode="Unimplemented", Detail="Method cms.ClubMembershipContract/GetClubStatistics is unimplemented")
```
### علت
متد `GetClubStatistics` در BFF Service override نشده بود.
### راه‌حل
**1. اضافه کردن override در ClubMembershipService.cs:**
```csharp
public override async Task<GetClubStatisticsResponse> GetClubStatistics(
GetClubStatisticsRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetClubStatisticsRequest, GetClubStatisticsQuery, GetClubStatisticsResponse>(request, context);
}
```
**2. اضافه کردن mappings در CMS ClubMembershipProfile:**
```csharp
config.NewConfig<GetClubStatisticsRequest, GetClubStatisticsQuery>();
config.NewConfig<GetClubStatisticsResponseDto, GetClubStatisticsResponse>()
.MapWith(src => new GetClubStatisticsResponse
{
TotalMembers = src.TotalMembers,
ActiveMembers = src.ActiveMembers,
// ... سایر فیلدها
PackageDistribution = { src.PackageDistribution?.Select(...) },
MonthlyTrend = { src.MonthlyTrend?.Select(...) }
});
```
**3. اضافه کردن mappings در BFF ClubMembershipProfile:**
- Mapping از BFF Proto به Query
- Mapping از CMS Response DTO به BFF Proto Response
---
## 4. فعال‌سازی قابلیت‌های Products
### مشکل
```
همه دکمه‌های صفحه محصولات "در حال توسعه" نشان می‌دادند
```
### علت
کدهای دیالوگ‌ها comment شده بودند با TODO markers.
### راه‌حل
Uncomment کردن کدها در `ProductsMainPage.razor.cs`:
**فایل: BackOffice/src/BackOffice/Pages/Products/ProductsMainPage.razor.cs**
```csharp
// ✅ CreateNew() - فعال شد
public async Task CreateNew()
{
var dialog = await DialogService.ShowAsync<CreateDialog>("افزودن محصول",
new DialogParameters<CreateDialog> { { x => x.Model, new CreateNewProductsRequest() } },
new DialogOptions { CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.Small });
// ...
}
// ✅ Update() - فعال شد
public async Task Update(DataModel model)
{
var parameters = new DialogParameters<UpdateDialog> { { x => x.Model, model.Adapt<UpdateProductsRequest>() } };
var dialog = await DialogService.ShowAsync<UpdateDialog>("ویرایش محصول", parameters, ...);
// ...
}
// ✅ OpenGallery() - فعال شد
public async Task OpenGallery(DataModel model)
{
var parameters = new DialogParameters<GalleryDialog>
{
{ x => x.ProductId, model.Id },
{ x => x.ProductTitle, model.Title }
};
await DialogService.ShowAsync<GalleryDialog>("گالری تصاویر", parameters, ...);
}
// ✅ OpenTagAssignment() - فعال شد
public async Task OpenTagAssignment(DataModel model)
{
var parameters = new DialogParameters<AssignTagsDialog>
{
{ x => x.ProductId, model.Id },
{ x => x.ProductTitle, model.Title }
};
await DialogService.ShowAsync<AssignTagsDialog>("مدیریت تگ‌های محصول", parameters, ...);
}
```
**Using statement uncomment شد:**
```csharp
using BackOffice.Pages.Tag.Components; // برای AssignTagsDialog
```
---
## 5. اضافه کردن فیلد "تعداد موجودی" به Products
### نیاز
نمایش و ویرایش تعداد موجودی محصول در فرم‌ها و لیست
### تغییرات
**1. فرم‌های دیالوگ (CreateDialog.razor & UpdateDialog.razor):**
```razor
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudItem xs="6">
<MudNumericField T="int" HideSpinButtons="true" @bind-Value="Model.Discount"
Disabled="_isLoading" Label="تخفیف (%)" Variant="Variant.Outlined" Margin="Margin.Dense" />
</MudItem>
<MudItem xs="6">
<MudNumericField T="int" HideSpinButtons="true" @bind-Value="Model.RemainingCount"
Disabled="_isLoading" Label="تعداد موجودی" Variant="Variant.Outlined" Margin="Margin.Dense" />
</MudItem>
</MudStack>
```
**2. ستون جدید در لیست (ProductsMainPage.razor):**
```razor
<TemplateColumn Title="موجودی" CellStyle="text-wrap: nowrap;">
<CellTemplate>
@if (context.Item.RemainingCount <= 0)
{
<MudChip T="string" Color="Color.Error" Size="Size.Small">ناموجود</MudChip>
}
else if (context.Item.RemainingCount < 10)
{
<MudChip T="string" Color="Color.Warning" Size="Size.Small">@context.Item.RemainingCount</MudChip>
}
else
{
<MudChip T="string" Color="Color.Success" Size="Size.Small">@context.Item.RemainingCount</MudChip>
}
</CellTemplate>
</TemplateColumn>
```
**3. اضافه کردن فیلد به BFF Commands (فیکس مهم!):**
مشکل: فیلد `RemainingCount` در BFF Application Commands نبود و باعث می‌شد مقدار ارسال/دریافت نشه.
**CreateNewProductsCommand.cs:**
```csharp
public int Discount { get; init; }
public int Rate { get; init; }
public int RemainingCount { get; init; } // ← اضافه شد
public ImageFileModel ImageFile { get; init; }
```
**UpdateProductsCommand.cs:**
```csharp
public int Discount { get; init; }
public int Rate { get; init; }
public int RemainingCount { get; init; } // ← اضافه شد
public string ImagePath { get; init; }
```
---
## لیست کامل فایل‌های تغییر یافته
### BackOffice.BFF
| فایل | نوع تغییر | توضیح |
|------|----------|-------|
| `WebApi/Common/Mappings/CommissionProfile.cs` | MODIFIED | اضافه شدن mapping برای GetUserWeeklyBalances |
| `WebApi/Common/Mappings/ClubMembershipProfile.cs` | REWRITTEN | Mappings کامل برای ClubMembership |
| `WebApi/Services/ClubMembershipService.cs` | MODIFIED | اضافه شدن GetClubStatistics override |
| `Application/ProductsCQ/Commands/CreateNewProducts/CreateNewProductsCommand.cs` | MODIFIED | اضافه شدن RemainingCount |
| `Application/ProductsCQ/Commands/UpdateProducts/UpdateProductsCommand.cs` | MODIFIED | اضافه شدن RemainingCount |
### CMS
| فایل | نوع تغییر | توضیح |
|------|----------|-------|
| `WebApi/Common/Mappings/ClubMembershipProfile.cs` | NEW | Mappings برای ClubMembership |
### BackOffice UI
| فایل | نوع تغییر | توضیح |
|------|----------|-------|
| `Pages/Products/ProductsMainPage.razor.cs` | MODIFIED | فعال‌سازی CreateNew, Update, OpenGallery, OpenTagAssignment |
| `Pages/Products/ProductsMainPage.razor` | MODIFIED | اضافه شدن ستون موجودی |
| `Pages/Products/Components/CreateDialog.razor` | MODIFIED | اضافه شدن فیلد موجودی |
| `Pages/Products/Components/UpdateDialog.razor` | MODIFIED | اضافه شدن فیلد موجودی |
---
## نکات فنی مهم
### 1. Mapster با Proto Types
برای proto types که immutable هستند، باید از `MapWith` استفاده کرد:
```csharp
config.NewConfig<SourceDto, ProtoResponse>()
.MapWith(src => new ProtoResponse
{
Field1 = src.Field1,
RepeatedField = { src.List?.Select(...) ?? Enumerable.Empty<...>() }
});
```
### 2. Alias Imports برای Proto Disambiguation
وقتی دو proto با اسم یکسان داریم:
```csharp
using BffProtos = BackOffice.BFF.ClubMembership.Protobuf.Protos.ClubMembership;
using CmsProtos = CMSMicroservice.Protobuf.Protos.ClubMembership;
```
### 3. Null-Safe MetaData Mapping
```csharp
MetaData = src.MetaData != null ? new MetaData
{
PageIndex = src.MetaData.PageIndex,
TotalPages = src.MetaData.TotalPages,
TotalCount = src.MetaData.TotalCount
} : null
```
---
## Build Status پایان Session
```
BackOffice.BFF: ✅ Build succeeded (0 errors)
CMS: ✅ Build succeeded (0 errors)
BackOffice UI: ✅ Build succeeded (0 errors)
```
-135
View File
@@ -1,135 +0,0 @@
# BackOffice Project Status
> آخرین بروزرسانی: **December 20, 2025**
---
## 🎯 وضعیت کلی
| Component | Build Status | Errors |
|-----------|--------------|--------|
| BackOffice UI | ✅ SUCCESS | 0 |
| BackOffice.BFF | ✅ SUCCESS | 0 |
| CMS Microservice | ✅ SUCCESS | 0 |
**System Status**: 🟢 **PRODUCTION READY**
---
## 📦 Proto Projects (24 پروژه فعال)
### Core Protos:
- ✅ Common.Protobuf
- ✅ Health.Protobuf
- ✅ Configuration.Protobuf
### User Management:
- ✅ User.Protobuf
- ✅ UserRole.Protobuf
- ✅ Role.Protobuf
- ✅ UserAddress.Protobuf
- ✅ UserWallet.Protobuf
- ✅ Otp.Protobuf
### Products & Shop:
- ✅ Products.Protobuf
- ✅ Category.Protobuf
- ✅ Tag.Protobuf
- ✅ ProductTag.Protobuf
- ✅ Package.Protobuf
### Discount Shop:
- ✅ DiscountProduct.Protobuf
- ✅ DiscountCategory.Protobuf
- ✅ DiscountOrder.Protobuf
- ✅ DiscountShoppingCart.Protobuf
### Network & Commission:
- ✅ NetworkMembership.Protobuf
- ✅ ClubMembership.Protobuf
- ✅ Commission.Protobuf
### Orders & Payments:
- ✅ UserOrder.Protobuf
- ✅ ManualPayment.Protobuf
### Messaging:
- ✅ PublicMessage.Protobuf
---
## 🗂️ ماژول‌های فعال
### 1. Products Module ✅
- صفحه اصلی محصولات با فیلتر و صفحه‌بندی
- ایجاد محصول جدید با آپلود تصویر
- ویرایش محصول
- گالری تصاویر محصول
- مدیریت تگ‌های محصول
- ویرایش گروهی (قیمت، موجودی، وضعیت)
- **ستون موجودی** با رنگ‌بندی هوشمند (🔴🟡🟢)
- DragDrop دسته‌بندی محصولات
### 2. Discount Shop Module ✅
- مدیریت محصولات تخفیفی
- مدیریت دسته‌بندی‌ها
- مدیریت سفارشات
- گزارش فروش
### 3. Commission Module ✅
- داشبورد استخر هفتگی
- لیست پرداخت‌ها
- لیست برداشت‌ها
- بالانس‌های هفتگی کاربران
### 4. Network Module ✅
- لیست اعضای شبکه
- نمای درختی شبکه
- آمار شبکه
### 5. Club Module ✅
- لیست اعضای باشگاه
- آمار باشگاه
- مدیریت ویژگی‌های باشگاه
### 6. Tag Module ✅
- مدیریت تگ‌ها (CRUD)
- اختصاص تگ به محصولات
### 7. Public Messages Module ✅
- مدیریت پیام‌های عمومی
- قالب‌های پیام
### 8. Manual Payments Module ✅
- ثبت پرداخت دستی
- تایید/رد پرداخت
### 9. System Management ✅
- تنظیمات سیستم
- لاگ تغییرات
- Health Check
### 10. Dashboard ✅
- ویجت آمار فروشگاه تخفیفی (7 روز اخیر)
---
## 📊 آمار
| Metric | Value |
|--------|-------|
| Build Errors | 0 |
| Proto Projects | 24 |
| Active Pages | 40+ |
| Active Components | 60+ |
| Excluded Files | 0 |
| Test Coverage | N/A |
---
## 🔧 Environment
- **Framework**: Blazor WebAssembly .NET 9.0
- **UI Library**: MudBlazor 8.14.0
- **gRPC**: Grpc.Net.Client 2.70.0
- **Mapping**: Mapster 7.4.0+
-230
View File
@@ -1,230 +0,0 @@
# BackOffice Technical Notes
> نکات فنی برای توسعه‌دهندگان
---
## 1. Mapster Mapping Patterns
### 1.1 Proto Types (Immutable)
برای proto types که immutable هستند، باید از `MapWith` استفاده کرد:
```csharp
config.NewConfig<SourceDto, ProtoResponse>()
.MapWith(src => new ProtoResponse
{
Field1 = src.Field1,
Field2 = src.Field2 ?? string.Empty,
RepeatedField = { src.List?.Select(x => new Item { ... }) ?? Enumerable.Empty<Item>() }
});
```
### 1.2 Null-Safe MetaData
```csharp
MetaData = src.MetaData != null ? new MetaData
{
PageIndex = src.MetaData.PageIndex,
TotalPages = src.MetaData.TotalPages,
TotalCount = src.MetaData.TotalCount
} : null
```
### 1.3 Alias Imports برای Disambiguation
وقتی دو proto با نام یکسان داریم:
```csharp
using BffProtos = BackOffice.BFF.ClubMembership.Protobuf.Protos.ClubMembership;
using CmsProtos = CMSMicroservice.Protobuf.Protos.ClubMembership;
// استفاده:
config.NewConfig<BffProtos.GetRequest, CmsProtos.GetRequest>();
```
### 1.4 PaginationState Mapping
```csharp
config.NewConfig<BffRequest, AppQuery>()
.Map(dest => dest.PaginationState, src => src.PaginationState);
```
---
## 2. MudBlazor 8 Breaking Changes
### 2.1 Dialog Instance
```csharp
// ❌ قبلی
[CascadingParameter] MudDialogInstance MudDialog { get; set; }
// ✅ جدید
[CascadingParameter] IMudDialogInstance MudDialog { get; set; }
```
### 2.2 Generic Components
```razor
<!-- ❌ قبلی -->
<MudSwitch @bind-Value="isActive" />
<MudChip>Text</MudChip>
<!-- ✅ جدید -->
<MudSwitch T="bool" @bind-Value="isActive" />
<MudChip T="string">Text</MudChip>
```
### 2.3 Drag Events
```razor
<!-- ❌ قبلی -->
@ondragover="e => e.PreventDefault()"
<!-- ✅ جدید -->
@ondragover:preventDefault
```
### 2.4 File Upload
```csharp
// FilesChanged حالا IBrowserFile می‌گیرد
<MudFileUpload T="IBrowserFile" FilesChanged="OnFileSelected">
```
---
## 3. gRPC Patterns
### 3.1 Service Override in BFF
```csharp
public override async Task<GetResponse> GetData(GetRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetRequest, GetQuery, GetResponse>(request, context);
}
```
### 3.2 CQRS Handler
```csharp
public class GetQueryHandler : IRequestHandler<GetQuery, GetResponseDto>
{
private readonly IApplicationContractContext _context;
public async Task<GetResponseDto> Handle(GetQuery request, CancellationToken ct)
{
var cmsRequest = request.Adapt<CmsProtos.GetRequest>();
var response = await _context.Service.GetAsync(cmsRequest, cancellationToken: ct);
return response.Adapt<GetResponseDto>();
}
}
```
---
## 4. Proto Update Checklist
هر تغییری در Proto نیاز به این مراحل دارد:
### Step 1: Update Version
```xml
<!-- در .csproj -->
<Version>0.0.142</Version><Version>0.0.143</Version>
```
### Step 2: Pack
```bash
cd path/to/proto/project
dotnet pack -c Release
# Push به GitLab Registry خودکار انجام می‌شود
```
### Step 3: Update References
```xml
<PackageReference Include="Foursat.Proto" Version="0.0.143" />
```
### Step 4: Build & Test
```bash
dotnet build
dotnet test
```
---
## 5. Common Fixes
### 5.1 Snackbar Duplicate Injection
اگر در `_Imports.razor` inject شده، در component نیاز نیست:
```csharp
// ❌ حذف کن
[Inject] ISnackbar Snackbar { get; set; }
```
### 5.2 BasePageComponent Reload
```csharp
private MudDataGrid<Model>? _gridData;
private async Task OnFilterSubmit()
{
if (_gridData != null)
await _gridData.ReloadServerData();
}
```
### 5.3 Nullable Wrapper Types
```csharp
// Proto nullable types:
// google.protobuf.Int64Value → long?
// google.protobuf.BoolValue → bool?
// Set value:
request.UserId = userId; // نه new Int64Value { Value = userId }
```
---
## 6. Build Commands
```bash
# Full Solution Build
cd /home/masoud/Apps/project/FourSat/BackOffice/src
dotnet build BackOffice.sln
# Single Project
dotnet build BackOffice/BackOffice.csproj
# With Restore
dotnet build --restore
# Clean Build
dotnet clean && dotnet build
# Check Errors Only
dotnet build 2>&1 | grep -E "error CS"
```
---
## 7. Project References
### ProjectReference (Local Development):
```xml
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/X.Protobuf/X.Protobuf.csproj" />
```
### PackageReference (Production):
```xml
<PackageReference Include="Foursat.X.Protobuf" Version="0.0.143" />
```
---
## 8. File Organization
```
BackOffice/
├── docs/
│ ├── README.md # Index
│ ├── STATUS.md # Current Status
│ ├── CHANGELOG.md # History
│ ├── TECHNICAL-NOTES.md # This file
│ └── SESSION-*.md # Session logs
├── src/
│ └── BackOffice/
│ ├── Pages/ # Blazor pages
│ ├── Services/ # gRPC clients
│ └── Common/ # Shared components
```
File diff suppressed because it is too large Load Diff
+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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,131 +0,0 @@
@page "/payment/membership"
@using BackOffice.BFF.ManualPayment.Protobuf
@using BackOffice.Main.Pages.AutoComplete
@attribute [Authorize(Roles = "Admin,SuperAdmin")]
<PageTitle>پرداخت دستی عضویت</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4">
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h5">
<MudIcon Icon="@Icons.Material.Filled.Payment" Class="ml-2" />
پرداخت دستی عضویت
</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudGrid>
<!-- انتخاب کاربر -->
<MudItem xs="12" md="6">
<UserAutoComplete
Label="انتخاب کاربر *"
@bind-SelectedUserId="_userId"
HelperText="جستجو با نام، موبایل یا شناسه" />
</MudItem>
<!-- مبلغ -->
<MudItem xs="12" md="6">
<MudNumericField
@bind-Value="_amount"
Label="مبلغ (ریال) *"
Min="0"
Step="10000"
Format="N0"
Variant="Variant.Outlined"
HelperText="مبلغ پرداختی به ریال" />
</MudItem>
<!-- شماره مرجع -->
<MudItem xs="12" md="6">
<MudTextField
@bind-Value="_referenceNumber"
Label="شماره مرجع *"
Variant="Variant.Outlined"
MaxLength="50"
HelperText="شماره مرجع تراکنش یا فیش" />
</MudItem>
<!-- توضیحات -->
<MudItem xs="12" md="6">
<MudTextField
@bind-Value="_description"
Label="توضیحات"
Variant="Variant.Outlined"
Lines="3"
MaxLength="500"
HelperText="توضیحات اختیاری" />
</MudItem>
</MudGrid>
@if (_showResult)
{
<MudAlert Severity="Severity.Success" Class="mt-4" ShowCloseIcon CloseIconClicked="@(() => _showResult = false)">
<MudText>@_resultMessage</MudText>
<MudText Typo="Typo.caption">شماره تراکنش: @_transactionId</MudText>
<MudText Typo="Typo.caption">شماره سفارش: @_orderId</MudText>
<MudText Typo="Typo.caption">موجودی جدید کیف پول: @_newWalletBalance.ToString("N0") ریال</MudText>
</MudAlert>
}
</MudCardContent>
<MudCardActions>
<MudButton
Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Check"
OnClick="ProcessPayment"
Disabled="@_isProcessing">
@if (_isProcessing)
{
<MudProgressCircular Class="ml-2" Size="Size.Small" Indeterminate="true" />
<MudText>در حال ثبت...</MudText>
}
else
{
<MudText>ثبت پرداخت</MudText>
}
</MudButton>
<MudButton
Variant="Variant.Outlined"
Color="Color.Default"
StartIcon="@Icons.Material.Filled.Clear"
OnClick="ResetForm"
Disabled="@_isProcessing">
پاک کردن فرم
</MudButton>
</MudCardActions>
</MudCard>
<!-- راهنما -->
<MudCard Class="mt-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.Info" Class="ml-2" />
راهنما
</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudText Typo="Typo.body2" Class="mb-2">این صفحه برای ثبت پرداخت‌های دستی عضویت استفاده می‌شود. پس از ثبت:</MudText>
<MudList Dense="true">
<MudListItem Icon="@Icons.Material.Filled.AccountBalance">
<MudText Typo="Typo.body2">مبلغ به کیف پول کاربر (Balance و DiscountBalance) اضافه می‌شود</MudText>
</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.History">
<MudText Typo="Typo.body2">لاگ تغییرات کیف پول ثبت می‌شود</MudText>
</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Receipt">
<MudText Typo="Typo.body2">تراکنش با وضعیت موفق ثبت می‌شود</MudText>
</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.ShoppingCart">
<MudText Typo="Typo.body2">سفارش خرید پکیج عضویت ثبت می‌شود</MudText>
</MudListItem>
</MudList>
</MudCardContent>
</MudCard>
</MudContainer>
@@ -1,103 +0,0 @@
using BackOffice.BFF.ManualPayment.Protobuf;
using BackOffice.Main.Components;
using Grpc.Core;
using Microsoft.AspNetCore.Components;
namespace BackOffice.Main.Pages.Payment;
public partial class ManualMembershipPayment
{
[Inject] private ManualPaymentContract.ManualPaymentContractClient ManualPaymentClient { get; set; } = default!;
[Inject] private ISnackbar Snackbar { get; set; } = default!;
private long? _userId;
private long _amount = 0;
private string _referenceNumber = string.Empty;
private string? _description;
private bool _isProcessing = false;
private bool _showResult = false;
private string _resultMessage = string.Empty;
private long _transactionId = 0;
private long _orderId = 0;
private long _newWalletBalance = 0;
private async Task ProcessPayment()
{
// Validation
if (!_userId.HasValue || _userId.Value <= 0)
{
Snackbar.Add("لطفا کاربر را انتخاب کنید", Severity.Warning);
return;
}
if (_amount <= 0)
{
Snackbar.Add("لطفا مبلغ معتبری وارد کنید", Severity.Warning);
return;
}
if (string.IsNullOrWhiteSpace(_referenceNumber))
{
Snackbar.Add("لطفا شماره مرجع را وارد کنید", Severity.Warning);
return;
}
try
{
_isProcessing = true;
_showResult = false;
var request = new ProcessManualMembershipPaymentRequest
{
UserId = _userId.Value,
Amount = _amount,
ReferenceNumber = _referenceNumber
};
if (!string.IsNullOrWhiteSpace(_description))
{
request.Description = _description;
}
var response = await ManualPaymentClient.ProcessManualMembershipPaymentAsync(request);
_resultMessage = response.Message;
_transactionId = response.TransactionId;
_orderId = response.OrderId;
_newWalletBalance = response.NewWalletBalance;
_showResult = true;
Snackbar.Add("پرداخت دستی با موفقیت ثبت شد", Severity.Success);
// Reset form
await Task.Delay(2000);
ResetForm();
}
catch (RpcException ex)
{
Snackbar.Add($"خطا در ثبت پرداخت: {ex.Status.Detail}", Severity.Error);
}
catch (Exception ex)
{
Snackbar.Add($"خطای غیرمنتظره: {ex.Message}", Severity.Error);
}
finally
{
_isProcessing = false;
}
}
private void ResetForm()
{
_userId = null;
_amount = 0;
_referenceNumber = string.Empty;
_description = null;
_showResult = false;
_resultMessage = string.Empty;
_transactionId = 0;
_orderId = 0;
_newWalletBalance = 0;
}
}
+130 -52
View File
@@ -9,6 +9,8 @@
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<!-- Suppress pre-existing warnings that do not affect runtime correctness -->
<NoWarn>MUD0002;CA2022;BL0005</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -109,71 +111,147 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0"/>
<PackageReference Include="Foursat.BackOffice.BFF.Category.Protobuf" Version="0.0.5"/>
<PackageReference Include="Foursat.BackOffice.BFF.ClubMembership.Protobuf" Version="0.0.7"/>
<PackageReference Include="Foursat.BackOffice.BFF.Commission.Protobuf" Version="0.0.13"/>
<PackageReference Include="Foursat.BackOffice.BFF.Common.Protobuf" Version="0.0.3"/>
<PackageReference Include="Foursat.BackOffice.BFF.Configuration.Protobuf" Version="1.0.20"/>
<PackageReference Include="Foursat.BackOffice.BFF.DiscountCategory.Protobuf" Version="0.0.3"/>
<PackageReference Include="Foursat.BackOffice.BFF.DiscountOrder.Protobuf" Version="0.0.3"/>
<PackageReference Include="Foursat.BackOffice.BFF.DiscountProduct.Protobuf" Version="0.0.3"/>
<PackageReference Include="Foursat.BackOffice.BFF.DiscountShoppingCart.Protobuf" Version="0.0.3"/>
<PackageReference Include="Foursat.BackOffice.BFF.Health.Protobuf" Version="1.0.5"/>
<PackageReference Include="Foursat.BackOffice.BFF.ManualPayment.Protobuf" Version="0.0.5"/>
<PackageReference Include="Foursat.BackOffice.BFF.NetworkMembership.Protobuf" Version="0.0.11"/>
<PackageReference Include="Foursat.BackOffice.BFF.Otp.Protobuf" Version="0.0.113"/>
<PackageReference Include="FourSat.BackOffice.BFF.Package.Protobuf" Version="0.0.113"/>
<PackageReference Include="Foursat.BackOffice.BFF.Products.Protobuf" Version="0.0.11"/>
<PackageReference Include="Foursat.BackOffice.BFF.ProductTag.Protobuf" Version="0.0.3"/>
<PackageReference Include="Foursat.BackOffice.BFF.PublicMessage.Protobuf" Version="0.0.5"/>
<PackageReference Include="Foursat.BackOffice.BFF.Role.Protobuf" Version="0.0.113"/>
<PackageReference Include="Foursat.BackOffice.BFF.Tag.Protobuf" Version="0.0.4"/>
<PackageReference Include="Foursat.BackOffice.BFF.User.Protobuf" Version="0.0.114"/>
<PackageReference Include="Foursat.BackOffice.BFF.UserAddress.Protobuf" Version="0.0.113"/>
<PackageReference Include="Foursat.BackOffice.BFF.UserOrder.Protobuf" Version="0.0.116"/>
<PackageReference Include="Foursat.BackOffice.BFF.UserRole.Protobuf" Version="0.0.113"/>
<!-- Proto Projects - Using ProjectReference for latest changes -->
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Category.Protobuf/BackOffice.BFF.Category.Protobuf.csproj" />-->
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.ClubMembership.Protobuf/BackOffice.BFF.ClubMembership.Protobuf.csproj" />-->
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Commission.Protobuf/BackOffice.BFF.Commission.Protobuf.csproj" />-->
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Common.Protobuf/BackOffice.BFF.Common.Protobuf.csproj" />-->
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Configuration.Protobuf/BackOffice.BFF.Configuration.Protobuf.csproj" />-->
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.DiscountCategory.Protobuf/BackOffice.BFF.DiscountCategory.Protobuf.csproj" />-->
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.DiscountOrder.Protobuf/BackOffice.BFF.DiscountOrder.Protobuf.csproj" />-->
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.DiscountProduct.Protobuf/BackOffice.BFF.DiscountProduct.Protobuf.csproj" />-->
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.DiscountShoppingCart.Protobuf/BackOffice.BFF.DiscountShoppingCart.Protobuf.csproj" />-->
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Health.Protobuf/BackOffice.BFF.Health.Protobuf.csproj" />-->
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.ManualPayment.Protobuf/BackOffice.BFF.ManualPayment.Protobuf.csproj" />-->
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.NetworkMembership.Protobuf/BackOffice.BFF.NetworkMembership.Protobuf.csproj" />-->
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Otp.Protobuf/BackOffice.BFF.Otp.Protobuf.csproj" />-->
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Package.Protobuf/BackOffice.BFF.Package.Protobuf.csproj" />-->
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Products.Protobuf/BackOffice.BFF.Products.Protobuf.csproj" />-->
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.ProductTag.Protobuf/BackOffice.BFF.ProductTag.Protobuf.csproj" />-->
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.PublicMessage.Protobuf/BackOffice.BFF.PublicMessage.Protobuf.csproj" />-->
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Role.Protobuf/BackOffice.BFF.Role.Protobuf.csproj" />-->
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Tag.Protobuf/BackOffice.BFF.Tag.Protobuf.csproj" />-->
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.User.Protobuf/BackOffice.BFF.User.Protobuf.csproj" />-->
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.UserAddress.Protobuf/BackOffice.BFF.UserAddress.Protobuf.csproj" />-->
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.UserOrder.Protobuf/BackOffice.BFF.UserOrder.Protobuf.csproj" />-->
<!-- <ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.UserRole.Protobuf/BackOffice.BFF.UserRole.Protobuf.csproj" />-->
<PackageReference Include="HtmlAgilityPack" Version="1.12.1"/>
<PackageReference Include="DateTimeConverterCL" Version="1.0.0"/>
<PackageReference Include="Grpc.Core" Version="2.46.6"/>
<PackageReference Include="Grpc.Net.Client" Version="2.71.0"/>
<PackageReference Include="Grpc.Net.Client.Web" Version="2.71.0"/>
<PackageReference Include="HtmlAgilityPack" Version="1.12.1"/>
<PackageReference Include="Mapster" Version="7.4.0"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.11"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.11"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.11" PrivateAssets="all"/>
<PackageReference Include="MudBlazor" Version="8.14.0"/>
<PackageReference Include="Newtonsoft.Json" Version="13.0.4"/>
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.8.0"/>
<PackageReference Include="Tizzani.MudBlazor.HtmlEditor" Version="2.3.0"/>
<!-- Proto dependencies (needed when using DLL references) -->
<PackageReference Include="Google.Protobuf" Version="3.28.3"/>
<PackageReference Include="Google.Api.CommonProtos" Version="2.10.0"/>
<PackageReference Include="FluentValidation" Version="11.2.2"/>
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.2.2"/>
</ItemGroup>
<!-- ============================================================ -->
<!-- Proto References: Direct reference to CMS Protobuf project -->
<!-- This enables direct communication with CMS (no BFF needed) -->
<!-- ============================================================ -->
<!-- CMS Protobuf: NuGet in CI/Docker; ProjectReference for local monorepo dev -->
<ItemGroup Condition="Exists('../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj')">
<ProjectReference Include="../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj" />
</ItemGroup>
<ItemGroup Condition="!Exists('../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj')">
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.208" />
</ItemGroup>
<!-- ============================================================ -->
<!-- OLD BFF Proto References (DEPRECATED - kept for reference) -->
<!-- ============================================================ -->
<!-- CI/CD Mode: Use pre-built DLLs from libs/ folder -->
<ItemGroup Condition="false AND Exists('../../libs/BackOffice.BFF.Common.Protobuf.dll')">
<Reference Include="BackOffice.BFF.Common.Protobuf">
<HintPath>../../libs/BackOffice.BFF.Common.Protobuf.dll</HintPath>
</Reference>
<Reference Include="BackOffice.BFF.Category.Protobuf">
<HintPath>../../libs/BackOffice.BFF.Category.Protobuf.dll</HintPath>
</Reference>
<Reference Include="BackOffice.BFF.ClubMembership.Protobuf">
<HintPath>../../libs/BackOffice.BFF.ClubMembership.Protobuf.dll</HintPath>
</Reference>
<Reference Include="BackOffice.BFF.Commission.Protobuf">
<HintPath>../../libs/BackOffice.BFF.Commission.Protobuf.dll</HintPath>
</Reference>
<Reference Include="BackOffice.BFF.Configuration.Protobuf">
<HintPath>../../libs/BackOffice.BFF.Configuration.Protobuf.dll</HintPath>
</Reference>
<Reference Include="BackOffice.BFF.DiscountCategory.Protobuf">
<HintPath>../../libs/BackOffice.BFF.DiscountCategory.Protobuf.dll</HintPath>
</Reference>
<Reference Include="BackOffice.BFF.DiscountOrder.Protobuf">
<HintPath>../../libs/BackOffice.BFF.DiscountOrder.Protobuf.dll</HintPath>
</Reference>
<Reference Include="BackOffice.BFF.DiscountProduct.Protobuf">
<HintPath>../../libs/BackOffice.BFF.DiscountProduct.Protobuf.dll</HintPath>
</Reference>
<Reference Include="BackOffice.BFF.DiscountShoppingCart.Protobuf">
<HintPath>../../libs/BackOffice.BFF.DiscountShoppingCart.Protobuf.dll</HintPath>
</Reference>
<Reference Include="BackOffice.BFF.Health.Protobuf">
<HintPath>../../libs/BackOffice.BFF.Health.Protobuf.dll</HintPath>
</Reference>
<Reference Include="BackOffice.BFF.Inventory.Protobuf">
<HintPath>../../libs/BackOffice.BFF.Inventory.Protobuf.dll</HintPath>
</Reference>
<Reference Include="BackOffice.BFF.ManualPayment.Protobuf">
<HintPath>../../libs/BackOffice.BFF.ManualPayment.Protobuf.dll</HintPath>
</Reference>
<Reference Include="BackOffice.BFF.NetworkMembership.Protobuf">
<HintPath>../../libs/BackOffice.BFF.NetworkMembership.Protobuf.dll</HintPath>
</Reference>
<Reference Include="BackOffice.BFF.Otp.Protobuf">
<HintPath>../../libs/BackOffice.BFF.Otp.Protobuf.dll</HintPath>
</Reference>
<Reference Include="BackOffice.BFF.Package.Protobuf">
<HintPath>../../libs/BackOffice.BFF.Package.Protobuf.dll</HintPath>
</Reference>
<Reference Include="BackOffice.BFF.Products.Protobuf">
<HintPath>../../libs/BackOffice.BFF.Products.Protobuf.dll</HintPath>
</Reference>
<Reference Include="BackOffice.BFF.ProductTag.Protobuf">
<HintPath>../../libs/BackOffice.BFF.ProductTag.Protobuf.dll</HintPath>
</Reference>
<Reference Include="BackOffice.BFF.PublicMessage.Protobuf">
<HintPath>../../libs/BackOffice.BFF.PublicMessage.Protobuf.dll</HintPath>
</Reference>
<Reference Include="BackOffice.BFF.Role.Protobuf">
<HintPath>../../libs/BackOffice.BFF.Role.Protobuf.dll</HintPath>
</Reference>
<Reference Include="BackOffice.BFF.Tag.Protobuf">
<HintPath>../../libs/BackOffice.BFF.Tag.Protobuf.dll</HintPath>
</Reference>
<Reference Include="BackOffice.BFF.User.Protobuf">
<HintPath>../../libs/BackOffice.BFF.User.Protobuf.dll</HintPath>
</Reference>
<Reference Include="BackOffice.BFF.UserAddress.Protobuf">
<HintPath>../../libs/BackOffice.BFF.UserAddress.Protobuf.dll</HintPath>
</Reference>
<Reference Include="BackOffice.BFF.UserOrder.Protobuf">
<HintPath>../../libs/BackOffice.BFF.UserOrder.Protobuf.dll</HintPath>
</Reference>
<Reference Include="BackOffice.BFF.UserRole.Protobuf">
<HintPath>../../libs/BackOffice.BFF.UserRole.Protobuf.dll</HintPath>
</Reference>
</ItemGroup>
<!-- Dev Mode: OLD BFF ProjectReference (DEPRECATED) -->
<ItemGroup Condition="false AND !Exists('../../libs/BackOffice.BFF.Common.Protobuf.dll')">
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Common.Protobuf/BackOffice.BFF.Common.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Category.Protobuf/BackOffice.BFF.Category.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.ClubMembership.Protobuf/BackOffice.BFF.ClubMembership.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Commission.Protobuf/BackOffice.BFF.Commission.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Configuration.Protobuf/BackOffice.BFF.Configuration.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.DiscountCategory.Protobuf/BackOffice.BFF.DiscountCategory.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.DiscountOrder.Protobuf/BackOffice.BFF.DiscountOrder.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.DiscountProduct.Protobuf/BackOffice.BFF.DiscountProduct.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.DiscountShoppingCart.Protobuf/BackOffice.BFF.DiscountShoppingCart.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Health.Protobuf/BackOffice.BFF.Health.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Inventory.Protobuf/BackOffice.BFF.Inventory.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.ManualPayment.Protobuf/BackOffice.BFF.ManualPayment.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.NetworkMembership.Protobuf/BackOffice.BFF.NetworkMembership.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Otp.Protobuf/BackOffice.BFF.Otp.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Package.Protobuf/BackOffice.BFF.Package.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Products.Protobuf/BackOffice.BFF.Products.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.ProductTag.Protobuf/BackOffice.BFF.ProductTag.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.PublicMessage.Protobuf/BackOffice.BFF.PublicMessage.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Role.Protobuf/BackOffice.BFF.Role.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.Tag.Protobuf/BackOffice.BFF.Tag.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.User.Protobuf/BackOffice.BFF.User.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.UserAddress.Protobuf/BackOffice.BFF.UserAddress.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.UserOrder.Protobuf/BackOffice.BFF.UserOrder.Protobuf.csproj" />
<ProjectReference Include="../../../BackOffice.BFF/src/Protobufs/BackOffice.BFF.UserRole.Protobuf/BackOffice.BFF.UserRole.Protobuf.csproj" />
</ItemGroup>
<ItemGroup>
@@ -1,34 +1,54 @@
<MudStack Row="true" AlignItems="AlignItems.Center" Class="pa-2 pr-4 rounded" Style="height:85vh;">
<MudItem xs="2">
<MudPaper Height="85vh" Style="overflow:hidden; position:relative;" Outlined="true" Class="rounded-lg">
<MudStack Class="pa-5" Style="height:100%;">
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudText Typo="Typo.subtitle2">فیلتر ها</MudText>
<MudSpacer />
@if (IsFiltered)
{
<MudButton Color="Color.Error" Variant="Variant.Text" Size="Size.Small" OnClick="@(async () => await OnClearFilterClick.InvokeAsync())">حذف فیلتر</MudButton>
}
</MudStack>
<MudItem Style="height:100%;">
<MudStack StretchItems="StretchItems.None" Class="filter-item" Style="height:100%;">
<MudStack>
@Filters
</MudStack>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Size="Size.Small" FullWidth="true" Class="mt-auto" OnClick="@(async () => await OnSubmitClick.InvokeAsync())">جستجو</MudButton>
</MudStack>
</MudItem>
@* ── Responsive base page: filter above grid, collapsible ── *@
<MudStack Spacing="0" Class="base-page">
@* ── Header row: page title + filter toggle ── *@
@if (Filters != null)
{
<MudPaper Outlined="true" Class="rounded-lg mb-3 base-page__filter-panel">
<MudStack Row="true" AlignItems="AlignItems.Center" Class="pa-3 cursor-pointer"
@onclick="ToggleFilterPanel">
<MudIcon Icon="@Icons.Material.Filled.FilterList" Size="Size.Small"
Color="@(IsFiltered ? Color.Primary : Color.Default)" />
<MudText Typo="Typo.subtitle2" Class="mx-1">فیلترها</MudText>
@if (IsFiltered)
{
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Variant="Variant.Text">فعال</MudChip>
}
<MudSpacer />
<MudIcon Icon="@(_filterOpen ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)"
Size="Size.Small" />
</MudStack>
@if (_filterOpen)
{
<MudDivider />
<div class="pa-3">
<MudGrid Spacing="2">
@Filters
</MudGrid>
<MudStack Row="true" Justify="Justify.FlexEnd" Class="mt-3" Spacing="2">
@if (IsFiltered)
{
<MudButton Color="Color.Error" Variant="Variant.Text" Size="Size.Small"
OnClick="@(async () => await OnClearFilterClick.InvokeAsync())">
پاک کردن فیلتر
</MudButton>
}
<MudButton Variant="Variant.Filled" Color="Color.Primary" Size="Size.Small"
StartIcon="@Icons.Material.Filled.Search"
OnClick="@(async () => await OnSubmitClick.InvokeAsync())">
جستجو
</MudButton>
</MudStack>
</div>
}
</MudPaper>
}
</MudItem>
<MudItem xs="10">
<MudPaper Height="85vh" Style="overflow:hidden; position:relative;" Outlined="true" Class="rounded-lg">
@* ── Content area (DataGrid or custom layout) ── *@
<MudPaper Outlined="true" Class="rounded-lg base-page__content" Style="overflow:hidden; position:relative;">
@Content
</MudPaper>
@Content
</MudPaper>
</MudItem>
</MudStack>
@@ -1,11 +1,17 @@
using Microsoft.AspNetCore.Components;
namespace BackOffice.Common.BaseComponents;
public partial class BasePageComponent
{
[Parameter] public RenderFragment Filters { get; set; } = default!;
[Parameter] public RenderFragment? Filters { get; set; }
[Parameter] public RenderFragment Content { get; set; } = default!;
[Parameter] public bool IsFiltered { get; set; }
[Parameter] public EventCallback OnSubmitClick { get; set; }
[Parameter] public EventCallback OnClearFilterClick { get; set; }
/// <summary>Whether the filter panel is expanded.</summary>
private bool _filterOpen;
private void ToggleFilterPanel() => _filterOpen = !_filterOpen;
}
@@ -0,0 +1,14 @@
/* ── BasePageComponent scoped styles ── */
.base-page {
min-height: 0;
}
.base-page__filter-panel {
transition: all 0.2s ease;
}
.base-page__content {
flex: 1 1 auto;
min-height: 0;
}
@@ -8,21 +8,46 @@ namespace BackOffice.Common.BaseComponents;
public partial class DateRangePicker
{
private DateRange _dateRange = new();
private MudDateRangePicker _picker = default!;
private DateTime? _lastSyncedFrom;
private DateTime? _lastSyncedTo;
private MudDateRangePicker _picker;
[Parameter] public string Label { get; set; } = "انتخاب بازه زمانی";
[Parameter] public DateTime? DefaultStart { get; set; }
[Parameter] public DateTime? DefaultEnd { get; set; }
[Parameter] public EventCallback<DateRange> OnChanged { get; set; }
[Parameter] public DateTime? From { get; set; }
[Parameter] public EventCallback<DateTime?> FromChanged { get; set; }
[Parameter] public DateTime? To { get; set; }
[Parameter] public EventCallback<DateTime?> ToChanged { get; set; }
protected override void OnInitialized()
{
base.OnInitialized();
if (DefaultStart.HasValue)
_dateRange.Start = DefaultStart.Value.Date;
if (DefaultEnd.HasValue)
_dateRange.End = DefaultEnd.Value.Date;
var start = From ?? DefaultStart;
var end = To ?? DefaultEnd;
ApplyExternalRange(start, end);
_lastSyncedFrom = From;
_lastSyncedTo = To;
}
protected override void OnParametersSet()
{
// فقط وقتی والد From/To را عوض کرده (مثلاً پاک کردن فیلتر)، نه هنگام انتخاب داخل پیکر
if (From == _lastSyncedFrom && To == _lastSyncedTo)
return;
_lastSyncedFrom = From;
_lastSyncedTo = To;
ApplyExternalRange(From, To);
}
private void ApplyExternalRange(DateTime? start, DateTime? end)
{
_dateRange = new DateRange(start?.Date, end?.Date);
}
public CultureInfo GetPersianCulture()
{
var culture = new CultureInfo("fa-IR");
@@ -53,19 +78,35 @@ public partial class DateRangePicker
culture.NumberFormat.NumberNegativePattern = 0;
return culture;
}
private async Task OnClickOK()
{
if (_picker.DateRange is not null)
{
await OnChanged.InvokeAsync(_picker.DateRange);
}
var range = _picker.DateRange ?? _dateRange;
_dateRange = range ?? new DateRange();
await NotifyRangeChangedAsync(_dateRange);
await _picker.CloseAsync();
}
private async Task OnClickClear()
{
_dateRange = new DateRange();
await OnChanged.InvokeAsync(_dateRange);
await NotifyRangeChangedAsync(_dateRange);
await _picker.CloseAsync();
}
private async Task NotifyRangeChangedAsync(DateRange range)
{
var start = range.Start?.Date;
var end = range.End?.Date;
_lastSyncedFrom = start;
_lastSyncedTo = end;
if (From != start)
await FromChanged.InvokeAsync(start);
if (To != end)
await ToChanged.InvokeAsync(end);
await OnChanged.InvokeAsync(range);
}
}
@@ -7,6 +7,6 @@
}
else
{
<MudImage Src="@src" Alt="@Alt" Elevation="@Elevation" Class="@Class" Fluid="@Fluid" Height="@Height" ObjectFit="@ObjectFit" ObjectPosition="@ObjectPosition" Style="@(Style + ";cursor:pointer;")" Tag="@Tag" Width="@Width" @onclick="async()=>await OnClick.InvokeAsync()" />
<MudImage Src="@src" Alt="@Alt" Elevation="@Elevation" Class="@Class" Fluid="@Fluid" Height="@Height" ObjectFit="@ObjectFit" ObjectPosition="@ObjectPosition" Style="@Style" Tag="@Tag" Width="@Width" @onclick="async()=>await OnClick.InvokeAsync()" />
}
@@ -4,6 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.Extensions.Configuration;
using MudBlazor;
namespace BackOffice.Common.BaseComponents
@@ -13,13 +14,24 @@ namespace BackOffice.Common.BaseComponents
{
private string src = string.Empty;
[Parameter] public EventCallback OnClick { get; set; }
[Inject] private IConfiguration Configuration { get; set; } = default!;
private string FileBaseUrl => Configuration["GwUrl"]?.TrimEnd('/') ?? "";
protected override async Task OnParametersSetAsync()
{
await base.OnParametersSetAsync();
if (!string.IsNullOrWhiteSpace(Src))
{
src = Src.Contains("data:") ? Src : "https://dl.afrino.co" + Src;
if (Src.Contains("data:") || Src.StartsWith("http"))
src = Src;
else
src = FileBaseUrl + "/uploads/" + Src.TrimStart('/');
}
else
{
src = string.Empty;
}
}
@@ -1,32 +1,49 @@
using BackOffice.BFF.Otp.Protobuf.Protos.Otp;
using BackOffice.BFF.Package.Protobuf.Protos.Package;
using BackOffice.BFF.Role.Protobuf.Protos.Role;
using BackOffice.BFF.Products.Protobuf.Protos.Products;
using BackOffice.BFF.User.Protobuf.Protos.User;
using BackOffice.BFF.UserAddress.Protobuf.Protos.UserAddress;
using BackOffice.BFF.UserOrder.Protobuf.Protos.UserOrder;
using BackOffice.BFF.UserRole.Protobuf.Protos.UserRole;
using BackOffice.BFF.Category.Protobuf.Protos.Category;
using Foursat.BackOffice.BFF.Commission.Protos;
using Foursat.BackOffice.BFF.ClubMembership.Protos;
using Foursat.BackOffice.BFF.Configuration.Protos;
using Foursat.BackOffice.BFF.NetworkMembership.Protos;
using Foursat.BackOffice.BFF.Health.Protobuf;
using BackOffice.BFF.Configuration.Protobuf.Protos.AppVersion;
using CMSMicroservice.Protobuf.Protos.OtpToken;
using CMSMicroservice.Protobuf.Protos.Package;
using CMSMicroservice.Protobuf.Protos.Role;
using CMSMicroservice.Protobuf.Protos.Products;
using CMSMicroservice.Protobuf.Protos.User;
using CMSMicroservice.Protobuf.Protos.UserAddress;
using CMSMicroservice.Protobuf.Protos.UserOrder;
using CMSMicroservice.Protobuf.Protos.UserRole;
using CMSMicroservice.Protobuf.Protos.Category;
using CMSMicroservice.Protobuf.Protos.Commission;
using CMSMicroservice.Protobuf.Protos.ClubMembership;
using CMSMicroservice.Protobuf.Protos.Configuration;
using CMSMicroservice.Protobuf.Protos.NetworkMembership;
using CMSMicroservice.Protobuf.Protos.Health;
using CMSMicroservice.Protobuf.Protos.AppVersion;
// TODO: Create these proto projects - temporarily disabled
// using BackOffice.BFF.DiscountProduct.Protobuf.Protos.DiscountProduct;
// using BackOffice.BFF.DiscountCategory.Protobuf.Protos.DiscountCategory;
// using BackOffice.BFF.DiscountOrder.Protobuf.Protos.DiscountOrder;
// using BackOffice.BFF.Tag.Protobuf.Protos.Tag;
// using BackOffice.BFF.ProductTag.Protobuf.Protos.ProductTag;
// using BackOffice.BFF.PublicMessage.Protobuf.Protos.PublicMessage;
// Discount Shop Proto Clients
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
using CMSMicroservice.Protobuf.Protos.DiscountCategory;
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
using CMSMicroservice.Protobuf.Protos.Tag;
using CMSMicroservice.Protobuf.Protos.ProductTag;
using CMSMicroservice.Protobuf.Protos.ManualPayment;
using CMSMicroservice.Protobuf.Protos;
using CMSMicroservice.Protobuf.Protos.Inventory;
using CMSMicroservice.Protobuf.Protos.BlogPost;
using CMSMicroservice.Protobuf.Protos.BlogCategory;
using CMSMicroservice.Protobuf.Protos.BlogPostImage;
using CMSMicroservice.Protobuf.Protos.SitePage;
using CMSMicroservice.Protobuf.Protos.SitePageSettings;
using CMSMicroservice.Protobuf.Protos.UserContract;
using CMSMicroservice.Protobuf.Protos.UserWallet;
using CMSMicroservice.Protobuf.Protos.UserWalletHistory;
using CMSMicroservice.Protobuf.Protos.PaymentTransaction;
using CMSMicroservice.Protobuf.Protos.Transactions;
using CMSMicroservice.Protobuf.Protos.UserPackagePurchase;
using CMSMicroservice.Protobuf.Protos.ClubMembershipCycle;
using CMSMicroservice.Protobuf.Protos.ClubMembershipCycleHistory;
using BackOffice.Common.Utilities;
// using BackOffice.Services.DiscountProduct;
// using BackOffice.Services.DiscountCategory;
// using BackOffice.Services.DiscountOrder;
// using BackOffice.Services.PublicMessage;
// using BackOffice.Services.Tag;
using BackOffice.Services.DiscountProduct;
using BackOffice.Services.DiscountCategory;
using BackOffice.Services.DiscountOrder;
using BackOffice.Services.PublicMessage;
using BackOffice.Services.Tag;
using BackOffice.Services.Blog;
using BackOffice.Services.Content;
using Blazored.LocalStorage;
using Grpc.Core;
using Grpc.Core.Interceptors;
@@ -37,9 +54,6 @@ using MudBlazor.Services;
using System.Text.Json;
using System.Text.Json.Serialization;
using Foursat.BackOffice.BFF.Health.Protobuf;
using Foursat.BackOffice.BFF.NetworkMembership.Protos;
namespace Microsoft.Extensions.DependencyInjection;
@@ -70,12 +84,20 @@ public static class ConfigureServices
// Application Services
services.AddScoped<BackOffice.Services.Authorization.IAuthorizationService, BackOffice.Services.Authorization.AuthorizationService>();
services.AddScoped<BackOffice.Services.AppVersion.IAppVersionService, BackOffice.Services.AppVersion.AppVersionService>();
// TODO: Re-enable when proto projects are created
// services.AddScoped<IDiscountProductService, DiscountProductService>();
// services.AddScoped<IDiscountCategoryService, DiscountCategoryService>();
// services.AddScoped<IDiscountOrderService, DiscountOrderService>();
// services.AddScoped<IPublicMessageService, PublicMessageService>();
// services.AddScoped<ITagService, TagService>();
// Discount Shop Services
services.AddScoped<IDiscountProductService, DiscountProductService>();
services.AddScoped<IDiscountCategoryService, DiscountCategoryService>();
services.AddScoped<IDiscountOrderService, DiscountOrderService>();
services.AddScoped<IPublicMessageService, PublicMessageService>();
services.AddScoped<ITagService, TagService>();
// Blog & Content Management Services
services.AddScoped<IBlogPostService, BlogPostService>();
services.AddScoped<IBlogCategoryService, BlogCategoryService>();
services.AddScoped<IBlogPostImageService, BlogPostImageService>();
services.AddScoped<ISitePageService, SitePageService>();
services.AddScoped<ISitePageSettingsService, SitePageSettingsService>();
return services;
}
@@ -102,7 +124,7 @@ public static class ConfigureServices
return CreateAuthenticatedChannel(baseUri, httpClient, sp);
});
services.AddTransient(sp => new OtpContract.OtpContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new OtpTokenContract.OtpTokenContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new PackageContract.PackageContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new ProductsContract.ProductsContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new RoleContract.RoleContractClient(sp.GetRequiredService<CallInvoker>()));
@@ -120,18 +142,44 @@ public static class ConfigureServices
services.AddTransient(sp => new HealthContract.HealthContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new AppVersionContract.AppVersionContractClient(sp.GetRequiredService<CallInvoker>()));
// TODO: Re-enable when proto projects are created
// Discount Shop Services
// services.AddTransient(sp => new DiscountProductsContract.DiscountProductsContractClient(sp.GetRequiredService<CallInvoker>()));
// services.AddTransient(sp => new DiscountCategoriesContract.DiscountCategoriesContractClient(sp.GetRequiredService<CallInvoker>()));
// services.AddTransient(sp => new DiscountOrdersContract.DiscountOrdersContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new DiscountProductContract.DiscountProductContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new DiscountCategoryContract.DiscountCategoryContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new DiscountOrderContract.DiscountOrderContractClient(sp.GetRequiredService<CallInvoker>()));
// Public Message Service
// services.AddTransient(sp => new PublicMessagesContract.PublicMessagesContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new PublicMessageContract.PublicMessageContractClient(sp.GetRequiredService<CallInvoker>()));
// Tag Management Services
// services.AddTransient(sp => new TagContract.TagContractClient(sp.GetRequiredService<CallInvoker>()));
// services.AddTransient(sp => new ProductTagContract.ProductTagContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new TagContract.TagContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new ProductTagContract.ProductTagContractClient(sp.GetRequiredService<CallInvoker>()));
// Manual Payment Service
services.AddTransient(sp => new ManualPaymentContract.ManualPaymentContractClient(sp.GetRequiredService<CallInvoker>()));
// Inventory Management Service
services.AddTransient(sp => new InventoryContract.InventoryContractClient(sp.GetRequiredService<CallInvoker>()));
// Blog & Content Management Services
services.AddTransient(sp => new BlogPostContract.BlogPostContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new BlogCategoryContract.BlogCategoryContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new BlogPostImageContract.BlogPostImageContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new SitePageContract.SitePageContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new SitePageSettingsContract.SitePageSettingsContractClient(sp.GetRequiredService<CallInvoker>()));
// User Contracts Service
services.AddTransient(sp => new UserContractContract.UserContractContractClient(sp.GetRequiredService<CallInvoker>()));
// User Wallet Services
services.AddTransient(sp => new UserWalletContract.UserWalletContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new UserWalletHistoryContract.UserWalletHistoryContractClient(sp.GetRequiredService<CallInvoker>()));
// Admin Report Services
services.AddTransient(sp => new PaymentTransactionContract.PaymentTransactionContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new TransactionsContract.TransactionsContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new UserPackagePurchaseContract.UserPackagePurchaseContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new ClubMembershipCycleContract.ClubMembershipCycleContractClient(sp.GetRequiredService<CallInvoker>()));
services.AddTransient(sp => new ClubMembershipCycleHistoryContract.ClubMembershipCycleHistoryContractClient(sp.GetRequiredService<CallInvoker>()));
return services;
}
@@ -1,4 +1,5 @@
using BackOffice.Common.Utilities;
using System.IdentityModel.Tokens.Jwt;
using BackOffice.Common.Utilities;
using Blazored.LocalStorage;
namespace BackOffice.Common.Utilities;
@@ -19,9 +20,41 @@ public class AppTokenProvider : ITokenProvider
{
var authorizationToken = await _localStorage.GetItemAsync<string>(GlobalConstants.JwtTokenKey);
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;
}
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 ? "بله" : "خیر")}";
}
@@ -10,6 +10,7 @@ public static class RouteConstance
public const string UserPage = "/UserPage/";
public const string UserOrder = "/UserOrderPage/";
public const string Orders = "/OrdersPage/";
public const string OrdersSalesReports = "/OrdersSalesReportsPage/";
public const string UserRole = "/UserRolePage/";
public const string UserAddress = "/UserAddressPage/";
public const string Products = "/ProductsPage/";
@@ -17,4 +18,11 @@ public static class RouteConstance
public const string ProductCategories = "/ProductCategoriesPage/";
public const string ProductsBulkEdit = "/ProductsBulkEditPage/";
public const string CategoryProducts = "/CategoryProductsPage/";
// Inventory Management
public const string Inventory = "/InventoryPage/";
public const string InventoryLowStock = "/InventoryLowStockPage/";
public const string InventoryWarehouses = "/InventoryWarehousesPage/";
public const string InventoryAddStock = "/InventoryAddStockPage/";
public const string InventoryMovements = "/InventoryMovementsPage/";
}
+11 -11
View File
@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
FROM 194.5.195.53:32082/dotnet/sdk:9.0 AS build
WORKDIR /src
# Copy NuGet config and project file
@@ -16,7 +16,7 @@ WORKDIR "/src/BackOffice"
RUN dotnet publish "BackOffice.csproj" -c Release -o /app/publish
# Runtime stage - nginx for Blazor WebAssembly
FROM nginx:alpine AS final
FROM 194.5.195.53:32082/nginx:alpine AS final
WORKDIR /usr/share/nginx/html
# Remove default nginx files
@@ -25,14 +25,14 @@ RUN rm -rf /usr/share/nginx/html/*
# Copy published wwwroot (Blazor WASM output)
COPY --from=build /app/publish/wwwroot .
# Configure nginx for SPA routing
RUN echo 'server { \
listen 80; \
server_name _; \
location / { \
root /usr/share/nginx/html; \
try_files $uri $uri/ /index.html; \
} \
}' > /etc/nginx/conf.d/default.conf
# Copy entrypoint script that sets blazor-environment header
COPY ["BackOffice/docker-entrypoint.sh", "/docker-entrypoint.sh"]
RUN chmod +x /docker-entrypoint.sh
# Default Blazor environment (override via K8s env var BLAZOR_ENVIRONMENT)
ENV BLAZOR_ENVIRONMENT=Staging
# Default gateway URL for gRPC calls (injected into appsettings at startup)
ENV GW_URL=https://cms.kbs2.ir
EXPOSE 80
CMD ["/docker-entrypoint.sh"]
+8 -10
View File
@@ -1,18 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
<add key="FourSat" value="https://git.afrino.co/api/packages/FourSat/nuget/index.json" />
<add key="Afrino" value="https://git.afrino.co/api/packages/Afrino/nuget/index.json" />
<clear />
<!-- Local packages (dev builds) -->
<!-- <add key="Local" value="/home/masoud/Apps/project/FourSat/nupkg" /> -->
<!-- Nexus (hosts FourSat packages + proxies nuget.org) -->
<add key="Nexus" value="http://194.5.195.53:32081/repository/nuget-all/index.json" allowInsecureConnections="true" />
</packageSources>
<packageSourceCredentials>
<FourSat>
<add key="Username" value="masoud" />
<Nexus>
<add key="Username" value="admin" />
<add key="ClearTextPassword" value="87zH26nbqT" />
</FourSat>
<Afrino>
<add key="Username" value="systemuser" />
<add key="ClearTextPassword" value="sZSA7PTiv3pUSQZ" />
</Afrino>
</Nexus>
</packageSourceCredentials>
</configuration>
@@ -1,4 +1,4 @@
@using BackOffice.BFF.Category.Protobuf.Protos.Category
@using CMSMicroservice.Protobuf.Protos.Category
<MudAutocomplete T="GetAllCategoryByFilterResponseModel"
Label="@Label"
@@ -1,4 +1,5 @@
using BackOffice.BFF.Category.Protobuf.Protos.Category;
using CMSMicroservice.Protobuf.Protos;
using CMSMicroservice.Protobuf.Protos.Category;
using Microsoft.AspNetCore.Components;
namespace BackOffice.Pages.AutoComplete;
@@ -1,4 +1,4 @@
@using BackOffice.BFF.Category.Protobuf.Protos.Category
@using CMSMicroservice.Protobuf.Protos.Category
<MudAutocomplete T="GetAllCategoryByFilterResponseModel"
Label="@Label"
@@ -1,4 +1,5 @@
using BackOffice.BFF.Category.Protobuf.Protos.Category;
using CMSMicroservice.Protobuf.Protos;
using CMSMicroservice.Protobuf.Protos.Category;
using Microsoft.AspNetCore.Components;
namespace BackOffice.Pages.AutoComplete;
@@ -1,13 +1,15 @@
@using BackOffice.BFF.Category.Protobuf.Protos.Category
@using CMSMicroservice.Protobuf.Protos.Category
<MudSelect T="long"
@ref="_selectRef"
Label="@Label"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Disabled="@Disabled"
MultiSelection="true"
MultiSelectionTextFunc="@GetSelectedText"
SelectedValues="_internalSelectedIds"
SelectedValuesChanged="@(_ => OnSelectedValuesChangedAsync())">
SelectedValuesChanged="OnSelectedValuesChangedAsync">
@foreach (var item in _items)
{
<MudSelectItem T="long" Value="@item.Id">
@@ -1,7 +1,8 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BackOffice.BFF.Category.Protobuf.Protos.Category;
using CMSMicroservice.Protobuf.Protos;
using CMSMicroservice.Protobuf.Protos.Category;
using Microsoft.AspNetCore.Components;
namespace BackOffice.Pages.AutoComplete;
@@ -61,11 +62,9 @@ public partial class CategoryMultiSelectCombo
: new HashSet<long>();
}
private async Task OnSelectedValuesChangedAsync()
private async Task OnSelectedValuesChangedAsync(IEnumerable<long> values)
{
var current = _selectRef?.SelectedValues ?? System.Array.Empty<long>();
_internalSelectedIds = current.Where(id => id > 0).Distinct().ToHashSet();
_internalSelectedIds = values.Where(id => id > 0).Distinct().ToHashSet();
SelectedIds = _internalSelectedIds.ToList();
if (SelectedIdsChanged.HasDelegate)
@@ -73,5 +72,27 @@ public partial class CategoryMultiSelectCombo
await SelectedIdsChanged.InvokeAsync(SelectedIds);
}
}
private string GetSelectedText(List<string> selectedValues)
{
if (selectedValues == null || selectedValues.Count == 0)
return string.Empty;
var titles = new List<string>();
foreach (var val in selectedValues)
{
if (long.TryParse(val, out var id))
{
var item = _items.FirstOrDefault(x => x.Id == id);
titles.Add(item != null ? item.Title : val);
}
else
{
titles.Add(val);
}
}
return string.Join("، ", titles);
}
}
@@ -0,0 +1,17 @@
@using BackOffice.Services.DiscountCategory
<MudSelect @ref="_selectRef"
T="long"
MultiSelection="true"
MultiSelectionTextFunc="@GetSelectedText"
SelectedValues="_internalSelectedIds"
SelectedValuesChanged="OnSelectedValuesChangedAsync"
Label="@Label"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Disabled="@Disabled">
@foreach (var item in _items)
{
<MudSelectItem T="long" Value="@item.Id">@item.Title</MudSelectItem>
}
</MudSelect>
@@ -0,0 +1,119 @@
using BackOffice.Services.DiscountCategory;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace BackOffice.Pages.AutoComplete;
public partial class DiscountCategoryMultiSelectCombo
{
[Inject] public IDiscountCategoryService CategoryService { get; set; } = default!;
[Parameter] public List<long> SelectedIds { get; set; } = new();
[Parameter] public EventCallback<List<long>> SelectedIdsChanged { get; set; }
[Parameter] public string? Label { get; set; } = "انتخاب دسته‌بندی‌ها";
[Parameter] public bool Disabled { get; set; }
private List<DiscountCategoryDto> _items = new();
private HashSet<long> _internalSelectedIds = new();
private MudSelect<long>? _selectRef;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
await LoadCategoriesAsync();
SyncInternalFromParameter();
}
protected override Task OnParametersSetAsync()
{
SyncInternalFromParameter();
return base.OnParametersSetAsync();
}
private async Task LoadCategoriesAsync()
{
try
{
var tree = await CategoryService.GetCategoriesAsync(isActive: true);
_items = FlattenCategories(tree ?? new List<DiscountCategoryDto>());
// Log first few items
if (_items.Count > 0)
{
foreach (var item in _items.Take(5))
{
}
}
}
catch (Exception ex)
{
if (ex.InnerException != null)
{
}
_items = new List<DiscountCategoryDto>();
}
}
private List<DiscountCategoryDto> FlattenCategories(List<DiscountCategoryDto> categories, string prefix = "")
{
var result = new List<DiscountCategoryDto>();
foreach (var category in categories)
{
var catCopy = new DiscountCategoryDto
{
Id = category.Id,
Title = prefix + category.Title,
Name = category.Name,
ParentCategoryId = category.ParentCategoryId,
IsActive = category.IsActive
};
result.Add(catCopy);
if (category.Children?.Any() == true)
{
result.AddRange(FlattenCategories(category.Children.ToList(), prefix + " "));
}
}
return result;
}
private void SyncInternalFromParameter()
{
_internalSelectedIds = SelectedIds != null
? SelectedIds.Where(id => id > 0).Distinct().ToHashSet()
: new HashSet<long>();
}
private async Task OnSelectedValuesChangedAsync(IEnumerable<long> values)
{
_internalSelectedIds = values.Where(id => id > 0).Distinct().ToHashSet();
SelectedIds = _internalSelectedIds.ToList();
if (SelectedIdsChanged.HasDelegate)
{
await SelectedIdsChanged.InvokeAsync(SelectedIds);
}
}
private string GetSelectedText(List<string> selectedValues)
{
if (selectedValues == null || selectedValues.Count == 0)
return string.Empty;
var titles = new List<string>();
foreach (var val in selectedValues)
{
if (long.TryParse(val, out var id))
{
var item = _items.FirstOrDefault(x => x.Id == id);
titles.Add(item != null ? item.Title : val);
}
else
{
titles.Add(val);
}
}
return string.Join("، ", titles);
}
}
@@ -0,0 +1,14 @@
@using CMSMicroservice.Protobuf.Protos.DiscountProduct
<MudAutocomplete T="DiscountProductDto"
Label="@Label"
Value="@_item"
DebounceInterval="700"
ValueChanged="@((e) => OnSelected(e))"
ToStringFunc="@(e => e == null ? null : e.Title)"
SearchFunc="@Search"
Variant="Variant.Outlined"
Clearable="true"
ShowProgressIndicator="true"
CoerceText="false"
OnClearButtonClick="()=> OnSelected(null)" />
@@ -0,0 +1,83 @@
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
using Microsoft.AspNetCore.Components;
namespace BackOffice.Pages.AutoComplete;
public partial class DiscountProductsAutoComplete
{
[Inject] public DiscountProductContract.DiscountProductContractClient DiscountProductService { get; set; } = default!;
[Parameter] public long? Value { get; set; }
[Parameter] public string? Label { get; set; } = "محصول تخفیفی";
[Parameter] public EventCallback<long?> ValueChanged { get; set; }
private List<DiscountProductDto> _items = new();
private DiscountProductDto? _item;
protected override async void OnParametersSet()
{
await base.OnParametersSetAsync();
if (Value.HasValue && Value.Value > 0 && (_item == null || _item.Id != Value.Value))
{
try
{
var getAll = await DiscountProductService.GetDiscountProductsAsync(new GetDiscountProductsRequest
{
PageNumber = 1,
PageSize = 50
});
if (getAll?.Models != null)
{
_item = getAll.Models.FirstOrDefault(x => x.Id == Value.Value);
StateHasChanged();
}
}
catch
{
// Silently handle
}
}
}
private async Task OnSelected(DiscountProductDto? model)
{
if (model == null)
{
_item = null;
await ValueChanged.InvokeAsync(null);
}
else
{
_item = model;
await ValueChanged.InvokeAsync(model.Id);
}
}
private async Task<IEnumerable<DiscountProductDto>> Search(string value, CancellationToken token)
{
var request = new GetDiscountProductsRequest
{
PageNumber = 1,
PageSize = 9
};
if (!string.IsNullOrWhiteSpace(value))
{
request.SearchQuery = value;
}
try
{
var getAll = await DiscountProductService.GetDiscountProductsAsync(request);
_items = getAll?.Models?.ToList() ?? new List<DiscountProductDto>();
}
catch
{
_items = new List<DiscountProductDto>();
}
return _items;
}
}
@@ -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);
}
}
@@ -1,4 +1,4 @@
@using BackOffice.BFF.Products.Protobuf.Protos.Products
@using CMSMicroservice.Protobuf.Protos.Products
<MudAutocomplete T="GetAllProductsByFilterResponseModel"
Label="@Label"
@@ -1,5 +1,5 @@
using BackOffice.BFF.Products.Protobuf.Protos.Products;
using BackOffice.BFF.Protobuf.Common;
using CMSMicroservice.Protobuf.Protos.Products;
using CMSMicroservice.Protobuf.Protos;
using Microsoft.AspNetCore.Components;
namespace BackOffice.Pages.AutoComplete;
@@ -1,4 +1,4 @@
@using BackOffice.BFF.Role.Protobuf.Protos.Role
@using CMSMicroservice.Protobuf.Protos.Role
<MudAutocomplete T="GetAllRoleByFilterResponseModel"
Label="@Label"
@@ -1,4 +1,4 @@
using BackOffice.BFF.Role.Protobuf.Protos.Role;
using CMSMicroservice.Protobuf.Protos.Role;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
@@ -1,4 +1,4 @@
@using BackOffice.BFF.User.Protobuf.Protos.User
@using CMSMicroservice.Protobuf.Protos.User
<MudAutocomplete T="GetAllUserByFilterResponseModel"
Label="@Label"
@@ -1,4 +1,4 @@
using BackOffice.BFF.User.Protobuf.Protos.User;
using CMSMicroservice.Protobuf.Protos.User;
using Grpc.Core;
using Microsoft.AspNetCore.Components;
@@ -1,4 +1,4 @@
@using Foursat.BackOffice.BFF.Commission.Protos
@using CMSMicroservice.Protobuf.Protos.Commission
<MudAutocomplete T="WeekInfo"
Label="@Label"
@@ -1,5 +1,5 @@
using BackOffice.Services;
using Foursat.BackOffice.BFF.Commission.Protos;
using CMSMicroservice.Protobuf.Protos.Commission;
using Grpc.Core;
using Microsoft.AspNetCore.Components;
@@ -0,0 +1,71 @@
@* Merged into /blog *@
@attribute [Authorize(Roles = "Administrator")]
@using BackOffice.Services.Blog
@inject IBlogCategoryService BlogCategoryService
<div>
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">مدیریت دسته‌بندی‌های بلاگ</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="CreateCategory">
افزودن دسته‌بندی
</MudButton>
</MudStack>
<MudDataGrid T="BlogCategoryItemDto"
@ref="_grid"
ServerData="LoadServerData"
Hover="true"
Elevation="1"
Dense="true"
Height="calc(100vh - 240px)">
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<PropertyColumn Property="x => x.Title" Title="عنوان" />
<PropertyColumn Property="x => x.Slug" Title="اسلاگ" />
<PropertyColumn Property="x => x.IconName" Title="آیکون" />
<PropertyColumn Property="x => x.PostCount" Title="تعداد پست" />
<PropertyColumn Property="x => x.IsActive" Title="وضعیت">
<CellTemplate>
<MudChip Color="@(context.Item.IsActive ? Color.Success : Color.Error)"
Size="Size.Small">
@(context.Item.IsActive ? "فعال" : "غیرفعال")
</MudChip>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.SortOrder" Title="ترتیب" />
<TemplateColumn Title="عملیات" Sortable="false">
<CellTemplate>
<MudStack Row="true" Spacing="1">
<MudTooltip Text="ویرایش">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Size="Size.Small"
Color="Color.Primary"
OnClick="@(() => EditCategory(context.Item))" />
</MudTooltip>
<MudTooltip Text="حذف">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Size="Size.Small"
Color="Color.Error"
OnClick="@(() => DeleteCategory(context.Item))" />
</MudTooltip>
</MudStack>
</CellTemplate>
</TemplateColumn>
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="BlogCategoryItemDto" PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد در صفحه" />
</PagerContent>
</MudDataGrid>
</MudStack>
</div>
@@ -0,0 +1,102 @@
using BackOffice.Services.Blog;
using BackOffice.Pages.Blog.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
namespace BackOffice.Pages.Blog;
public partial class BlogCategoryManagementPage
{
private MudDataGrid<BlogCategoryItemDto> _grid = default!;
private async Task<GridData<BlogCategoryItemDto>> LoadServerData(GridState<BlogCategoryItemDto> state)
{
var filter = new BlogCategoryFilterDto
{
PageNumber = state.Page + 1,
PageSize = state.PageSize
};
var result = await BlogCategoryService.GetAllAsync(filter);
return new GridData<BlogCategoryItemDto>
{
Items = result.Items,
TotalItems = result.TotalCount
};
}
private async Task ReloadAsync()
{
if (_grid != null)
{
await _grid.ReloadServerData();
}
}
private async Task CreateCategory()
{
var parameters = new DialogParameters<BlogCategoryEditDialog>
{
{ nameof(BlogCategoryEditDialog.Model), new BlogCategoryEditDto() },
{ nameof(BlogCategoryEditDialog.IsEditMode), false }
};
var dialog = await DialogService.ShowAsync<BlogCategoryEditDialog>("ایجاد دسته‌بندی جدید", parameters,
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true });
var result = await dialog.Result;
if (!result.Canceled)
{
Snackbar.Add("دسته‌بندی با موفقیت ایجاد شد", Severity.Success);
await ReloadAsync();
}
}
private async Task EditCategory(BlogCategoryItemDto item)
{
var dto = new BlogCategoryEditDto
{
Title = item.Title,
Slug = item.Slug,
Description = item.Description,
IconName = item.IconName,
SortOrder = item.SortOrder,
IsActive = item.IsActive
};
var parameters = new DialogParameters<BlogCategoryEditDialog>
{
{ nameof(BlogCategoryEditDialog.Model), dto },
{ nameof(BlogCategoryEditDialog.CategoryId), item.Id },
{ nameof(BlogCategoryEditDialog.IsEditMode), true }
};
var dialog = await DialogService.ShowAsync<BlogCategoryEditDialog>("ویرایش دسته‌بندی", parameters,
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true });
var result = await dialog.Result;
if (!result.Canceled)
{
Snackbar.Add("دسته‌بندی با موفقیت به‌روزرسانی شد", Severity.Success);
await ReloadAsync();
}
}
private async Task DeleteCategory(BlogCategoryItemDto item)
{
var confirmed = await DialogService.ShowMessageBox(
"حذف دسته‌بندی",
$"آیا از حذف دسته‌بندی «{item.Title}» مطمئن هستید؟",
yesText: "حذف",
cancelText: "لغو");
if (confirmed == true)
{
await BlogCategoryService.DeleteAsync(item.Id);
Snackbar.Add("دسته‌بندی حذف شد", Severity.Success);
await ReloadAsync();
}
}
}
+32
View File
@@ -0,0 +1,32 @@
@page "/blog"
@page "/blog/posts"
@page "/blog/categories"
@attribute [Authorize(Roles = "Administrator")]
<div>
<MudText Typo="Typo.h5" Class="mb-3">مدیریت بلاگ</MudText>
<MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true"
PanelClass="pt-4" @bind-ActivePanelIndex="_activeTab">
<MudTabPanel Text="پست‌ها" Icon="@Icons.Material.Filled.PostAdd">
<BlogPostManagementPage />
</MudTabPanel>
<MudTabPanel Text="دسته‌بندی‌ها" Icon="@Icons.Material.Filled.Category">
<BlogCategoryManagementPage />
</MudTabPanel>
</MudTabs>
</div>
@code {
private int _activeTab;
protected override void OnInitialized()
{
var uri = Navigation.ToBaseRelativePath(Navigation.Uri).ToLowerInvariant().TrimEnd('/');
_activeTab = uri switch
{
"blog/categories" => 1,
_ => 0
};
}
}
@@ -0,0 +1,152 @@
@* Merged into /blog *@
@attribute [Authorize(Roles = "Administrator")]
@using BackOffice.Services.Blog
@inject IBlogPostService BlogPostService
@inject IBlogCategoryService BlogCategoryService
<div>
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">مدیریت پست‌های بلاگ</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="CreatePost">
افزودن پست
</MudButton>
</MudStack>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudTextField @bind-Value="_search"
Label="جستجو بر اساس عنوان"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search"
Immediate="true"
OnBlur="ReloadAsync"
OnKeyDown="OnSearchKeyDown" />
<MudSelect @bind-Value="_statusFilter"
Label="وضعیت"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Style="width: 160px;">
<MudSelectItem T="int?" Value="@( (int?)null )">همه</MudSelectItem>
<MudSelectItem T="int?" Value="0">پیش‌نویس</MudSelectItem>
<MudSelectItem T="int?" Value="1">منتشر شده</MudSelectItem>
<MudSelectItem T="int?" Value="2">آرشیو شده</MudSelectItem>
</MudSelect>
<MudSelect @bind-Value="_categoryFilter"
Label="دسته‌بندی"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Style="width: 200px;">
<MudSelectItem T="long?" Value="@( (long?)null )">همه</MudSelectItem>
@foreach (var cat in _categories)
{
<MudSelectItem T="long?" Value="@( (long?)cat.Id )">@cat.Title</MudSelectItem>
}
</MudSelect>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
OnClick="ReloadAsync">
اعمال فیلتر
</MudButton>
</MudStack>
<MudDataGrid T="BlogPostListItemDto"
@ref="_grid"
ServerData="LoadServerData"
Hover="true"
Elevation="1"
Dense="true"
Height="calc(100vh - 240px)">
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<PropertyColumn Property="x => x.Title" Title="عنوان" />
<PropertyColumn Property="x => x.Slug" Title="اسلاگ" />
<PropertyColumn Property="x => x.StatusName" Title="وضعیت">
<CellTemplate>
<MudChip Color="@GetStatusColor(context.Item.Status)"
Size="Size.Small">
@context.Item.StatusName
</MudChip>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.IsFeatured" Title="ویژه">
<CellTemplate>
@if (context.Item.IsFeatured)
{
<MudIcon Icon="@Icons.Material.Filled.Star" Color="Color.Warning" Size="Size.Small" />
}
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.ViewCount" Title="بازدید" />
<TemplateColumn Title="دسته‌بندی‌ها" Sortable="false">
<CellTemplate>
<MudStack Row="true" Spacing="1">
@foreach (var cat in context.Item.Categories)
{
<MudChip Size="Size.Small" Color="Color.Info">@cat.Title</MudChip>
}
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="تاریخ ایجاد">
<CellTemplate>
@context.Item.Created.MiladiToJalali()
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="عملیات" Sortable="false">
<CellTemplate>
<MudStack Row="true" Spacing="1">
@if (context.Item.Status == 0)
{
<MudTooltip Text="انتشار">
<MudIconButton Icon="@Icons.Material.Filled.Publish"
Size="Size.Small"
Color="Color.Success"
OnClick="@(() => PublishPost(context.Item))" />
</MudTooltip>
}
@if (context.Item.Status == 1)
{
<MudTooltip Text="آرشیو">
<MudIconButton Icon="@Icons.Material.Filled.Archive"
Size="Size.Small"
Color="Color.Warning"
OnClick="@(() => ArchivePost(context.Item))" />
</MudTooltip>
}
<MudTooltip Text="ویرایش">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Size="Size.Small"
Color="Color.Primary"
OnClick="@(() => EditPost(context.Item))" />
</MudTooltip>
<MudTooltip Text="حذف">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Size="Size.Small"
Color="Color.Error"
OnClick="@(() => DeletePost(context.Item))" />
</MudTooltip>
</MudStack>
</CellTemplate>
</TemplateColumn>
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="BlogPostListItemDto" PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد در صفحه" />
</PagerContent>
</MudDataGrid>
</MudStack>
</div>
@@ -0,0 +1,189 @@
using BackOffice.Services.Blog;
using BackOffice.Pages.Blog.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
namespace BackOffice.Pages.Blog;
public partial class BlogPostManagementPage
{
private MudDataGrid<BlogPostListItemDto> _grid = default!;
private string? _search;
private int? _statusFilter;
private long? _categoryFilter;
private List<BlogCategoryItemDto> _categories = new();
protected override async Task OnInitializedAsync()
{
_categories = await BlogCategoryService.GetActiveAsync();
}
private async Task<GridData<BlogPostListItemDto>> LoadServerData(GridState<BlogPostListItemDto> state)
{
var filter = new BlogPostFilterDto
{
PageNumber = state.Page + 1,
PageSize = state.PageSize,
SearchTerm = _search,
Status = _statusFilter,
CategoryId = _categoryFilter
};
var result = await BlogPostService.GetAllAsync(filter);
return new GridData<BlogPostListItemDto>
{
Items = result.Items,
TotalItems = result.TotalCount
};
}
private async Task ReloadAsync()
{
if (_grid != null)
{
await _grid.ReloadServerData();
}
}
private void OnSearchKeyDown(KeyboardEventArgs args)
{
if (args.Key == "Enter")
{
_ = ReloadAsync();
}
}
private static Color GetStatusColor(int status) => status switch
{
0 => Color.Default, // Draft
1 => Color.Success, // Published
2 => Color.Warning, // Archived
_ => Color.Default
};
private async Task CreatePost()
{
var parameters = new DialogParameters<BlogPostEditDialog>
{
{ nameof(BlogPostEditDialog.Model), new BlogPostEditDto() },
{ nameof(BlogPostEditDialog.IsEditMode), false },
{ nameof(BlogPostEditDialog.Categories), _categories }
};
var dialog = await DialogService.ShowAsync<BlogPostEditDialog>("ایجاد پست جدید", parameters,
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Large, FullWidth = true });
var result = await dialog.Result;
if (!result.Canceled)
{
Snackbar.Add("پست با موفقیت ایجاد شد", Severity.Success);
await ReloadAsync();
}
}
private async Task EditPost(BlogPostListItemDto item)
{
var details = await BlogPostService.GetByIdAsync(item.Id);
if (details == null)
{
Snackbar.Add("خطا در دریافت اطلاعات پست", Severity.Error);
return;
}
var dto = new BlogPostEditDto
{
Title = details.Title,
Slug = details.Slug,
Summary = details.Summary,
HtmlContent = details.HtmlContent,
FeaturedImagePath = details.FeaturedImagePath,
FeaturedImageThumbnailPath = details.FeaturedImageThumbnailPath,
CategoryIds = details.Categories.Select(c => c.Id).ToList(),
TagIds = details.Tags.Select(t => t.Id).ToList(),
IsFeatured = details.IsFeatured,
SortOrder = details.SortOrder
};
var parameters = new DialogParameters<BlogPostEditDialog>
{
{ nameof(BlogPostEditDialog.Model), dto },
{ nameof(BlogPostEditDialog.PostId), item.Id },
{ nameof(BlogPostEditDialog.IsEditMode), true },
{ nameof(BlogPostEditDialog.Categories), _categories }
};
var dialog = await DialogService.ShowAsync<BlogPostEditDialog>("ویرایش پست", parameters,
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Large, FullWidth = true });
var result = await dialog.Result;
if (!result.Canceled)
{
Snackbar.Add("پست با موفقیت به‌روزرسانی شد", Severity.Success);
await ReloadAsync();
}
}
private async Task PublishPost(BlogPostListItemDto item)
{
var confirmed = await DialogService.ShowMessageBox(
"انتشار پست",
$"آیا از انتشار پست «{item.Title}» مطمئن هستید؟",
yesText: "انتشار",
cancelText: "لغو");
if (confirmed == true)
{
var result = await BlogPostService.PublishAsync(item.Id);
if (result.Success)
{
Snackbar.Add("پست با موفقیت منتشر شد", Severity.Success);
}
else
{
Snackbar.Add(result.Message, Severity.Error);
}
await ReloadAsync();
}
}
private async Task ArchivePost(BlogPostListItemDto item)
{
var confirmed = await DialogService.ShowMessageBox(
"آرشیو پست",
$"آیا از آرشیو کردن پست «{item.Title}» مطمئن هستید؟",
yesText: "آرشیو",
cancelText: "لغو");
if (confirmed == true)
{
var result = await BlogPostService.ArchiveAsync(item.Id);
if (result.Success)
{
Snackbar.Add("پست آرشیو شد", Severity.Success);
}
else
{
Snackbar.Add(result.Message, Severity.Error);
}
await ReloadAsync();
}
}
private async Task DeletePost(BlogPostListItemDto item)
{
var confirmed = await DialogService.ShowMessageBox(
"حذف پست",
$"آیا از حذف پست «{item.Title}» مطمئن هستید؟",
yesText: "حذف",
cancelText: "لغو");
if (confirmed == true)
{
await BlogPostService.DeleteAsync(item.Id);
Snackbar.Add("پست حذف شد", Severity.Success);
await ReloadAsync();
}
}
}
@@ -0,0 +1,48 @@
@using BackOffice.Services.Blog
<MudDialog>
<DialogContent>
<EditForm Model="@Model" OnValidSubmit="HandleValidSubmit">
<MudStack Spacing="2">
<MudTextField @bind-Value="Model.Title"
Label="عنوان"
Required="true"
Variant="Variant.Outlined" />
<MudTextField @bind-Value="Model.Slug"
Label="اسلاگ (URL)"
Required="true"
Variant="Variant.Outlined"
HelperText="مثال: tech-news" />
<MudTextField @bind-Value="Model.Description"
Label="توضیحات"
Variant="Variant.Outlined"
Lines="3" />
<MudTextField @bind-Value="Model.IconName"
Label="نام آیکون"
Variant="Variant.Outlined"
HelperText="مثال: category" />
<MudNumericField T="int"
@bind-Value="Model.SortOrder"
Label="ترتیب نمایش"
Variant="Variant.Outlined" />
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudSwitch T="bool" @bind-Value="Model.IsActive" Color="Color.Primary" />
<MudText Typo="Typo.body2">فعال</MudText>
</MudStack>
</MudStack>
</EditForm>
</DialogContent>
<DialogActions>
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="SubmitAsync">
@(_isEditMode ? "ذخیره تغییرات" : "ایجاد دسته‌بندی")
</MudButton>
<MudButton Color="Color.Default" Variant="Variant.Text" OnClick="Cancel">
انصراف
</MudButton>
</DialogActions>
</MudDialog>
@@ -0,0 +1,41 @@
using BackOffice.Services.Blog;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace BackOffice.Pages.Blog.Components;
public partial class BlogCategoryEditDialog
{
[CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!;
[Inject] public IBlogCategoryService BlogCategoryService { get; set; } = default!;
[Parameter] public BlogCategoryEditDto Model { get; set; } = new();
[Parameter] public long? CategoryId { get; set; }
[Parameter] public bool IsEditMode { get; set; }
private bool _isEditMode => IsEditMode && CategoryId.HasValue;
private async Task HandleValidSubmit()
{
await SubmitAsync();
}
private async Task SubmitAsync()
{
if (_isEditMode && CategoryId.HasValue)
{
await BlogCategoryService.UpdateAsync(CategoryId.Value, Model);
}
else
{
await BlogCategoryService.CreateAsync(Model);
}
DialogInstance.Close(DialogResult.Ok(true));
}
private void Cancel()
{
DialogInstance.Cancel();
}
}
@@ -0,0 +1,98 @@
@using BackOffice.Services.Blog
@using Microsoft.AspNetCore.Components.Forms
@using Tizzani.MudBlazor.HtmlEditor
@using BackOffice.Common.BaseComponents
<MudDialog>
<DialogContent>
<MudForm @ref="_form" @bind-IsValid="@_isValid">
<MudStack Spacing="2">
<MudTextField @bind-Value="Model.Title"
Label="عنوان پست"
Required="true"
Variant="Variant.Outlined" />
<MudTextField @bind-Value="Model.Slug"
Label="نشانی URL (slug)"
Variant="Variant.Outlined"
HelperText="اختیاری — خالی بگذارید تا خودکار از عنوان ساخته شود. فقط حروف کوچک انگلیسی، عدد و خط تیره." />
<MudTextField @bind-Value="Model.Summary"
Label="خلاصه"
Variant="Variant.Outlined"
Lines="3" />
<MudText Typo="Typo.subtitle2">محتوای HTML</MudText>
<MudHtmlEditor @bind-Html="Model.HtmlContent" MinHeight="300px">
<MudHtmlToolbarOptions InsertImage="false" />
</MudHtmlEditor>
<MudText Typo="Typo.subtitle2">تصویر شاخص</MudText>
<MudStack Justify="Justify.Center" AlignItems="AlignItems.Center">
@if (!string.IsNullOrWhiteSpace(_imagePreview))
{
<Image Src="@_imagePreview" Width="280" Height="180" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
}
else if (!string.IsNullOrEmpty(Model.FeaturedImagePath))
{
<Image Src="@Model.FeaturedImagePath" Width="280" Height="180" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
}
else
{
<MudPaper Class="d-flex align-center justify-center" Style="width:280px;height:180px;">
<MudText Typo="Typo.caption">تصویری انتخاب نشده است</MudText>
</MudPaper>
}
<MudFileUpload T="IBrowserFile" Accept="image/*" FilesChanged="OnImageSelected">
<ActivatorContent>
<MudButton HtmlTag="label"
Variant="Variant.Filled"
Color="Color.Primary"
ButtonType="ButtonType.Button"
StartIcon="@Icons.Material.Filled.Image">
انتخاب تصویر شاخص
</MudButton>
</ActivatorContent>
<SelectedTemplate>
@if (context != null)
{
<MudText Class="mt-1" Typo="Typo.caption">@context.Name</MudText>
}
</SelectedTemplate>
</MudFileUpload>
</MudStack>
<MudSelect T="long"
Label="دسته‌بندی‌ها"
MultiSelection="true"
@bind-SelectedValues="_selectedCategoryIds"
Variant="Variant.Outlined"
MultiSelectionTextFunc="@(new Func<List<string>, string>(GetCategorySelectionText))">
@foreach (var cat in Categories)
{
<MudSelectItem T="long" Value="@cat.Id">@cat.Title</MudSelectItem>
}
</MudSelect>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudSwitch T="bool" @bind-Value="Model.IsFeatured" Color="Color.Warning" />
<MudText Typo="Typo.body2">پست ویژه</MudText>
</MudStack>
<MudNumericField T="int"
@bind-Value="Model.SortOrder"
Label="ترتیب نمایش"
Variant="Variant.Outlined" />
</MudStack>
</MudForm>
</DialogContent>
<DialogActions>
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="SubmitAsync" Disabled="@(!_isValid || _loading)">
@(_isEditMode ? "ذخیره تغییرات" : "ایجاد پست")
</MudButton>
<MudButton Color="Color.Default" Variant="Variant.Text" OnClick="Cancel">
انصراف
</MudButton>
</DialogActions>
</MudDialog>
@@ -0,0 +1,115 @@
using BackOffice.Services.Blog;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using MudBlazor;
namespace BackOffice.Pages.Blog.Components;
public partial class BlogPostEditDialog
{
[CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!;
[Inject] public IBlogPostService BlogPostService { get; set; } = default!;
[Parameter] public BlogPostEditDto Model { get; set; } = new();
[Parameter] public long? PostId { get; set; }
[Parameter] public bool IsEditMode { get; set; }
[Parameter] public List<BlogCategoryItemDto> Categories { get; set; } = new();
private bool _isEditMode => IsEditMode && PostId.HasValue;
private MudForm? _form;
private bool _isValid = true;
private bool _loading;
private IBrowserFile? _imageFile;
private byte[]? _imageBuffer;
private string? _imagePreview;
private readonly long _maxAllowedSize = (1024 * 1024) * 5;
private IEnumerable<long> _selectedCategoryIds
{
get => Model.CategoryIds;
set => Model.CategoryIds = value.ToList();
}
private string GetCategorySelectionText(List<string> selectedValues)
{
if (selectedValues == null || !selectedValues.Any())
return "انتخاب دسته‌بندی";
var names = new List<string>();
foreach (var val in selectedValues)
{
if (long.TryParse(val, out var id))
{
var cat = Categories.FirstOrDefault(c => c.Id == id);
if (cat != null) names.Add(cat.Title);
}
}
return names.Any() ? string.Join("، ", names) : "انتخاب دسته‌بندی";
}
private async Task OnImageSelected(IBrowserFile? file)
{
_imageFile = file;
if (file != null)
{
using var ms = new MemoryStream();
await file.OpenReadStream(_maxAllowedSize).CopyToAsync(ms);
_imageBuffer = ms.ToArray();
_imagePreview = $"data:{file.ContentType};base64," + Convert.ToBase64String(_imageBuffer);
StateHasChanged();
}
}
private async Task HandleValidSubmit()
{
await SubmitAsync();
}
private async Task SubmitAsync()
{
if (_form != null)
{
await _form.Validate();
if (!_isValid) return;
}
_loading = true;
// Handle image upload
if (_imageFile != null && _imageBuffer != null)
{
Model.ImageFile = _imageBuffer;
Model.ImageMime = _imageFile.ContentType;
Model.ImageFileName = Path.GetFileNameWithoutExtension(_imageFile.Name);
}
try
{
if (_isEditMode && PostId.HasValue)
{
await BlogPostService.UpdateAsync(PostId.Value, Model);
}
else
{
await BlogPostService.CreateAsync(Model);
}
DialogInstance.Close(DialogResult.Ok(true));
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
private void Cancel()
{
DialogInstance.Cancel();
}
}
@@ -1,27 +1,31 @@
@attribute [Route(RouteConstance.Category)]
@using BackOffice.BFF.Category.Protobuf.Protos.Category
@using CMSMicroservice.Protobuf.Protos.Category
@using BackOffice.Common.BaseComponents
@using BackOffice.Pages.AutoComplete
@using DataModel = BackOffice.BFF.Category.Protobuf.Protos.Category.GetAllCategoryByFilterResponseModel
@using DataModel = CMSMicroservice.Protobuf.Protos.Category.GetAllCategoryByFilterResponseModel
<BasePageComponent @ref="_basePage" OnClearFilterClick="OnFilterCleared" OnSubmitClick="OnFilterSubmit">
<Filters>
<MudTextField T="string" Clearable="true" Label="عنوان" @bind-Value="@_request.Filter.Title" Variant="Variant.Outlined" Margin="Margin.Dense" />
<CategoryAutoComplete Value="_parentFilterId"
ValueChanged="OnParentFilterChanged"
Label="فیلتر بر اساس دسته‌بندی والد" />
<MudItem xs="12" sm="6" md="3">
<MudTextField T="string" Clearable="true" Label="عنوان" @bind-Value="@_request.Filter.Title" Variant="Variant.Outlined" Margin="Margin.Dense" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<CategoryAutoComplete Value="_parentFilterId"
ValueChanged="OnParentFilterChanged"
Label="فیلتر بر اساس دسته‌بندی والد" />
</MudItem>
</Filters>
<Content>
<MudGrid>
<MudItem xs="12" md="3">
<MudPaper Class="pa-2" Style="height:72vh; overflow:auto;">
<MudPaper Class="pa-2" Style="height:calc(100vh - 240px); overflow:auto;">
<MudText Typo="Typo.subtitle2" Class="mb-2">درخت دسته‌بندی‌ها</MudText>
<MudList T="string" Dense="true">
<MudListItem T="string"
OnClick="@(() => OnTreeNodeSelected(null))"
Selected="@(_parentFilterId == null)"
Style="cursor:pointer;">
>
<MudText Typo="Typo.body2">همه دسته‌بندی‌ها</MudText>
</MudListItem>
@foreach (var node in _treeNodes)
@@ -29,7 +33,7 @@
<MudListItem T="string"
OnClick="@(() => OnTreeNodeSelected(node.Id))"
Selected="@(_parentFilterId == node.Id)"
Style="cursor:pointer;">
>
<MudStack Row="true" AlignItems="AlignItems.Center">
<div style="width:@(node.Level * 16)px;"></div>
<MudText Typo="Typo.body2">@node.Title</MudText>
@@ -44,13 +48,14 @@
ServerData="@(new Func<GridState<DataModel>, Task<GridData<DataModel>>>(ServerReload))"
Hover="true"
@ref="_gridData"
Height="72vh">
Height="calc(100vh - 240px)">
<ColGroup>
<col />
<col />
<col />
<col />
<col />
<col />
<col style="width: 80px;" />
</ColGroup>
<ToolBarContent>
@@ -62,7 +67,7 @@
Size="Size.Large"
ButtonType="ButtonType.Button"
OnClick="CreateNew"
Style="cursor:pointer;">
>
افزودن
</MudButton>
</MudStack>
@@ -85,6 +90,8 @@
<MudText Typo="Typo.body2">@parentTitle</MudText>
</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.SortOrder" Title="ترتیب" />
<PropertyColumn Property="x => x.ProductCount" Title="تعداد محصولات" />
<TemplateColumn Title="فعال؟">
<CellTemplate>
<MudChip Color="@(context.Item.IsActive ? Color.Success : Color.Error)"
@@ -102,15 +109,16 @@
Size="Size.Small"
ButtonType="ButtonType.Button"
OnClick="@(() => Update(context.Item))"
Style="cursor:pointer;" />
/>
</MudTooltip>
<MudTooltip Text="حذف">
<MudIconButton Icon="@Icons.Material.Filled.DeleteOutline"
Size="Size.Small"
ButtonType="ButtonType.Button"
Disabled="@HasChildren(context.Item.Id)"
OnClick="@(() => OnDelete(context.Item))"
Style="cursor:pointer;" />
/>
</MudTooltip>
<MudTooltip Text="مدیریت محصولات این دسته (درگ و دراپ)">
@@ -118,17 +126,29 @@
Size="Size.Small"
ButtonType="ButtonType.Button"
OnClick="@(() => OpenCategoryProducts(context.Item))"
Style="cursor:pointer;" />
/>
</MudTooltip>
<MudTooltip Text="افزودن زیردسته">
<MudIconButton Icon="@Icons.Material.Filled.CreateNewFolder"
Size="Size.Small"
Color="Color.Primary"
ButtonType="ButtonType.Button"
OnClick="@(() => CreateSubcategory(context.Item))"
/>
</MudTooltip>
</MudStack>
</CellTemplate>
</TemplateColumn>
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="DataModel"
PageSizeOptions="@(new int[] { 30, 60, 90 })"
PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد سطرهای صفحه" />
RowsPerPageString="تعداد در صفحه" />
</PagerContent>
</MudDataGrid>
</MudItem>
@@ -1,11 +1,12 @@
using System.Text.Json;
using BackOffice.BFF.Category.Protobuf.Protos.Category;
using CMSMicroservice.Protobuf.Protos;
using CMSMicroservice.Protobuf.Protos.Category;
using BackOffice.Common.BaseComponents;
using BackOffice.Common.Utilities;
using Mapster;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using DataModel = BackOffice.BFF.Category.Protobuf.Protos.Category.GetAllCategoryByFilterResponseModel;
using DataModel = CMSMicroservice.Protobuf.Protos.Category.GetAllCategoryByFilterResponseModel;
namespace BackOffice.Pages.Category;
@@ -151,12 +152,30 @@ public partial class CategoryMainPage
}
}
public async Task CreateSubcategory(DataModel parent)
{
var dialog = await DialogService.ShowAsync<CreateOrUpdateCategoryDialog>(
$"افزودن زیردسته برای «{parent.Title}»",
new DialogParameters<CreateOrUpdateCategoryDialog>
{
{ x => x.Model, new CreateNewCategoryRequest { IsActive = true, ParentId = parent.Id } }
},
new DialogOptions { CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.Small });
var result = await dialog.Result;
if (!result.Canceled)
{
await ReloadData();
await LoadTreeAsync();
Snackbar.Add("زیردسته با موفقیت ایجاد شد.", Severity.Success);
}
}
public async Task Update(DataModel model)
{
var getResponse = await CategoryContract.GetCategoryAsync(new GetCategoryRequest { Id = model.Id });
var editModel = getResponse.Adapt<UpdateCategoryRequest>();
// Console.WriteLine(JsonSerializer.Serialize(editModel));
var dialog = await DialogService.ShowAsync<CreateOrUpdateCategoryDialog>(
"ویرایش دسته‌بندی",
new DialogParameters<CreateOrUpdateCategoryDialog>
@@ -179,7 +198,7 @@ public partial class CategoryMainPage
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Small };
bool? result = await DialogService.ShowMessageBox(
"اخطار",
"آیا از حذف این دسته‌بندی مطمئن هستید؟",
$"آیا از حذف دسته‌بندی «{model.Title}» مطمئن هستید؟",
yesText: "حذف",
cancelText: "لغو",
options: options);
@@ -237,4 +256,9 @@ public partial class CategoryMainPage
{
Navigation.NavigateTo($"{RouteConstance.CategoryProducts}{model.Id}");
}
private bool HasChildren(long categoryId)
{
return _treeNodes.Any(n => n.ParentId == categoryId);
}
}
@@ -1,6 +1,6 @@
@attribute [Route(RouteConstance.CategoryProducts + "{CategoryId:long?}")]
@using BackOffice.BFF.Products.Protobuf.Protos.Products
@using CMSMicroservice.Protobuf.Protos.Products
@using BackOffice.Common.BaseComponents
@using BackOffice.Pages.AutoComplete
@@ -1,5 +1,5 @@
using BackOffice.BFF.Products.Protobuf.Protos.Products;
using BackOffice.BFF.Category.Protobuf.Protos.Category;
using CMSMicroservice.Protobuf.Protos.Products;
using CMSMicroservice.Protobuf.Protos.Category;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
@@ -1,46 +1,75 @@
@using BackOffice.BFF.Category.Protobuf.Protos.Category
@using CMSMicroservice.Protobuf.Protos.Category
@using Microsoft.AspNetCore.Components.Forms
@using BackOffice.Pages.AutoComplete
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@(IsEdit ? Icons.Material.Filled.Edit : Icons.Material.Filled.Add)" Class="ml-2" />
@(IsEdit ? "ویرایش دسته‌بندی" : "ایجاد دسته‌بندی جدید")
</MudText>
</TitleContent>
<DialogContent>
<MudStack Spacing="2">
<MudTextField T="string"
@bind-Value="Name"
Label="نام لاتین"
Variant="Variant.Outlined"
Margin="Margin.Dense" />
<MudForm @ref="_form" @bind-IsValid="@_isValid">
<MudStack Spacing="2">
<MudTextField T="string"
@bind-Value="Name"
Label="نام لاتین"
Required="true"
RequiredError="نام لاتین الزامی است"
Variant="Variant.Outlined"
Margin="Margin.Dense" />
<MudTextField T="string"
@bind-Value="Title"
Label="عنوان"
Variant="Variant.Outlined"
Margin="Margin.Dense" />
<MudTextField T="string"
@bind-Value="Title"
Label="عنوان"
Required="true"
RequiredError="عنوان الزامی است"
Variant="Variant.Outlined"
Margin="Margin.Dense" />
<MudTextField T="string"
@bind-Value="Description"
Label="توضیحات"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Lines="3"
TextArea="true" />
<MudTextField T="string"
@bind-Value="Description"
Label="توضیحات"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Lines="3"
TextArea="true" />
<CategoryAutoComplete Value="ParentId"
ValueChanged="OnParentChanged"
Label="دسته‌بندی والد (اختیاری)" />
<MudTextField T="string"
@bind-Value="ImagePath"
Label="آدرس تصویر"
Variant="Variant.Outlined"
Margin="Margin.Dense" />
<MudNumericField T="int"
@bind-Value="SortOrder"
HideSpinButtons="true"
Label="ترتیب نمایش"
Variant="Variant.Outlined"
Margin="Margin.Dense" />
<CategoryAutoComplete Value="ParentId"
ValueChanged="OnParentChanged"
Label="دسته‌بندی والد (اختیاری)" />
<MudText Typo="Typo.caption" Color="Color.Secondary">برای دسته اصلی خالی بگذارید</MudText>
<MudCheckBox T="bool" @bind-Value="IsActive" Label="فعال" />
</MudStack>
<MudNumericField T="int"
@bind-Value="SortOrder"
HideSpinButtons="true"
Label="ترتیب نمایش"
Variant="Variant.Outlined"
Margin="Margin.Dense"
HelperText="عدد کمتر = اولویت بالاتر" />
<MudSwitch T="bool" @bind-Value="IsActive" Label="فعال" Color="Color.Success" />
</MudStack>
</MudForm>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">لغو</MudButton>
<MudButton Color="Color.Primary" OnClick="Save">ثبت</MudButton>
<MudButton OnClick="Cancel" Variant="Variant.Text">لغو</MudButton>
<MudButton Color="Color.Primary"
Variant="Variant.Filled"
OnClick="Save"
Disabled="@(!_isValid || _isSaving)">
@if (_isSaving)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="ml-2" />
}
@(IsEdit ? "ذخیره تغییرات" : "ایجاد دسته‌بندی")
</MudButton>
</DialogActions>
</MudDialog>
@@ -1,4 +1,4 @@
using BackOffice.BFF.Category.Protobuf.Protos.Category;
using CMSMicroservice.Protobuf.Protos.Category;
using Microsoft.AspNetCore.Components;
using MudBlazor;
@@ -14,9 +14,14 @@ public partial class CreateOrUpdateCategoryDialog
private bool IsEdit => EditModel != null;
private MudForm? _form;
private bool _isValid;
private bool _isSaving;
private string Name { get; set; } = string.Empty;
private string Title { get; set; } = string.Empty;
private string? Description { get; set; }
private string? ImagePath { get; set; }
private long? ParentId { get; set; }
private bool IsActive { get; set; } = true;
private int SortOrder { get; set; }
@@ -28,9 +33,9 @@ public partial class CreateOrUpdateCategoryDialog
Name = EditModel.Name;
Title = EditModel.Title;
Description = EditModel.Description;
ImagePath = EditModel.ImagePath;
ParentId = EditModel.ParentId;
IsActive = EditModel.IsActive;
Console.WriteLine(EditModel.IsActive);
SortOrder = EditModel.SortOrder;
}
else if (Model != null)
@@ -38,6 +43,7 @@ public partial class CreateOrUpdateCategoryDialog
Name = Model.Name;
Title = Model.Title;
Description = Model.Description;
ImagePath = Model.ImagePath;
ParentId = Model.ParentId;
IsActive = Model.IsActive;
SortOrder = Model.SortOrder;
@@ -46,37 +52,60 @@ public partial class CreateOrUpdateCategoryDialog
private async Task Save()
{
if (IsEdit && EditModel != null)
if (_form != null)
{
var request = new UpdateCategoryRequest
{
Id = EditModel.Id,
Name = Name,
Title = Title,
Description = Description,
ParentId = ParentId,
IsActive = IsActive,
SortOrder = SortOrder
};
await CategoryContract.UpdateCategoryAsync(request);
}
else
{
var request = new CreateNewCategoryRequest
{
Name = Name,
Title = Title,
Description = Description,
ParentId = ParentId,
IsActive = IsActive,
SortOrder = SortOrder
};
await CategoryContract.CreateNewCategoryAsync(request);
await _form.Validate();
if (!_isValid) return;
}
MudDialog.Close(DialogResult.Ok(true));
_isSaving = true;
StateHasChanged();
try
{
if (IsEdit && EditModel != null)
{
var request = new UpdateCategoryRequest
{
Id = EditModel.Id,
Name = Name,
Title = Title,
Description = Description,
ImagePath = ImagePath,
ParentId = ParentId,
IsActive = IsActive,
SortOrder = SortOrder
};
await CategoryContract.UpdateCategoryAsync(request);
}
else
{
var request = new CreateNewCategoryRequest
{
Name = Name,
Title = Title,
Description = Description,
ImagePath = ImagePath,
ParentId = ParentId,
IsActive = IsActive,
SortOrder = SortOrder
};
await CategoryContract.CreateNewCategoryAsync(request);
}
MudDialog.Close(DialogResult.Ok(true));
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_isSaving = false;
StateHasChanged();
}
}
private Task OnParentChanged(long? parentId)
@@ -0,0 +1,202 @@
@page "/club/features"
@attribute [Authorize(Roles = "Administrator")]
@using CMSMicroservice.Protobuf.Protos.Configuration
@using CMSMicroservice.Protobuf.Protos.ClubMembership
@using BackOffice.Pages.AutoComplete
@inject ConfigurationContract.ConfigurationContractClient ConfigClient
@inject ClubMembershipContract.ClubMembershipContractClient ClubClient
<div>
<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"
PanelClass="pt-4" @bind-ActivePanelIndex="_activeTab">
@* ── تب اول: لیست فیچرها ── *@
<MudTabPanel Text="فیچرهای تعریف‌شده" Icon="@Icons.Material.Filled.Star">
@if (_loadingFeatures)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
}
else if (_features.Count == 0)
{
<MudAlert Severity="Severity.Info" Variant="Variant.Text">فیچری تعریف نشده است.</MudAlert>
}
else
{
<MudGrid Spacing="2">
@foreach (var feature in _features)
{
<MudItem xs="12" sm="6" md="4">
<MudPaper Class="pa-3 rounded-lg" Outlined="true">
<MudStack Spacing="1">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
<MudText Typo="Typo.subtitle1"><strong>@feature.Title</strong></MudText>
<MudChip T="string" Size="Size.Small"
Color="@(feature.IsEnabled ? Color.Success : Color.Error)">
@(feature.IsEnabled ? "فعال" : "غیرفعال")
</MudChip>
</MudStack>
@if (!string.IsNullOrWhiteSpace(feature.Description))
{
<MudText Typo="Typo.body2" Color="Color.Secondary">@feature.Description</MudText>
}
<MudText Typo="Typo.caption">ترتیب: @feature.DisplayOrder</MudText>
</MudStack>
</MudPaper>
</MudItem>
}
</MudGrid>
}
</MudTabPanel>
@* ── تب دوم: فیچرهای کاربران ── *@
<MudTabPanel Text="فیچرهای کاربران" Icon="@Icons.Material.Filled.PeopleAlt">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center" Class="mb-3">
<UserAutoComplete Label="جستجوی کاربر" @bind-SelectedUserId="_featureFilterUserId" />
<MudButton Variant="Variant.Filled" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Search"
OnClick="LoadUserFeatures">جستجو</MudButton>
</MudStack>
@if (_loadingUserFeatures)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
}
else if (_featureFilterUserId == null || _featureFilterUserId == 0)
{
<MudAlert Severity="Severity.Info" Variant="Variant.Text">لطفاً یک کاربر انتخاب کنید.</MudAlert>
}
else if (_userFeatures.Count == 0)
{
<MudAlert Severity="Severity.Info" Variant="Variant.Text">فیچری برای این کاربر یافت نشد.</MudAlert>
}
else
{
<MudDataGrid T="UserClubFeatureModel" Items="_userFeatures"
Hover="true" Dense="true"
Height="calc(100vh - 340px)">
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<PropertyColumn Property="x => x.FeatureTitle" Title="عنوان فیچر" />
<TemplateColumn Title="توضیحات">
<CellTemplate>
<MudText Typo="Typo.body2">@(string.IsNullOrWhiteSpace(context.Item.FeatureDescription) ? "—" : context.Item.FeatureDescription)</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="وضعیت">
<CellTemplate>
<MudSwitch T="bool" Value="context.Item.IsActive"
Color="Color.Success"
ValueChanged="@(val => ToggleFeature(context.Item, val))" />
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="تاریخ اختصاص">
<CellTemplate>
@if (context.Item.GrantedAt != null)
{
@context.Item.GrantedAt.ToDateTime().ToLocalTime().MiladiToJalali()
}
</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.Notes" Title="یادداشت" />
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="UserClubFeatureModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد در صفحه" />
</PagerContent>
</MudDataGrid>
}
</MudTabPanel>
</MudTabs>
</div>
@code {
private int _activeTab;
private bool _loadingFeatures;
private bool _loadingUserFeatures;
// Features tab
private List<ClubFeatureModel> _features = new();
// User features tab
private long? _featureFilterUserId;
private List<UserClubFeatureModel> _userFeatures = new();
protected override async Task OnInitializedAsync()
{
await LoadFeatures();
}
private async Task LoadFeatures()
{
_loadingFeatures = true;
try
{
var result = await ConfigClient.GetClubFeaturesAsync(new Google.Protobuf.WellKnownTypes.Empty());
_features = result?.Features?.ToList() ?? new();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری فیچرها: {ex.Message}", Severity.Error);
}
finally
{
_loadingFeatures = false;
}
}
private async Task LoadUserFeatures()
{
if (_featureFilterUserId == null || _featureFilterUserId == 0) return;
_loadingUserFeatures = true;
try
{
var result = await ClubClient.GetUserClubFeaturesAsync(new GetUserClubFeaturesRequest
{
UserId = _featureFilterUserId.Value
});
_userFeatures = result?.Features?.ToList() ?? new();
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_loadingUserFeatures = false;
}
}
private async Task ToggleFeature(UserClubFeatureModel feature, bool isActive)
{
try
{
await ClubClient.ToggleUserClubFeatureAsync(new ToggleUserClubFeatureRequest
{
UserId = feature.UserId,
ClubFeatureId = feature.ClubFeatureId,
IsActive = isActive
});
feature.IsActive = isActive;
Snackbar.Add($"فیچر «{feature.FeatureTitle}» {(isActive ? "فعال" : "غیرفعال")} شد", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
}
}
+53
View File
@@ -0,0 +1,53 @@
@page "/club"
@page "/club/members"
@page "/club/purchases"
@page "/club/customer-packages"
@page "/club/cycles"
@page "/club/history"
@page "/club/statistics"
@attribute [Authorize]
@using BackOffice.Pages.Club.Reports
<div>
<MudText Typo="Typo.h5" Class="mb-3">باشگاه مشتریان</MudText>
<MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true"
PanelClass="pt-4" @bind-ActivePanelIndex="_activeTab">
<MudTabPanel Text="اعضا" Icon="@Icons.Material.Filled.Groups">
<ClubMembers />
</MudTabPanel>
<MudTabPanel Text="گزارش خرید پکیج" Icon="@Icons.Material.Filled.ShoppingBag">
<ClubPackagePurchasesReport />
</MudTabPanel>
<MudTabPanel Text="گزارش مشتریان پکیج" Icon="@Icons.Material.Filled.PeopleAlt">
<CustomerPackagePurchasesReport />
</MudTabPanel>
<MudTabPanel Text="گزارش دوره‌ها" Icon="@Icons.Material.Filled.Loop">
<ClubCyclesReport />
</MudTabPanel>
<MudTabPanel Text="گزارش تاریخچه" Icon="@Icons.Material.Filled.History">
<ClubHistoriesReport />
</MudTabPanel>
<MudTabPanel Text="آمار" Icon="@Icons.Material.Filled.PieChart">
<Statistics />
</MudTabPanel>
</MudTabs>
</div>
@code {
private int _activeTab;
protected override void OnInitialized()
{
var uri = Navigation.ToBaseRelativePath(Navigation.Uri).ToLowerInvariant().TrimEnd('/');
_activeTab = uri switch
{
"club/purchases" => 1,
"club/customer-packages" => 2,
"club/cycles" => 3,
"club/history" => 4,
"club/statistics" => 5,
_ => 0
};
}
}
+25 -9
View File
@@ -1,22 +1,29 @@
@page "/club/members"
@* Merged into /club *@
@attribute [Authorize]
@using Google.Protobuf.WellKnownTypes
@using BackOffice.Pages.Club.Components
@using Foursat.BackOffice.BFF.ClubMembership.Protos
@using BackOffice.Pages.AutoComplete
@using CMSMicroservice.Protobuf.Protos.ClubMembership
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
<div>
<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"
ServerData="@(new Func<GridState<ClubMembershipModel>, Task<GridData<ClubMembershipModel>>>(ServerReload))"
Hover="true"
@ref="_gridData"
Height="75vh">
Height="calc(100vh - 240px)">
<ToolBarContent>
<MudText Typo="Typo.h6">لیست اعضا</MudText>
<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"
Color="Color.Success"
Label="فقط فعال‌ها" />
@@ -32,6 +39,12 @@
StartIcon="@Icons.Material.Filled.Add">
فعال‌سازی عضو جدید
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Info"
OnClick="ExportToExcel"
StartIcon="@Icons.Material.Filled.FileDownload">
خروجی Excel
</MudButton>
</MudStack>
</ToolBarContent>
<Columns>
@@ -58,14 +71,14 @@
<PropertyColumn Property="x => x.ActivatedAt" Title="تاریخ فعال‌سازی">
<CellTemplate>
@context.Item.ActivatedAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd")
@(context.Item.ActivatedAt != null ? context.Item.ActivatedAt.ToDateTime().ToLocalTime().MiladiToJalali() : "—")
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.ExpiresAt" Title="تاریخ انقضا">
<CellTemplate>
<MudText Color="@(context.Item.IsExpired ? Color.Error : Color.Success)">
@context.Item.ExpiresAt.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd")
@(context.Item.ExpiresAt != null ? context.Item.ExpiresAt.ToDateTime().ToLocalTime().MiladiToJalali() : "—")
</MudText>
</CellTemplate>
</PropertyColumn>
@@ -102,11 +115,14 @@
</CellTemplate>
</TemplateColumn>
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="ClubMembershipModel"
PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد سطرهای صفحه" />
RowsPerPageString="تعداد در صفحه" />
</PagerContent>
</MudDataGrid>
</MudContainer>
</div>
+66 -1
View File
@@ -2,16 +2,21 @@ using Microsoft.AspNetCore.Components;
using MudBlazor;
using Google.Protobuf.WellKnownTypes;
using BackOffice.Pages.Club.Components;
using Foursat.BackOffice.BFF.ClubMembership.Protos;
using CMSMicroservice.Protobuf.Protos.ClubMembership;
using Microsoft.JSInterop;
using System.Text;
using DateTimeConverterCL;
namespace BackOffice.Pages.Club;
public partial class ClubMembers
{
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubContract { get; set; }
[Inject] public IJSRuntime JsRuntime { get; set; }
private MudDataGrid<ClubMembershipModel> _gridData;
private bool _filterIsActive = true;
private long? _filterUserId;
private async Task<GridData<ClubMembershipModel>> ServerReload(GridState<ClubMembershipModel> state)
{
@@ -26,6 +31,9 @@ public partial class ClubMembers
// Always use the filter value since it's now a bool (not nullable)
request.IsActive = _filterIsActive;
if (_filterUserId.HasValue && _filterUserId.Value > 0)
request.UserId = _filterUserId.Value;
var result = await ClubContract.GetAllClubMembershipsAsync(request);
if (result?.Models != null && result.Models.Any())
@@ -108,4 +116,61 @@ public partial class ClubMembers
await ApplyFilter();
}
}
private async Task ExportToExcel()
{
try
{
var request = new GetAllClubMembershipsRequest
{
PageIndex = 1,
PageSize = 1000,
IsActive = _filterIsActive
};
var result = await ClubContract.GetAllClubMembershipsAsync(request);
if (result?.Models == null || !result.Models.Any())
{
Snackbar.Add("داده‌ای برای خروجی وجود ندارد.", Severity.Info);
return;
}
var sb = new StringBuilder();
sb.AppendLine("شناسه,کاربر,نام کاربر,پکیج,کد فعال‌سازی,تاریخ فعال‌سازی,تاریخ انقضا,فعال,منقضی");
foreach (var m in result.Models)
{
sb.AppendLine(string.Join(",",
m.Id,
m.UserId,
EscapeCsv(m.UserName),
EscapeCsv(m.PackageName),
EscapeCsv(m.ActivationCode),
m.ActivatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime(),
m.ExpiresAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime(),
m.IsActive ? "فعال" : "غیرفعال",
m.IsExpired ? "بله" : "خیر"));
}
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
var base64 = Convert.ToBase64String(bytes);
var filename = $"club-members-{DateTime.Now:yyyyMMddHHmmss}.csv";
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
}
}
private static string EscapeCsv(string value)
{
if (string.IsNullOrEmpty(value)) return "";
if (value.Contains(',') || value.Contains('"') || value.Contains('\n'))
return $"\"{value.Replace("\"", "\"\"")}\"";
return value;
}
}
@@ -1,32 +1,39 @@
@using Foursat.BackOffice.BFF.ClubMembership.Protos
@using CMSMicroservice.Protobuf.Protos.ClubMembership
@using CMSMicroservice.Protobuf.Protos.Package
@using BackOffice.Pages.AutoComplete
<MudDialog>
<DialogContent>
<MudForm @ref="_form" @bind-IsValid="_formValid">
<MudStack Spacing="3">
<MudNumericField @bind-Value="_model.UserId"
Label="شناسه کاربر"
Required="true"
RequiredError="شناسه کاربر الزامی است"
Variant="Variant.Outlined" />
<UserAutoComplete Label="انتخاب کاربر" @bind-SelectedUserId="_model.UserId" />
<MudNumericField @bind-Value="_model.PackageId"
Label="شناسه پکیج"
Required="true"
RequiredError="شناسه پکیج الزامی است"
Variant="Variant.Outlined" />
<MudNumericField @bind-Value="_model.DurationMonths"
Label="مدت زمان (ماه)"
Required="true"
RequiredError="مدت زمان الزامی است"
Min="1"
Max="12"
Variant="Variant.Outlined" />
@if (_isLoadingPackages)
{
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Height="56px" />
}
else
{
<MudSelect T="long" @bind-Value="_model.PackageId"
Label="انتخاب پکیج"
Required="true"
RequiredError="انتخاب پکیج الزامی است"
Variant="Variant.Outlined"
AnchorOrigin="Origin.BottomCenter">
@foreach (var pkg in _packages)
{
<MudSelectItem T="long" Value="pkg.Id">
@pkg.Title — @($"{pkg.Price:N0}") ریال
</MudSelectItem>
}
</MudSelect>
}
<MudAlert Severity="Severity.Info" Dense="true">
<MudText Typo="Typo.body2">
با تایید، عضویت باشگاه برای کاربر فعال می‌شود.
<br/>
<small>عضویت باشگاه بدون محدودیت زمانی است.</small>
</MudText>
</MudAlert>
</MudStack>
@@ -37,7 +44,7 @@
<MudButton Color="Color.Success"
Variant="Variant.Filled"
OnClick="Submit"
Disabled="@(!_formValid || _isSubmitting)">
Disabled="@(!_formValid || _isSubmitting || _isLoadingPackages)">
@if (_isSubmitting)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" />
@@ -54,12 +61,49 @@
@code {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; }
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubContract { get; set; }
[Inject] public PackageContract.PackageContractClient PackageClient { get; set; }
private MudForm _form;
private bool _formValid;
private bool _isSubmitting;
private bool _isLoadingPackages = true;
private List<GetAllPackageByFilterResponseModel> _packages = 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 async Task Submit()
@@ -67,19 +111,31 @@
await _form.Validate();
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;
try
{
var request = new ActivateClubMembershipRequest
{
UserId = _model.UserId,
UserId = _model.UserId.Value,
PackageId = _model.PackageId,
DurationMonths = _model.DurationMonths
ForceActivation = true
};
var response = await ClubContract.ActivateClubMembershipAsync(request);
Snackbar.Add($"عضویت با موفقیت فعال شد.", Severity.Success);
Snackbar.Add("عضویت با موفقیت فعال شد.", Severity.Success);
MudDialog.Close(DialogResult.Ok(true));
}
catch (Exception ex)
@@ -94,8 +150,7 @@
private class ActivateModel
{
public long UserId { get; set; }
public long? UserId { get; set; }
public long PackageId { get; set; }
public int DurationMonths { get; set; } = 1;
}
}

Some files were not shown because too many files have changed in this diff Show More