Compare commits

..

4 Commits

Author SHA1 Message Date
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 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 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 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
121 changed files with 2845 additions and 6339 deletions
-2
View File
@@ -1,2 +0,0 @@
# Keep production config from current branch during merges — never overwrite
src/BackOffice/wwwroot/appsettings.Production.json merge=ours
+2 -9
View File
@@ -67,15 +67,8 @@ jobs:
- name: Deploy to Kubernetes
run: |
export SSHPASS="${{ secrets.SERVER_PASSWORD }}"
# Copy K8s manifests to server
sshpass -e scp -o StrictHostKeyChecking=no k8s/staging/backoffice-deployment.yaml root@${{ env.K8S_SERVER }}:/tmp/backoffice-deployment.yaml
# Apply manifests and restart
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
kubectl apply -f /tmp/backoffice-deployment.yaml &&
kubectl rollout restart deployment/backoffice &&
kubectl rollout status deployment/backoffice --timeout=180s &&
rm -f /tmp/backoffice-deployment.yaml
kubectl rollout restart deployment/backoffice
kubectl rollout status deployment/backoffice --timeout=180s
"
echo "✅ Deployed!"
+9 -16
View File
@@ -22,11 +22,11 @@ jobs:
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'DAEMON'
{
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32082"]
}
DAEMON
echo "🚀 Starting Docker daemon..."
dockerd --iptables=false --ip6tables=false --bridge=none --storage-driver=vfs &
dockerd &
for i in $(seq 1 90); do
if docker info >/dev/null 2>&1; then
@@ -40,7 +40,7 @@ jobs:
done
if ! docker info >/dev/null 2>&1; then
echo "❌ Docker daemon failed to start after 3 minutes"
echo "❌ Docker daemon failed to start"
exit 1
fi
@@ -48,28 +48,21 @@ jobs:
run: |
git clone --depth 1 --branch production http://gitea-svc:3000/admin/BackOffice.git .
- name: Login to Docker registries
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login 194.5.195.53:32082 -u admin --password-stdin
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
- name: Build Docker Image
run: |
cd src
DOCKER_BUILDKIT=0 docker build --network host -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
docker build -f BackOffice/Dockerfile \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod \
-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 }}:prod
- name: Deploy to Production
run: |
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!"
sshpass -p "${{ secrets.K8S_SSH_PASSWORD }}" ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} \
"kubectl rollout restart deployment/backoffice && kubectl rollout status deployment/backoffice --timeout=5m" || echo "Deployment pending"
+2 -2
View File
@@ -10,8 +10,8 @@
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
-421
View File
@@ -1,421 +0,0 @@
# سرویس‌های 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
+1009 -3
View File
File diff suppressed because it is too large Load Diff
+750 -2
View File
@@ -1,3 +1,751 @@
# Docs moved to totalDoc
# 📝 BackOffice — Changelog
See [totalDoc/INDEX.md](../../totalDoc/INDEX.md) for all documentation.
**آخرین بروزرسانی:** بهمن ۱۴۰۴ (February 2026)
---
## 🏷 فاز ۱۴ — فیکس Docker Build + Deployment (بهمن ۱۴۰۴)
> **هدف:** رفع مشکلات Docker build و CI/CD pipeline
### ۱۴.۱ — تغییر به NuGet Package ✅
- **مشکل:** `ProjectReference` به `../../../CMS/src/CMSMicroservice.Protobuf/` خارج از Docker build context بود
- **رفع:** تغییر به `PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.178"`
- **فایل:** `BackOffice.csproj`
### ۱۴.۲ — حذف COPY libs/ از Dockerfile ✅
- **مشکل:** `COPY ["libs/", "libs/"]` فیل می‌شد — `libs/` خارج از build context
- **رفع:** حذف خط — دیگه نیازی به DLLهای BFF نیست
- **فایل:** `Dockerfile`
### ۱۴.۳ — تغییر nginx:alpine به رجیستری لوکال ✅
- **مشکل:** `FROM nginx:alpine` از Docker Hub → TLS handshake timeout
- **رفع:** `FROM 194.5.195.53:32082/nginx:alpine`
- **فایل:** `Dockerfile`
### ۱۴.۴ — اضافه شدن SERVER_PASSWORD secret ✅
- **مشکل:** مرحله Deploy to Kubernetes با `Permission denied` فیل می‌شد
- **رفع:** سکرت `SERVER_PASSWORD` به ریپو Gitea اضافه شد (BackOffice + FrontOffice)
---
## 🏷 فاز ۱۳ — بهبودهای UI بک‌آفیس (بهمن ۱۴۰۴)
> **هدف:** رفع مشکلات بصری، بهبود UX فرم‌ها و ناوبری
### ۱۳.۱ — فیکس کامپوننت Image (پیشوند `/uploads/`) ✅
- **مشکل:** تصاویر آپلود‌شده (بلاگ، صفحات سایت، سکشن‌ها) در بک‌آفیس نمایش داده نمی‌شدند — مسیر `GwUrl + Src` با مسیر `UploadsController` هماهنگ نبود
- **رفع:** اضافه شدن `/uploads/` بین `FileBaseUrl` و مسیر نسبی تصویر
- **فایل:** `BackOffice/Common/BaseComponents/Image.razor.cs`
### ۱۳.۲ — فیکس فیلتر هفته در گزارش‌های هفتگی ✅
- **مشکل:** فیلتر هفته از `MudTextField` (ورودی متنی) استفاده می‌کرد — کاربر باید عدد حدس می‌زد
- **رفع:** جایگزینی با `WeekNumberPicker` — انتخاب هفته از لیست با بایندینگ `WeekDefinitionId`
- **فایل‌ها:**
- `CMS/src/CMSMicroservice.Protobuf/Protos/commission.proto` — تغییر فیلدها از `StringValue` به `Int64Value`
- `CMS/.../CommissionCQ/Queries/GetAllWeeklyPools/GetAllWeeklyPoolsQuery.cs` + `Handler.cs`
- `BackOffice/Pages/Commission/WeeklyReports.razor`
### ۱۳.۳ — فیکس دانلود PDF قراردادها ✅
- **مشکل:** فیلد `SignedPdfFile` محتوای HTML کامل دارد (نه URL فایل PDF) — دکمه دانلود کار نمی‌کرد
- **رفع:** تابع JS جدید `jsHtmlToPdf()` برای باز کردن HTML در پنجره جدید و چاپ/ذخیره به‌عنوان PDF
- **فایل‌ها:**
- `BackOffice/wwwroot/js/main.js` — تابع `jsHtmlToPdf`
- `BackOffice/Pages/Contract/UserContractPage.razor`
- `BackOffice/Pages/Contract/ContractDetailsDialog.razor`
### ۱۳.۴ — یکسان‌سازی ارتفاع ویرایشگر HTML ✅
- **مشکل:** ارتفاع `MudHtmlEditor` در صفحات مختلف متفاوت بود — برخی خیلی کوتاه
- **رفع:** اضافه شدن `MinHeight="300px"` به تمام ۷ ویرایشگر HTML
- **فایل‌ها:** `BlogPostEditDialog`, `SitePageSectionEditDialog`, `DiscountShop/ProductFormDialog`, `Package/CreateDialog`, `Package/UpdateDialog`, `Products/CreateDialog`, `Products/UpdateDialog`
### ۱۳.۵ — تولید خودکار اسلاگ بلاگ ✅
- **مشکل:** کاربر باید اسلاگ URL را دستی وارد می‌کرد — غیرضروری و مستعد خطا
- **رفع:** حذف فیلد اسلاگ از فرم، تولید خودکار `post-{GUID}` در بک‌اند (حداکثر ۲۰ کاراکتر)
- **فایل‌ها:**
- `CMS/.../BlogPostCQ/Commands/CreateBlogPost/CreateBlogPostCommandHandler.cs`
- `CMS/.../BlogPostCQ/Commands/UpdateBlogPost/UpdateBlogPostCommandHandler.cs`
- `BackOffice/Pages/Blog/Components/BlogPostEditDialog.razor`
### ۱۳.۶ — انتقال تگ‌ها به منوی جداگانه ✅
- **مشکل:** تگ‌ها فقط از تب سوم بلاگ قابل دسترسی بودند — ولی تگ‌ها بین محصولات و بلاگ مشترکند
- **رفع:** اضافه شدن مسیر `/tags` به `TagManagementPage`، لینک جدید «مدیریت تگ‌ها» در منوی ناوبری، حذف تب تگ‌ها از `BlogHub`
- **فایل‌ها:**
- `BackOffice/Pages/Tag/TagManagementPage.razor`
- `BackOffice/Shared/NavMenu.razor`
- `BackOffice/Pages/Blog/BlogHub.razor`
### ۱۳.۷ — تولید خودکار کلید سکشن ✅
- **مشکل:** کاربر باید کلید سکشن صفحه سایت را دستی وارد می‌کرد (مثلاً `about-us-section`)
- **رفع:** فیلد اختیاری شد، تولید خودکار `section-{GUID}` (حداکثر ۲۰ کاراکتر) در هندلر CMS
- **فایل‌ها:**
- `CMS/.../SitePageCQ/Commands/CreateSitePageSection/CreateSitePageSectionCommandHandler.cs`
- `CMS/.../SitePageCQ/Commands/CreateSitePageSection/CreateSitePageSectionCommandValidator.cs`
- `BackOffice/Pages/Content/Components/SitePageSectionEditDialog.razor`
---
## 🏷 فاز ۱۲ — فیکس فلوی ثبت‌نام / ورود FrontOffice (بهمن ۱۴۰۴)
> **هدف:** رفع ۳ باگ بحرانی در فلوی ثبت‌نام و ورود FrontOffice
> **مستند کامل:** [`totalDoc/cms/REGISTRATION-FLOW-FIXES.md`](../../totalDoc/cms/REGISTRATION-FLOW-FIXES.md)
### ۱۲.۱ — رفع ایجاد کاربر جدید در VerifyOtpToken 🔴 ✅
- **مشکل:** `UserCQ/VerifyOtpTokenCommandHandler` کاربران جدید ایجاد نمی‌کرد — فقط خطای «کاربر یافت نشد»
- **رفع:** اضافه شدن لاجیک کامل: اعتبارسنجی کد معرف، بررسی ظرفیت درخت باینری (حداکثر ۲ فرزند)، تعیین شاخه (چپ/راست)، ایجاد User + UserRole + UserWallet، رویدادهای دامنه
- **فایل:** `CMS/src/CMSMicroservice.Application/UserCQ/Commands/VerifyOtpToken/VerifyOtpTokenCommandHandler.cs`
### ۱۲.۲ — رفع جستجوی موبایل در AcceptContract 🔴 ✅
- **مشکل:** `_currentUserService.Username` = `"{FirstName} {LastName}"` — نه شماره موبایل! در نتیجه OTP و کاربر هیچوقت پیدا نمی‌شد
- **رفع:** ابتدا کاربر از `UserId` پیدا شود، سپس `user.Mobile` برای جستجوی OTP استفاده شود
- **فایل:** `CMS/src/CMSMicroservice.Application/UserCQ/Commands/AcceptContract/AcceptContractCommandHandler.cs`
### ۱۲.۳ — رفع `IsCompleteRegister()` در FrontOffice 🟡 ✅
- **مشکل:** `InitUserAuthInfo().GetAwaiter()` بدون `.GetResult()` — عملیات async اجرا نمی‌شود، منوی کناری وضعیت نادرست
- **رفع:** تغییر به `InitUserAuthInfo().GetAwaiter().GetResult()`
- **فایل:** `FrontOffice/src/FrontOffice.Main/Utilities/AuthService.cs`
---
## 🏷 فاز ۱۱ — رفع خطای صفحات + سرویس عمومی تصاویر (بهمن ۱۴۰۴)
> **هدف:** رفع خطای ۳ صفحه BackOffice + ایجاد اندپوینت HTTP عمومی برای سرو تصاویر
### ۱۱.۱ — سرویس عمومی سرو تصاویر (CMS) ✅
- **`UploadsController`** — اندپوینت `GET /uploads/{path}` برای سرو مستقیم فایل‌ها
- `[AllowAnonymous]` — بدون نیاز به احراز هویت
- کش مرورگر ۲۴ ساعته (`ResponseCache`)
- پشتیبانی از Range Requests
- **FMS Fallback:** اگر فایل محلی نباشد، از `dl.afrino.co` دانلود و کش می‌شود
- `IHttpClientFactory` با named client `"FMS"` (timeout: 30s)
- **فایل‌ها:** `CMS/WebApi/Controllers/UploadsController.cs`, `CMS/WebApi/Program.cs`
### ۱۱.۲ — رفع صفحه «صفحات سایت» ✅
- اضافه شدن **دکمه «افزودن صفحه جدید»** به بالای صفحه
- اضافه شدن **دکمه «حذف»** (آیکون قرمز) برای هر ردیف
- ایجاد **`SitePageCreateDialog`** — دیالوگ ایجاد صفحه جدید با فیلدها:
- کلید صفحه (PageKey)، عنوان، توضیحات متا، عنوان هیرو، زیرعنوان هیرو، تصویر هیرو، وضعیت
- Proto: اضافه شدن `CreateSitePage` و `DeleteSitePage` RPC
- CQRS: `CreateSitePageCommand/Handler` و `DeleteSitePageCommand/Handler`
- **فایل‌ها:**
- `sitepage.proto` — 2 RPC جدید + پیام‌های Request/Response
- `SitePageCQ/Commands/CreateSitePage/` — Command + Handler
- `SitePageCQ/Commands/DeleteSitePage/` — Command + Handler
- `CMS/WebApi/Services/SitePageService.cs` — 2 override جدید
- `BackOffice/Services/Content/ISitePageService.cs``CreateAsync` + `DeleteAsync` + `SitePageCreateDto`
- `BackOffice/Services/Content/SitePageService.cs` — پیاده‌سازی Create/Delete
- `BackOffice/Pages/Content/SitePageManagementPage.razor` — دکمه‌های Add/Delete
- `BackOffice/Pages/Content/SitePageManagementPage.razor.cs` — متدهای CreatePage/DeletePage
- `BackOffice/Pages/Content/Components/SitePageCreateDialog.razor` + `.razor.cs`
### ۱۱.۳ — رفع صفحه «قراردادها» (Contracts) ✅
- **مشکل:** صفحه `/contracts` اصلاً بارگذاری نمی‌شد — crash در DI
- **علت:** `UserContractContract.UserContractContractClient` در DI ثبت نشده بود
- **رفع:** اضافه شدن `using CMSMicroservice.Protobuf.Protos.UserContract` و ثبت gRPC client
- **فایل:** `BackOffice/Common/Configure/ConfigureService.cs`
### ۱۱.۴ — رفع صفحه «کیف پول کاربران» (Wallets) ✅
- **مشکل:** صفحه `/wallets` اصلاً بارگذاری نمی‌شد — crash در DI
- **علت:** دو سرویس ثبت نشده بود:
- `UserWalletContract.UserWalletContractClient`
- `UserWalletChangeLogContract.UserWalletChangeLogContractClient`
- **رفع:** اضافه شدن using‌ها و ثبت هر دو gRPC client
- **فایل:** `BackOffice/Common/Configure/ConfigureService.cs`
---
## 🏷 فاز ۱۰ — بهبود مدیریت محتوا: بلاگ + صفحات سایت (بهمن ۱۴۰۴)
> **هدف:** فعال‌سازی آپلود تصویر باینری و ادیتور HTML در صفحات بلاگ و صفحات سایت
### ۱۰.۱ — بهبود ادیتور پست بلاگ ✅
- **`MudTextField Lines="10"`** → **`MudHtmlEditor`** برای ویرایش محتوای HTML
- **فیلد متنی مسیر تصویر** → **`MudFileUpload`** با پیش‌نمایش تصویر (`Image` component)
- حذف فیلد `FeaturedImageThumbnailPath` (تولید خودکار توسط بک‌اند)
- اضافه شدن `MudForm` با اعتبارسنجی + حالت بارگذاری (`_loading`)
- مدیریت خطا: try-catch + Snackbar
- **فایل‌ها:** `Pages/Blog/Components/BlogPostEditDialog.razor` + `.razor.cs`
### ۱۰.۲ — بهبود ادیتور صفحه سایت ✅
- **فیلد متنی Hero Image** → **`MudFileUpload`** با پیش‌نمایش تصویر
- اضافه شدن `MudForm` + حالت آپلود + مدیریت خطا
- **فایل‌ها:** `Pages/Content/Components/SitePageEditDialog.razor` + `.razor.cs`
### ۱۰.۳ — بهبود ادیتور بخش صفحه سایت (Section) ✅
- **`MudTextField Lines="8"`** → **`MudHtmlEditor`** برای ویرایش محتوای HTML بخش‌ها
- **فیلدهای متنی تصویر/thumbnail** → **`MudFileUpload`** با پیش‌نمایش
- اضافه شدن `MudForm` + حالت آپلود + مدیریت خطا
- **فایل‌ها:** `Pages/Content/Components/SitePageSectionEditDialog.razor` + `.razor.cs`
### ۱۰.۴ — تغییرات Proto ✅
| Proto | تغییر |
|-------|-------|
| `blogpost.proto` | اضافه شدن `BlogImageFileModel` + فیلد `image_file` در Create/Update |
| `sitepage.proto` | اضافه شدن `SitePageImageFileModel` + فیلد `image_file` در Update/CreateSection/UpdateSection |
### ۱۰.۵ — تغییرات CQRS (بک‌اند CMS) ✅
- **Commands:** اضافه شدن `ImageFileBytes`, `ImageMime`, `ImageFileName` به:
- `CreateBlogPostCommand`, `UpdateBlogPostCommand`
- `UpdateSitePageCommand`, `CreateSitePageSectionCommand`, `UpdateSitePageSectionCommand`
- **Handlers:** تزریق `IFileManager` + فراخوانی `UploadImageAsync`:
- BlogPost → `Images/BlogPosts/`
- SitePage → `Images/SitePages/`
- SitePageSection → `Images/SitePageSections/`
### ۱۰.۶ — تغییرات سرویس‌های BackOffice ✅
- `BlogPostService.cs` — ارسال `BlogImageFileModel` با `ByteString.CopyFrom(dto.ImageFile)`
- `SitePageService.cs` — ارسال `SitePageImageFileModel` در Update/CreateSection/UpdateSection
- **DTOs:** اضافه شدن `ImageFile`, `ImageMime`, `ImageFileName` به `BlogPostEditDto`, `SitePageEditDto`, `SitePageSectionEditDto`
### خلاصه آمار فاز ۱۰
| معیار | تعداد |
|---|---|
| فایل‌های proto تغییریافته | 2 |
| Command‌های بروزشده | 5 |
| Handler‌های بروزشده | 5 |
| سرویس‌های gRPC تغییریافته | 2 (BlogPostService + SitePageService) |
| دیالوگ‌های UI بازنویسی‌شده | 3 (BlogPost, SitePage, SitePageSection) |
| سرویس‌های BackOffice بروزشده | 2 |
| DTO‌های بروزشده | 3 |
| Build | 0 Error ✅ (هر ۳ پروژه) |
---
## 🏷 فاز ۹ — معماری مدیریت فایل + رفع سفارشات + مهاجرت DB (بهمن ۱۴۰۴)
> **مستند کامل معماری فایل:** [`totalDoc/cms/FILE-MANAGEMENT-ARCHITECTURE.md`](../../totalDoc/cms/FILE-MANAGEMENT-ARCHITECTURE.md)
### ۹.۱ — حذف کد مرده FMS ✅
- حذف ۳ فایل غیرقابل استفاده مرتبط با FMS خارجی (`https://dl.afrino.co`):
| فایل حذف‌شده | شرح |
|-------------|------|
| `Infrastructure/Services/FmsFileManager.cs` | پیاده‌سازی HTTP upload به FMS |
| `Application/Common/FileManager/FileManagementService.cs` | سرویس قدیمی مدیریت فایل |
| `Application/Common/FileManager/IFileManagementService.cs` | اینترفیس قدیمی |
### ۹.۲ — رفع صفحه سفارشات (Orders) ✅
**مشکل ۱ — صفحه خالی (بدون پیغام خطا)**
- **علت:** `ServerReload` بدون try-catch — exception‌های gRPC ساکت می‌شدند
- **رفع:** اضافه شدن try-catch + نمایش خطا با Snackbar
- **فایل:** `Pages/UserOrder/UserOrderMainPage.razor.cs`
**مشکل ۲ — Shadow FK تکراری در OrderVAT**
- **علت:** `.WithOne()` بدون inverse navigation → EF Core یک shadow FK اضافی می‌ساخت
- **رفع:** `.WithOne()``.WithOne(x => x.OrderVAT)`
- **فایل:** `Persistence/Configurations/OrderVATConfiguration.cs`
**مشکل ۳ — فیلترهای oneof همیشه فعال**
- **علت:** فیلدهای proto `oneof` مقدار پیش‌فرض enum دارند (هیچ‌وقت null نیستند)، پس `PaymentStatus != null` همیشه true بود
- **رفع:** `PaymentStatus != null``HasPaymentStatus == true` (و مشابه برای DeliveryStatus و PaymentMethod)
- **فایل‌ها:** `UserOrderService.cs` — هر دو متد `GetAllUserOrderByFilter` و `GetCustomerOrders`
### ۹.۳ — مهاجرت DB: حذف محدودیت طول ستون‌های تصویر ✅
- **علت:** data URI‌های base64 بزرگ‌تر از `nvarchar(500)` بودند → truncation خاموش
- **رفع:** حذف `HasMaxLength(500)` از تمام ستون‌های image path
- **Migration:** `RemoveImagePathMaxLength`
| Entity | ستون‌ها |
|--------|---------|
| Product | `ImagePath`, `ImageThumbnailPath` |
| DiscountProduct | `ImagePath`, `ImageThumbnailPath` |
| Category | `ImagePath` |
| DiscountCategory | `ImagePath` |
| BlogPost | `FeaturedImagePath`, `FeaturedImageThumbnailPath` |
| SitePage | `HeroImagePath` |
| SitePageSection | `ImagePath`, `ImageThumbnailPath` |
### ۹.۴ — بازنویسی کامل مدیریت فایل: ذخیره دیسکی ✅
**معماری قبلی:** ذخیره data URI مستقیم در DB (ناکارآمد + حجم بالا)
**معماری جدید:**
1. فایل‌ها در دیسک ذخیره می‌شوند: `Uploads/Images/{folder}/{guid}.jpg`
2. مسیر نسبی در DB ذخیره می‌شود
3. `ImagePathResolverInterceptor` هنگام serve تبدیل به base64 می‌کند
**فایل‌های اصلی:**
| فایل | شرح |
|------|------|
| `Application/Common/FileManager/IFileManager.cs` | اینترفیس: Upload, UploadImage, Delete, ResolveImageUrl |
| `Infrastructure/Services/LocalFileManager.cs` | پیاده‌سازی: ImageSharp, main 1200×1200 + thumb 300×300, JPEG Q75 |
| `WebApi/Interceptors/ImagePathResolverInterceptor.cs` | gRPC Interceptor: walk بازگشتی response → تبدیل path به data URI |
**تنظیمات:**
- `FileStorage:UploadPath` → پوشه ریشه آپلود (پیش‌فرض: `AppContext.BaseDirectory/Uploads`)
- gRPC MaxReceiveMessageSize: 50MB
### ۹.۵ — پاکسازی LoggingBehaviour ✅
- **قبل:** فیلدهای باینری (byte[] تصاویر) کامل در لاگ چاپ می‌شدند → لاگ‌های چند مگابایتی
- **بعد:**
- فرمت: `JsonFormatter.Default.Format()` به جای `{@Request}`
- Regex: حذف فیلدهای باینری (`File`, `ImageFile`, `image_file`, `file`)
- محدودیت: حداکثر 2000 کاراکتر
- **فایل:** `WebApi/Common/Behaviours/LoggingBehaviour.cs`
### خلاصه آمار فاز ۹
| معیار | تعداد |
|---|---|
| فایل‌های حذف‌شده (کد مرده) | 3 |
| فایل‌های جدید | 2 (LocalFileManager بازنویسی + ImagePathResolverInterceptor) |
| فایل‌های ویرایش‌شده | ~12 |
| باگ‌های رفع‌شده | 4 (Orders page ×3 + DB truncation) |
| Migration‌های جدید | 1 (RemoveImagePathMaxLength) |
| ستون‌های DB تغییریافته | 9 (nvarchar(500) → nvarchar(max)) |
| Build | 0 Error ✅ (هر ۳ پروژه) |
---
## 🏷 فاز ۸ — یکسان‌سازی فروشگاه عادی و تخفیفی (بهمن ۱۴۰۴)
> **مستند کامل:** [`totalDoc/ui-modernization/BACKOFFICE-STORE-UNIFICATION.md`](../../totalDoc/ui-modernization/BACKOFFICE-STORE-UNIFICATION.md)
### ۸.۱ — بازسازی NavMenu ✅
- ایجاد دو NavGroup مجزا: «فروشگاه عادی» و «فروشگاه تخفیفی»
- حذف «ویرایش دسته‌جمعی» از منو
- انبارداری → گروه مستقل | پکیج‌ها → آیتم مستقل
- **فایل:** `Shared/NavMenu.razor`
### ۸.۲ — حذف گزارش‌های کوچک از سفارشات ✅
- حذف کارت‌های آماری (TotalOrders + TotalAmount) و نمودار Bar ارسال
- حذف فیلدها: `_stats`, `_statusChartLabels`, `_statusChartSeries`
- حذف متد `UpdateStats()` و کلاس `OrderStatsViewModel`
- عنوان: «سفارش‌های کاربر» → «لیست سفارشات»
- **فایل‌ها:** `Pages/UserOrder/UserOrderMainPage.razor` + `.razor.cs`
### ۸.۳ — رفع باگ لیست خالی سفارشات ✅
- **علت:** `PaymentDate.ToDateTime()` بدون null check → exception ساکت در WASM
- **رفع:** `@if (context.Item.PaymentDate != null)` + fallback "-"
- **فایل:** `Pages/UserOrder/UserOrderMainPage.razor`
### ۸.۴ — بازنویسی صفحه محصولات تخفیفی ✅
- تبدیل به `BasePageComponent` (مطابق فروشگاه عادی)
- فیلترهای جدید: وضعیت + موجودی
- ستون‌های جدید: تصویر inline + عنوان truncate + چیپ رنگی موجودی + چیپ وضعیت
- **فایل‌ها:** `Pages/DiscountShop/DiscountProductsMainPage.razor` + `.razor.cs` (بازنویسی کامل)
### ۸.۵ — بازنویسی صفحه دسته‌بندی‌های تخفیفی ✅
- تبدیل به `BasePageComponent` (مطابق فروشگاه عادی)
- ستون‌های جدید: نام لاتین، دسته‌بندی والد (resolve شده)، تعداد محصولات، ترتیب
- code-behind جدید (`@code` درون‌خطی → `.razor.cs`)
- **فایل‌ها:** `Pages/DiscountShop/DiscountCategoriesMainPage.razor` + `.razor.cs` (بازنویسی کامل)
### خلاصه آمار فاز ۸
| معیار | تعداد |
|---|---|
| فایل‌های بازنویسی‌شده | 4 |
| فایل‌های ویرایش‌شده | 3 |
| باگ‌های رفع‌شده | 2 (PaymentDate null + لیست خالی) |
| Build | 0 Error ✅ |
---
## 🏷 فاز ۷ — پس از اجرای Audit v7.0 (تیر ۱۴۰۴)
### خلاصه تغییرات
- **۵۷ مورد** از ۵۸ یافته audit اجرا شد (≈98%)
- **تمام ۱۶ مورد بحرانی 🔴** حل شد
- **تمام ۱۸ مورد جزئی 🟢** حل شد
- **فازهای ۱ تا ۶ تکمیل شدند** (به‌جز DayaLoan و Audit Trail که نیاز به بک‌اند دارند)
- **فاز ۷** — Global Search, Notification Bell, NavMenu Badge, Export Excel 4 صفحه جدید
- **۱۳+ فایل جدید** ساخته شد
- **۹ صفحه** ادغام شدند (۱۴ صفحه → ۵ صفحه)
- **Build**: 0 error ✅
---
## فاز ۷ — Global Search + Notifications + Export All + NavMenu Badge (v7.0)
### 6.5 — Badge Pending در NavMenu — ✅
- **`NavMenu.razor`**: اضافه شدن `MudBadge` روی لینک «درخواست‌های برداشت»
- نمایش تعداد درخواست‌های Pending بصورت real-time از gRPC (`CommissionContract.GetWithdrawalRequestsAsync` با `Status=0`)
- fire-and-forget در `OnInitializedAsync` با try/catch silent
### 6.2 — Global Search در AppBar — ✅
- **فایل جدید**: `Shared/GlobalSearch.razor`
- `MudAutocomplete<SearchResultItem>` با Debounce 400ms
- دو نوع جستجو: ۱) نام صفحات (۱۵ صفحه اصلی) ۲) کاربران از gRPC (`UserContract.GetAllUserByFilterAsync`)
- Template سفارشی با آیکون + عنوان + زیرنویس
- ناوبری خودکار با `Navigation.NavigateTo`
- اضافه شده در `MainLayout.razor` داخل `<AuthorizeView>`
- CSS در `app.css` — rounded border-radius 24px, compact padding
### 6.1 — Notification Bell (placeholder) — ✅
- **`MainLayout.razor`**: اضافه شدن `MudMenu` + `MudBadge` (Dot) + آیکون Notifications
- Dropdown با هدر «اعلان‌ها» + placeholder «اعلان جدیدی وجود ندارد»
- آماده اتصال به بک‌اند — فقط `Visible=true` و populate items
### 6.3 — Export Excel صفحات جدید — ✅ (۱۰ صفحه کامل)
- **۴ صفحه جدید** اضافه شدند به ۶ صفحه قبلی:
1. **UserMainPage** — خروجی CSV: شناسه, موبایل, نام, نام خانوادگی, کدملی
2. **DiscountOrdersMainPage** — خروجی CSV: شماره سفارش, تاریخ, مبلغ کل, تخفیف, قابل پرداخت, تعداد, وضعیت, پرداخت
3. **ManualPayments** — خروجی CSV: شناسه, شناسه کاربر, نام کاربر, مبلغ, نوع, وضعیت, تاریخ
4. **InventoryMainPage** — خروجی CSV: شناسه, محصول, انبار, موجودی, رزرو, قابل فروش, حد هشدار, وضعیت
- هر ۴ صفحه: دکمه «خروجی Excel» + `ExportToExcel()` + `EscapeCsv()` + UTF-8 BOM + `jsSaveAsFile`
- فایل‌های code-behind جدید: `ManualPayments.razor.cs`, `DiscountOrdersMainPage.razor.cs`
---
## فاز ۶ — تاریخ شمسی + تاریخچه‌ها + Export Excel (v6.0)
### تاریخ شمسی یکپارچه (3.6) — ✅ کامل
- **۱۹+ فایل** آپدیت شدند: تمام تاریخ‌ها از فرمت میلادی (`yyyy/MM/dd`) به شمسی تبدیل شدند
- از extension method‌های `DateTimeConverterCL` استفاده شد:
- `.MiladiToJalali()` — فرمت تاریخ فقط (مثال: ۱۴۰۴/۰۴/۱۲)
- `.MiladiToJalaliWithTime()` — فرمت تاریخ و ساعت
- **صفحات اصلاح‌شده شامل**: Club (Members, Statistics, Features, MemberDetailsDialog)، Commission (Dashboard, WeeklyReports, WithdrawalRequests, Payouts, WithdrawalReports)، Network (UserNetworkInfo, BalancesReport, Statistics, NetworkTreeViewer)، UserOrder, Products, Package, Inventory, DiscountShop, Blog, Content, و غیره
- `PropertyColumn Format="yyyy/MM/dd"``TemplateColumn` با `.MiladiToJalali()`
- `MudDatePicker DateFormat` تغییر نکرد (قابلیت تقویم شمسی نیاز به کتابخانه جداگانه دارد)
### تاریخچه عضویت باشگاه (5.7) — ✅
- **`MemberDetailsDialog.razor`**: تب تاریخچه با فیلدهای صحیح proto:
- ستون‌ها: `Action`, `Created`, `PackageName`, `Reason`, `PerformedBy`
- Request: `PageIndex = 0` (نه PageNumber)
### تاریخچه تغییرات شبکه (5.8) — ✅
- **`UserNetworkInfo.razor`**: تب تاریخچه با فیلدهای تخصصی:
- ستون‌ها: `Action`, `Created`, `OldParentId → NewParentId`, `OldNetworkLeg → NewNetworkLeg` (چپ/راست)
- ستون `Reason` و `PerformedBy` اضافه شد
- Request: `PageIndex = 0`
### لاگ اجرای Worker‌ها (5.6) — ✅
- در تب WorkerControl از SystemHub قبلاً پیاده‌سازی شده بود
### Export Excel / CSV (6.3) — ✅ ۶ صفحه
قابلیت خروجی CSV (سازگار با Excel) به ۶ صفحه اصلی اضافه شد:
| صفحه | فایل(ها) | ستون‌های خروجی |
|---|---|---|
| **Products** (قبلاً موجود) | ProductsMainPage.razor.cs | — |
| **Club Members** | ClubMembers.razor + .razor.cs | شناسه,کاربر,نام,پکیج,کد فعال‌سازی,تاریخ فعال‌سازی,تاریخ انقضا,فعال,منقضی |
| **Withdrawal Requests** | WithdrawalRequests.razor + .razor.cs | شناسه,کاربر,نام,مبلغ,وضعیت,روش,شبا,تاریخ درخواست,تاریخ پردازش,مرجع بانکی |
| **Weekly Reports** | WeeklyReports.razor + .razor.cs | هفته,مبلغ استخر,موجودی‌ها,ارزش,وضعیت,تاریخ محاسبه |
| **User Orders** | UserOrderMainPage.razor + .razor.cs | شناسه,کاربر,نام,مبلغ,وضعیت پرداخت,وضعیت ارسال,روش پرداخت,تاریخ |
| **Stock Movements** | MovementsPage.razor + .razor.cs | شناسه,محصول,نوع,تعداد,قبل,بعد,مرجع,یادداشت,تاریخ |
**الگوی پیاده‌سازی:**
- دکمه `<MudButton>` با آیکون `FileDownload` در ToolBarContent
- `StringBuilder` → CSV با هدرهای فارسی
- UTF-8 BOM (`Encoding.UTF8.GetPreamble()`) برای نمایش صحیح فارسی در Excel
- `jsSaveAsFile(filename, base64)` از `wwwroot/js/main.js`
- متد `EscapeCsv()` برای مقادیر حاوی کاما/کوتیشن
- تاریخ‌ها در خروجی نیز شمسی هستند
### فایل‌های جدید (این نسخه)
| فایل | عملکرد |
|---|---|
| `Pages/Commission/WeeklyReports.razor.cs` | Code-behind برای export (به‌دلیل محدودیت Razor parser با escaped quotes) |
---
## نسخه قبلی — Audit v5.0 (خرداد ۱۴۰۴)
### تم رنگی یکپارچه
- **`CustomMudTheme.cs`**: تم Primary از `#0380C0` به `#6366f1` (Indigo) تغییر کرد — هماهنگ با FrontOffice
- **`PaletteDark`** اضافه شد: Surface `#1a1a2e`, Background `#16213e`, AppbarBackground `#0f3460`
- **`app.css`**: رنگ loading از `#1b6ec2` به `var(--mud-palette-primary)` تغییر کرد
- **`NavMenu.razor.css`**: رنگ active از `#0380C0` هاردکد به CSS variable تغییر کرد
- **`MainLayout.razor`**: حذف inline style `border: 1px solid #5e4df9`
### Dark Mode Toggle
- آیکون ماه/آفتاب در AppBar اضافه شد
- `_isDarkMode` state با `MudThemeProvider` اتصال دارد
- تعویض بین `PaletteLight` و `PaletteDark` در CustomMudTheme
### عنوان اپلیکیشن
- "پنل مدیریت **فرصت**" → "پنل مدیریت **کارا بازار سلامت**"
---
## فاز ۲ — بازطراحی لایوت صفحات
### BasePageComponent بازنویسی کامل
- **قبل**: فیلتر ثابت سمت راست (20%) + جدول (80%)، ارتفاع 85vh هاردکد
- **بعد**: فیلتر collapsible بالای جدول، فیلدها در `MudGrid` با `MudItem`، responsive
- **۱۳ صفحه** آپدیت شدند: User, Products, Package, Category, UserOrder, UserAddress, UserRole, Role, BlogPost, BlogCategory, Tag, Transactions, Content
### DataGrid Height یکسان‌سازی
- همه DataGrid‌ها: `Height="calc(100vh - 240px)"`
- **صفحات اصلاح‌شده**: تمام ~33 صفحه دارای DataGrid
### Pager یکسان‌سازی
- **PageSize**: همه `20, 50, 100`
- **متن فارسی**: `RowsPerPageString="تعداد در صفحه"` و `InfoFormat="سطر {first_item} تا {last_item} از {all_items}"`
### MaxWidth یکسان‌سازی
- `MainLayout.razor` MudContainer wrapper اضافه شد → MaxWidth یکسان برای همه صفحات
- **۳۱ صفحه**: حذف `MudContainer` تکراری (double nesting)
### Breadcrumb
- **`Shared/AppBreadcrumb.razor`** ساخته شد
- جداکننده چپ‌به‌راست (`ChevronLeft`)
- نقشه فارسی URL segments (60+ مسیر)
- آخرین آیتم disabled (صفحه فعلی)
### Login + VerifyCode بازطراحی
- عرض ثابت 35% → responsive `MaxWidth.Small`
- لوگو + برندینگ "کارا بازار سلامت" اضافه شد
- طراحی gradient background
---
## فاز ۲.۵ — پاکسازی ساختاری
### NavMenu بازسازی کامل
- **لینک مرده `categories-dragdrop`**: حذف شد
- **Transactions**: حذف از NavMenu (صفحه stub خالی)
- **AlertsMonitoring**: حذف از NavMenu (100% mock)
- **WorkerControl**: به SystemHub منتقل شد (قبلاً لینک نداشت)
- **«پیام‌های عمومی»**: از بخش فروشگاه تخفیفی به «مدیریت محتوا» منتقل شد
- **ساختار گروه‌بندی** اصلاح شد:
- کمیسیون و شبکه: NavGroup واضح
- باشگاه مشتریان: اعضا و آمار + فیچرها
- فروشگاه: پکیج‌ها، محصولات (group)، سفارش‌ها، فروشگاه تخفیفی (group)
- مدیریت: کاربران، نقش‌ها، پرداخت دستی، **کیف‌پول (جدید)**، **قراردادها (جدید)**
- محتوا: بلاگ، صفحات سایت، پیام‌های عمومی
- سیستم: مدیریت سیستم، نسخه اپ‌ها
---
## فاز ۳ — UX بهبودها
### NoRecordsContent (Empty State)
- **۳۳ DataGrid** آپدیت شدند
- پیام: `<MudAlert Severity="Info">موردی یافت نشد.</MudAlert>`
### Delete Confirmation با نام آیتم
- **۱۱ صفحه** اصلاح شدند:
- Role → `«{model.Title}»`
- User → `«{model.FirstName} {model.LastName}»`
- Package → `«{model.Title}»`
- UserAddress → `«{model.Title}»`
- UserRole → `شناسه «{model.Id}»`
- UserOrder → `شماره «{model.Id}»`
- Category → `«{model.Title}»`
- Products → `«{model.Title}»`
- DiscountCategories → `«{category?.Title}»` (lookup اضافه شد)
- DiscountProducts → `«{product?.Title}»` (lookup اضافه شد)
- PublicMessages → `«{message?.Title}»` (lookup اضافه شد)
### User Menu در AppBar
- `MudMenu` جایگزین متن ساده شماره موبایل شد
- آیتم‌ها: نام کاربر (غیرفعال) + خروج
- آیکون `AccountCircle`
- خروج (`signout`) از NavMenu به AppBar منتقل شد
---
## فاز ۳.۵ — ادغام صفحات
### Dashboard Merge ← `Pages/Index.razor`
- **`Index.razor` + `SystemOverview.razor`** ادغام شدند
- Routes: `/` + `/dashboard/overview`
- بخش‌ها:
- 6 کارت آمار CMS (کاربران، محصولات، پکیج‌ها، دسته‌بندی‌ها، سفارش‌ها، مبلغ پرداخت‌شده)
- آمار کمیسیون (هفته جاری، استخر، موجودی‌ها، وضعیت محاسبه)
- آمار باشگاه (کل/فعال/غیرفعال)
- جدول آخرین ۵ سفارش پرداخت‌شده
- ۶ دکمه Quick Action
- بارگذاری موازی با `Task.WhenAll` + `.ResponseAsync`
- `SystemOverview.razor` **حذف شد**
### DiscountShopHub ← `Pages/DiscountShop/DiscountShopHub.razor`
- Routes: `/discount-shop`, `/discount-orders`, `/discount-sales-reports`
- ۲ تب: DiscountOrdersMainPage + SalesReports
- انتخاب تب از URL
### ClubHub ← `Pages/Club/ClubHub.razor`
- Routes: `/club`, `/club/members`, `/club/statistics`
- ۲ تب: ClubMembers + Statistics
### BlogHub ← `Pages/Blog/BlogHub.razor`
- Routes: `/blog`, `/blog/posts`, `/blog/categories`, `/tags`
- ۳ تب: BlogPostManagementPage + BlogCategoryManagementPage + TagManagementPage
### SystemHub ← `Pages/SystemManagement/SystemHub.razor`
- Routes: `/system`, `/system/configuration`, `/system/worker-control`, `/system/health`
- ۳ تب: Configuration + WorkerControl + HealthDashboard
### ۱۰ صفحه — `@page` directive حذف شد
صفحاتی که به hub منتقل شدند دیگر route مستقل ندارند:
DiscountOrdersMainPage, SalesReports, ClubMembers, Statistics, BlogPostManagementPage, BlogCategoryManagementPage, TagManagementPage, Configuration, WorkerControl, HealthDashboard
---
## فاز ۴ — فیکس‌های بیزینسی
### Dashboard آمار اصلاح شد
- **قبل**: آمار از ۱۰ رکورد اول محاسبه می‌شد (نادرست)
- **بعد**: از `metadata.TotalCount` و API‌های جداگانه استفاده می‌شود
### ManualPayments — تأیید/رد
- دکمه‌های Approve/Reject وقتی `Status==0` (در انتظار) نمایش داده می‌شوند
- Confirmation dialog قبل از اجرا
- از `ManualPaymentId` (نه `Id`) استفاده می‌شود
### UserOrder — فیلتر Cancelled
- `<MudSelectItem Value="@(5)">لغو شده</MudSelectItem>` اضافه شد
### WithdrawalRequests — بهبود کامل
- **Pager**: 10/25/50/100 → 20/50/100
- **`WithdrawalDetailsDialog.razor`** (جدید): نمایش کامل اطلاعات کاربر، درخواست، و بانکی
- **`RejectReasonDialog.razor`** (جدید): ورود دلیل رد با اعتبارسنجی
- ViewDetails: از Snackbar TODO به دیالوگ واقعی تغییر کرد
- RejectRequest: از دلیل هاردکد به دیالوگ custom تغییر کرد
---
## فاز ۵ — صفحات مفقود (CMS-Derived)
### WalletManagementPage ← `Pages/Wallet/WalletManagementPage.razor`
- Route: `/wallets`
- **تب ۱ — کیف‌پول‌ها**: DataGrid با ServerData، فیلتر UserAutoComplete
- ستون‌ها: شناسه، کاربر، موجودی اصلی، موجودی شبکه، مشاهده تغییرات
- نوع: `GetAllUserWalletByFilterResponseModel`
- **تب ۲ — تاریخچه تغییرات**: DataGrid با ServerData، فیلتر کاربر + نوع (افزایش/کاهش)
- ستون‌ها: شناسه، کیف‌پول، نوع (چیپ رنگی)، تغییر موجودی، موجودی فعلی، تغییر شبکه، موجودی شبکه فعلی، شناسه مرجع، تاریخ
- نوع: `GetAllUserWalletChangeLogByFilterResponseModel`
- فیلدهای proto با typo: `ChangeNerworkValue`, `RefrenceId`
### UserContractPage ← `Pages/Contract/UserContractPage.razor`
- Route: `/contracts`
- DataGrid با ServerData، فیلتر UserAutoComplete
- ستون‌ها: شناسه، کاربر، قرارداد، کد امضا، لینک PDF، جزئیات
- نوع: `GetAllUserContractByFilterResponseModel`
### ContractDetailsDialog ← `Pages/Contract/ContractDetailsDialog.razor`
- جدول ساده: شناسه، کاربر، قالب قرارداد، کد امضا، لینک دانلود PDF
### ClubFeaturesPage ← `Pages/Club/ClubFeaturesPage.razor`
- Route: `/club/features`
- **تب ۱ — فیچرهای تعریف‌شده**: کارت‌های MudPaper با عنوان، توضیحات، وضعیت (فعال/غیرفعال)، ترتیب
- از `ConfigClient.GetClubFeaturesAsync`
- **تب ۲ — فیچرهای کاربران**: DataGrid با فیلتر کاربر، سوئیچ فعال/غیرفعال
- از `ClubClient.GetUserClubFeaturesAsync` + `ToggleUserClubFeatureAsync`
---
## ⚠️ نکات فنی مهم
### الگوهای Proto
- **فیلتر**: `new GetAllXxxByFilterFilter()` (سطح بالا، بدون `.Types.`)
- **پاسخ لیست**: `GetAllXxxByFilterResponseModel` (نه `GetXxxResponse`)
- **Typo‌ها در proto**: `ChangeNerworkValue` و `RefrenceId` — باید دقیقاً همین‌ها استفاده شوند
### _Imports.razor — سرویس‌های Global
سرویس‌های زیر از `_Imports.razor` تزریق می‌شوند و نباید در code-behind با `[Inject]` دوباره تعریف شوند:
- `NavigationManager Navigation`
- `ILocalStorageService LocalStorageService`
- `ISnackbar Snackbar`
- `IJSRuntime jsRuntime`
- `IDialogService DialogService`
- `AuthenticationStateProvider`
### MudBlazor محدودیت‌ها
- `Variant.Flat` وجود ندارد → از `Variant.Text` استفاده شود
- `GutterSize` روی `MudGrid` deprecated است
---
## فاز ۴ (ادامه) — تکمیل فیکس‌های بیزینسی
### UserSettings — حذف تب‌های جعلی (2.5.5)
- **`UserSettings.razor`**: تب Notifications (۷ تاگل جعلی بدون بک‌اند) → placeholder «به‌زودی فعال می‌شود» با آیکون NotificationsActive
- **`UserSettings.razor`**: تب Security (فرم تغییر رمز + 2FA بدون بک‌اند) → placeholder «به‌زودی فعال می‌شود» با آیکون Lock
- **حذف کد مرده**: ~۱۵ متغیر notification/security + متدهای `SaveNotificationSettings()`, `SaveSecuritySettings()`, `ChangePassword()`
### HealthDashboard — حذف متریک‌های Mock (2.5.6)
- **`HealthDashboard.razor`**: بخش System Resources (۴ کارت CPU=45.2%, Memory=62.8%, Disk=73.5%, Network) → یک کارت با پیام «مانیتورینگ منابع سیستم در نسخه‌های آینده با اتصال به Prometheus/Grafana فعال خواهد شد»
- **حذف کد مرده**: ۹ متغیر mock (`_cpuUsage`, `_memoryUsage`, etc.) + متد `GetResourceColor()`
### حذف cursor:pointer زائد (4.10)
- `Style="cursor:pointer;"` از **۱۷ فایل** حذف شد (MudButton/MudIconButton خودشان cursor:pointer دارند)
- شامل: ProductsMainPage, GalleryDialog, Image.razor (base component), RoleMainPage, UserOrderMainPage, PackageMainPage, etc.
### حذف Console.WriteLine (4.11)
- از **۱۱ فایل** حذف شد: DiscountProductService, DiscountCategoryService, NetworkTreeViewer, UserSettings, CategoryMainPage, UserOrderMainPage, Commission/Dashboard, DiscountProductsMainPage, DiscountCategoryMultiSelectCombo, UserRole/CreateDialog
- **`Commission/Dashboard.razor.cs`**: `using System.Text.Json` هم حذف شد (دیگر استفاده نمی‌شد)
### DiscountProducts — فیلتر دسته‌بندی (4.7)
- **`DiscountProductsMainPage.razor`**: `@inject IDiscountCategoryService` اضافه شد
- دسته‌بندی‌های فعال از سرویس بارگذاری و در MudSelect نمایش داده می‌شوند
- جایگزینی `@* TODO: Load categories *@` با `@foreach` واقعی
### DiscountProducts — Items → ServerData (4.8)
- **`DiscountProductsMainPage.razor`**: تبدیل از `Items="@_products"` به `ServerData="LoadServerData"`
- متد `LoadServerData(GridState<T>)` اضافه شد با `PageNumber` و `PageSize` از state
- `Height="calc(100vh - 300px)"` + `FixedHeader="true"` اضافه شد
- Snackbar اطلاع‌رسانی اضافی در هر بارگذاری حذف شد (redundant)
### UserOrder — فعال‌سازی آمار (4.6)
- **`UserOrderMainPage.razor`**: بخش آمار (MudCard + MudChart) از حالت comment خارج شد
- نمایش تعداد سفارش‌ها + مجموع مبلغ + نمودار Bar وضعیت ارسال
### UserSettings — persist با localStorage (4.9)
- تب General از قبل با localStorage کار می‌کرد ✅ (فقط تب‌های جعلی حذف شدند)
---
## فاز ۴.۵ — Cross-Navigation
### لینک نام کاربر → پروفایل (4.5.1, 4.5.2)
- **`UserOrderMainPage.razor`**: ستون `UserFullName` از PropertyColumn به TemplateColumn تبدیل شد — MudLink به `/network/user-info/{UserId}`
- **`UserPayouts.razor`**: نام/شناسه کاربر به MudLink تبدیل شد → `/network/user-info/{UserId}`
- **`WithdrawalRequests.razor`**: نام کاربر به MudLink تبدیل شد → `/network/user-info/{UserId}`
### لینک محصول ↔ انبار (4.5.3, 4.5.4) — Skip
- صفحه جزئیات محصول وجود ندارد (فقط لیست + دیالوگ ویرایش)
- لینک cross-navigation بدون صفحه مقصد معنادار نیست
### Extract shared status helpers (3.5.6) — Skip
- بررسی نشان داد هر context مقادیر متفاوتی دارد:
- UserPayouts: status 1 = Success/پرداخت شده
- WithdrawalRequests: status 1 = Info/واریز شده
- PayoutDetailsDialog: فقط ۳ وضعیت (نه ۶)
- استخراج به helper مشترک باعث از دست رفتن معنای domain-specific می‌شود
---
## 📈 آمار تغییرات
| معیار | تعداد |
|---|---|
| فایل‌های جدید | 11 |
| فایل‌های حذف‌شده | 1 (SystemOverview.razor) |
| فایل‌های ویرایش‌شده | ~65+ |
| DataGrid‌های آپدیت‌شده | 33 |
| Delete confirmation اصلاح‌شده | 11 |
| صفحات ادغام‌شده | 14 → 5 |
| خطاهای build حل‌شده | 9+ |
| Console.WriteLine حذف‌شده | 22+ (از ۱۱ فایل) |
| cursor:pointer حذف‌شده | 17 فایل |
| Cross-link اضافه‌شده | 3 صفحه (UserOrder, UserPayouts, WithdrawalRequests) |
-82
View File
@@ -1,82 +0,0 @@
---
# 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
+3 -8
View File
@@ -9,8 +9,6 @@
<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>
@@ -138,12 +136,9 @@
<!-- 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.209" />
<!-- CMS Protobuf NuGet package (used for both local dev and Docker builds) -->
<ItemGroup>
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.178" />
</ItemGroup>
<!-- ============================================================ -->
@@ -8,46 +8,21 @@ 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();
var start = From ?? DefaultStart;
var end = To ?? DefaultEnd;
ApplyExternalRange(start, end);
_lastSyncedFrom = From;
_lastSyncedTo = To;
if (DefaultStart.HasValue)
_dateRange.Start = DefaultStart.Value.Date;
if (DefaultEnd.HasValue)
_dateRange.End = DefaultEnd.Value.Date;
}
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");
@@ -78,35 +53,19 @@ public partial class DateRangePicker
culture.NumberFormat.NumberNegativePattern = 0;
return culture;
}
private async Task OnClickOK()
{
var range = _picker.DateRange ?? _dateRange;
_dateRange = range ?? new DateRange();
await NotifyRangeChangedAsync(_dateRange);
if (_picker.DateRange is not null)
{
await OnChanged.InvokeAsync(_picker.DateRange);
}
await _picker.CloseAsync();
}
private async Task OnClickClear()
{
_dateRange = new DateRange();
await NotifyRangeChangedAsync(_dateRange);
await OnChanged.InvokeAsync(_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);
}
}
@@ -27,15 +27,9 @@ 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 CMSMicroservice.Protobuf.Protos.UserWalletChangeLog;
using BackOffice.Common.Utilities;
using BackOffice.Services.DiscountProduct;
using BackOffice.Services.DiscountCategory;
@@ -97,7 +91,6 @@ public static class ConfigureServices
services.AddScoped<IBlogCategoryService, BlogCategoryService>();
services.AddScoped<IBlogPostImageService, BlogPostImageService>();
services.AddScoped<ISitePageService, SitePageService>();
services.AddScoped<ISitePageSettingsService, SitePageSettingsService>();
return services;
}
@@ -165,21 +158,13 @@ public static class ConfigureServices
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>()));
services.AddTransient(sp => new UserWalletChangeLogContract.UserWalletChangeLogContractClient(sp.GetRequiredService<CallInvoker>()));
return services;
}
@@ -1,5 +1,4 @@
using System.IdentityModel.Tokens.Jwt;
using BackOffice.Common.Utilities;
using BackOffice.Common.Utilities;
using Blazored.LocalStorage;
namespace BackOffice.Common.Utilities;
@@ -20,41 +19,9 @@ public class AppTokenProvider : ITokenProvider
{
var authorizationToken = await _localStorage.GetItemAsync<string>(GlobalConstants.JwtTokenKey);
if (!string.IsNullOrEmpty(authorizationToken))
{
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;
_token = authorizationToken.ToString().Replace("Bearer ", "");
}
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;
}
}
}
@@ -1,145 +0,0 @@
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);
}
@@ -1,161 +0,0 @@
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 ?? "");
}
@@ -1,83 +0,0 @@
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 -10
View 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 194.5.195.53:32082/nginx:alpine AS final
FROM 194.5.195.53:32082/library/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 .
# 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.se.kbs1.ir
# 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
EXPOSE 80
CMD ["/docker-entrypoint.sh"]
@@ -1,14 +0,0 @@
@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)" />
@@ -1,83 +0,0 @@
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;
}
}
@@ -1,16 +0,0 @@
@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>
@@ -1,46 +0,0 @@
using CMSMicroservice.Protobuf.Protos.Package;
using Microsoft.AspNetCore.Components;
namespace BackOffice.Pages.AutoComplete;
public partial class PackageSelect
{
[Inject] public PackageContract.PackageContractClient PackageClient { get; set; }
[Parameter] public string Label { get; set; } = "پکیج";
[Parameter] public long? SelectedPackageId { get; set; }
[Parameter] public EventCallback<long?> SelectedPackageIdChanged { get; set; }
private List<GetAllPackageByFilterResponseModel> _packages = new();
protected override async Task OnInitializedAsync()
{
await LoadPackages();
}
private async Task LoadPackages()
{
try
{
var request = new GetAllPackageByFilterRequest
{
PaginationState = new() { PageNumber = 1, PageSize = 100 },
Filter = new()
};
var result = await PackageClient.GetAllPackageByFilterAsync(request);
_packages = result.Models?.OrderBy(p => p.SortOrder).ToList()
?? new List<GetAllPackageByFilterResponseModel>();
}
catch
{
_packages = new List<GetAllPackageByFilterResponseModel>();
}
}
private async Task OnValueChanged(long? value)
{
SelectedPackageId = value;
await SelectedPackageIdChanged.InvokeAsync(value);
}
}
@@ -12,11 +12,6 @@
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"
@@ -11,11 +11,6 @@
<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">
+3 -24
View File
@@ -1,34 +1,17 @@
@page "/club"
@page "/club/members"
@page "/club/purchases"
@page "/club/customer-packages"
@page "/club/cycles"
@page "/club/history"
@page "/club/statistics"
@attribute [Authorize]
@using BackOffice.Pages.Club.Reports
<div>
<MudText Typo="Typo.h5" Class="mb-3">باشگاه مشتریان</MudText>
<MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true"
PanelClass="pt-4" @bind-ActivePanelIndex="_activeTab">
<MudTabPanel Text="اعضا" Icon="@Icons.Material.Filled.Groups">
<MudTabPanel Text="اعضای باشگاه" Icon="@Icons.Material.Filled.Groups">
<ClubMembers />
</MudTabPanel>
<MudTabPanel Text="گزارش خرید پکیج" Icon="@Icons.Material.Filled.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">
<MudTabPanel Text="آمار باشگاه" Icon="@Icons.Material.Filled.PieChart">
<Statistics />
</MudTabPanel>
</MudTabs>
@@ -42,11 +25,7 @@
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,
"club/statistics" => 1,
_ => 0
};
}
+1 -8
View File
@@ -3,17 +3,11 @@
@using Google.Protobuf.WellKnownTypes
@using BackOffice.Pages.Club.Components
@using BackOffice.Pages.AutoComplete
@using CMSMicroservice.Protobuf.Protos.ClubMembership
<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"
@@ -22,8 +16,7 @@
<ToolBarContent>
<MudText Typo="Typo.h6">لیست اعضا</MudText>
<MudSpacer />
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<UserAutoComplete Label="جستجوی کاربر" @bind-SelectedUserId="_filterUserId" />
<MudStack Row="true" Spacing="2">
<MudSwitch T="bool" @bind-Value="_filterIsActive"
Color="Color.Success"
Label="فقط فعال‌ها" />
@@ -16,7 +16,6 @@ public partial class ClubMembers
private MudDataGrid<ClubMembershipModel> _gridData;
private bool _filterIsActive = true;
private long? _filterUserId;
private async Task<GridData<ClubMembershipModel>> ServerReload(GridState<ClubMembershipModel> state)
{
@@ -31,9 +30,6 @@ 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())
@@ -1,39 +1,32 @@
@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">
<UserAutoComplete Label="انتخاب کاربر" @bind-SelectedUserId="_model.UserId" />
<MudNumericField @bind-Value="_model.UserId"
Label="شناسه کاربر"
Required="true"
RequiredError="شناسه کاربر الزامی است"
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>
}
<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" />
<MudAlert Severity="Severity.Info" Dense="true">
<MudText Typo="Typo.body2">
با تایید، عضویت باشگاه برای کاربر فعال می‌شود.
<br/>
<small>عضویت باشگاه بدون محدودیت زمانی است.</small>
</MudText>
</MudAlert>
</MudStack>
@@ -44,7 +37,7 @@
<MudButton Color="Color.Success"
Variant="Variant.Filled"
OnClick="Submit"
Disabled="@(!_formValid || _isSubmitting || _isLoadingPackages)">
Disabled="@(!_formValid || _isSubmitting)">
@if (_isSubmitting)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" />
@@ -61,49 +54,12 @@
@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()
@@ -111,31 +67,19 @@
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.Value,
UserId = _model.UserId,
PackageId = _model.PackageId,
ForceActivation = true
DurationMonths = _model.DurationMonths
};
var response = await ClubContract.ActivateClubMembershipAsync(request);
Snackbar.Add("عضویت با موفقیت فعال شد.", Severity.Success);
Snackbar.Add($"عضویت با موفقیت فعال شد.", Severity.Success);
MudDialog.Close(DialogResult.Ok(true));
}
catch (Exception ex)
@@ -150,7 +94,8 @@
private class ActivateModel
{
public long? UserId { get; set; }
public long UserId { get; set; }
public long PackageId { get; set; }
public int DurationMonths { get; set; } = 1;
}
}
@@ -33,11 +33,23 @@
</tr>
<tr>
<td><strong>پکیج:</strong></td>
<td>@(string.IsNullOrEmpty(Member.PackageName) ? "—" : Member.PackageName)</td>
<td>@Member.PackageName</td>
</tr>
<tr>
<td><strong>کد فعال‌سازی:</strong></td>
<td dir="ltr">@Member.ActivationCode</td>
</tr>
<tr>
<td><strong>تاریخ فعال‌سازی:</strong></td>
<td>@(Member.ActivatedAt != null ? Member.ActivatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() : "—")</td>
<td>@Member.ActivatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td>
</tr>
<tr>
<td><strong>تاریخ انقضا:</strong></td>
<td>
<MudText Color="@(Member.IsExpired ? Color.Error : Color.Success)">
@Member.ExpiresAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()
</MudText>
</td>
</tr>
<tr>
<td><strong>وضعیت:</strong></td>
@@ -50,16 +62,59 @@
</td>
</tr>
<tr>
<td><strong>تاریخ ایجاد:</strong></td>
<td>@Member.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td>
<td><strong>منقضی شده:</strong></td>
<td>
<MudChip T="string"
Color="@(Member.IsExpired ? Color.Warning : Color.Default)"
Size="Size.Small">
@(Member.IsExpired ? "بله" : "خیر")
</MudChip>
</td>
</tr>
</tbody>
</MudSimpleTable>
</MudPaper>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text">
تاریخچه تغییرات عضویت در تب «گزارش تاریخچه» باشگاه قابل مشاهده است.
</MudAlert>
<MudPaper Class="pa-3" Elevation="0">
<MudText Typo="Typo.h6" Color="Color.Primary">تاریخچه</MudText>
<MudDivider Class="my-2" />
<MudSimpleTable Dense="true">
<tbody>
<tr>
<td><strong>تاریخ ایجاد:</strong></td>
<td>@Member.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td>
</tr>
</tbody>
</MudSimpleTable>
@if (_historyLoading)
{
<MudProgressLinear Indeterminate="true" Color="Color.Primary" Class="my-2" />
}
else if (_history.Any())
{
<MudTable T="ClubMembershipHistoryModel" Items="_history" Dense="true" Hover="true" Class="mt-2">
<HeaderContent>
<MudTh>عملیات</MudTh>
<MudTh>تاریخ</MudTh>
<MudTh>دلیل</MudTh>
<MudTh>توسط</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="عملیات">
<MudChip T="string" Size="Size.Small" Color="Color.Info">@context.Action</MudChip>
</MudTd>
<MudTd DataLabel="تاریخ">@context.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</MudTd>
<MudTd DataLabel="دلیل">@(string.IsNullOrEmpty(context.Reason) ? "-" : context.Reason)</MudTd>
<MudTd DataLabel="توسط">@(string.IsNullOrEmpty(context.PerformedBy) ? "-" : context.PerformedBy)</MudTd>
</RowTemplate>
</MudTable>
}
else
{
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="mt-2">تاریخچه‌ای یافت نشد.</MudText>
}
</MudPaper>
</MudStack>
}
</DialogContent>
@@ -71,5 +126,39 @@
@code {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; }
[Parameter] public ClubMembershipModel Member { get; set; }
[Inject] public ClubMembershipContract.ClubMembershipContractClient ClubClient { get; set; }
private List<ClubMembershipHistoryModel> _history = new();
private bool _historyLoading;
protected override async Task OnInitializedAsync()
{
await LoadHistory();
}
private async Task LoadHistory()
{
_historyLoading = true;
try
{
var request = new GetClubMembershipHistoryRequest
{
UserId = Member.UserId,
PageIndex = 0,
PageSize = 50
};
var response = await ClubClient.GetClubMembershipHistoryAsync(request);
_history = response.Models?.ToList() ?? new();
}
catch
{
_history = new();
}
finally
{
_historyLoading = false;
}
}
private void Close() => MudDialog.Close();
}
@@ -1,153 +0,0 @@
@using BackOffice.Common.BaseComponents
@using BackOffice.Pages.AutoComplete
@using CMSMicroservice.Protobuf.Protos.ClubMembershipCycle
@using Google.Protobuf.WellKnownTypes
@inject ClubMembershipCycleContract.ClubMembershipCycleContractClient CycleClient
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
گزارش دوره‌های عضویت باشگاه — هر خرید پکیج یک دور جدید ایجاد می‌کند.
</MudAlert>
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
<Filters>
<MudItem xs="12" sm="6" md="3">
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_userId" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="bool?" Clearable="true" Label="دوره جاری" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_isCurrent">
<MudSelectItem T="bool?" Value="@((bool?)true)">فقط دوره جاری</MudSelectItem>
<MudSelectItem T="bool?" Value="@((bool?)false)">دوره‌های قبلی</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="int?" Clearable="true" Label="وضعیت جادویی" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_magicState">
<MudSelectItem T="int?" Value="0">عادی</MudSelectItem>
<MudSelectItem T="int?" Value="1">در حالت جادویی</MudSelectItem>
<MudSelectItem T="int?" Value="2">جادویی تکمیل‌شده</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه خرید پکیج" />
</MudItem>
</Filters>
<Content>
<MudGrid Class="mb-3" Spacing="2">
<MudItem xs="4">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">کل دوره‌ها</MudText>
<MudText Typo="Typo.h6">@_totalCount.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="4">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">دوره جاری</MudText>
<MudText Typo="Typo.h6">@_currentCount.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="4">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">جادویی فعال</MudText>
<MudText Typo="Typo.h6">@_magicActive.ToString("N0")</MudText>
</MudPaper>
</MudItem>
</MudGrid>
<MudDataGrid @ref="_grid" T="ClubMembershipCycleModel" ServerData="LoadData" ReadOnly="true"
Hover="true" Dense="true" Height="calc(100vh - 420px)">
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<PropertyColumn Property="x => x.UserName" Title="کاربر" />
<PropertyColumn Property="x => x.CycleNumber" Title="شماره دور" />
<PropertyColumn Property="x => x.PackageName" Title="پکیج" />
<TemplateColumn Title="مبلغ (ریال)">
<CellTemplate>@context.Item.PackageAmount.ToString("N0")</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="روش خرید">
<CellTemplate>@ReportLabels.GetPurchaseMethodLabel(context.Item.PurchaseMethod)</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="تاریخ خرید">
<CellTemplate>@context.Item.PackagePurchasedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="وضعیت جادویی">
<CellTemplate>
@ReportLabels.GetMagicStateLabel(null,
context.Item.MagicStartedAt?.ToDateTime(),
context.Item.MagicCompletedAt?.ToDateTime())
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="دوره جاری">
<CellTemplate>
<MudChip T="string" Size="Size.Small" Color="@(context.Item.IsCurrentCycle ? Color.Success : Color.Default)">
@(context.Item.IsCurrentCycle ? "بله" : "خیر")
</MudChip>
</CellTemplate>
</TemplateColumn>
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="ClubMembershipCycleModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد در صفحه" />
</PagerContent>
</MudDataGrid>
</Content>
</BasePageComponent>
@code {
private BasePageComponent? _basePage;
private MudDataGrid<ClubMembershipCycleModel>? _grid;
private long? _userId;
private bool? _isCurrent;
private int? _magicState;
private DateTime? _fromDate, _toDate;
private long _totalCount, _currentCount, _magicActive;
private GetAllClubMembershipCycleByFilterFilter BuildFilter() => new()
{
UserId = _userId > 0 ? _userId : null,
IsCurrentCycle = _isCurrent,
MagicState = _magicState,
PackagePurchasedFrom = _fromDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_fromDate.Value, DateTimeKind.Utc)) : null,
PackagePurchasedTo = _toDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_toDate.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc)) : null
};
private async Task<GridData<ClubMembershipCycleModel>> LoadData(GridState<ClubMembershipCycleModel> state)
{
try
{
var filter = BuildFilter();
var summary = await CycleClient.GetClubMembershipCycleSummaryAsync(new GetClubMembershipCycleSummaryRequest { Filter = filter });
_totalCount = summary.TotalCount;
_currentCount = summary.CurrentCycleCount;
_magicActive = summary.MagicActiveCount;
var result = await CycleClient.GetAllClubMembershipCycleByFilterAsync(new GetAllClubMembershipCycleByFilterRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize },
Filter = filter
});
return new GridData<ClubMembershipCycleModel>
{
Items = result?.Models?.ToList() ?? new(),
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
return new GridData<ClubMembershipCycleModel>();
}
}
private async Task OnFilterSubmit() { if (_grid != null) await _grid.ReloadServerData(); }
private async Task OnFilterCleared()
{
_userId = null; _isCurrent = null; _magicState = null; _fromDate = _toDate = null;
if (_grid != null) await _grid.ReloadServerData();
}
}
@@ -1,153 +0,0 @@
@using BackOffice.Common.BaseComponents
@using BackOffice.Pages.AutoComplete
@using CMSMicroservice.Protobuf.Protos.ClubMembership
@using CMSMicroservice.Protobuf.Protos.ClubMembershipCycleHistory
@using Google.Protobuf.WellKnownTypes
@inject ClubMembershipContract.ClubMembershipContractClient ClubClient
@inject ClubMembershipCycleHistoryContract.ClubMembershipCycleHistoryContractClient CycleHistoryClient
<MudTabs Elevation="0" Rounded="true" Border="true" ApplyEffectsToContainer="true" PanelClass="pt-4">
<MudTabPanel Text="تغییرات عضویت" Icon="@Icons.Material.Filled.History">
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
تاریخچه فعال/غیرفعال شدن عضویت باشگاه.
</MudAlert>
<MudStack Row="true" Spacing="2" Class="mb-3">
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_memUserId" />
<MudSelect T="int?" Clearable="true" Label="عملیات" Variant="Variant.Outlined" Margin="Margin.Dense"
@bind-Value="_memAction" Style="max-width:200px;">
<MudSelectItem T="int?" Value="0">فعال‌سازی</MudSelectItem>
<MudSelectItem T="int?" Value="1">غیرفعال‌سازی</MudSelectItem>
<MudSelectItem T="int?" Value="2">ویرایش</MudSelectItem>
<MudSelectItem T="int?" Value="3">اصلاح دستی</MudSelectItem>
</MudSelect>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ReloadMembership">جستجو</MudButton>
</MudStack>
<MudDataGrid @ref="_memGrid" T="ClubMembershipHistoryModel" ServerData="LoadMembershipHistory" ReadOnly="true"
Hover="true" Dense="true" Height="calc(100vh - 400px)">
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<PropertyColumn Property="x => x.UserName" Title="کاربر" />
<TemplateColumn Title="وضعیت">
<CellTemplate>@ReportLabels.FormatActiveChange(context.Item.OldIsActive, context.Item.NewIsActive)</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="مشارکت اولیه">
<CellTemplate>
@(context.Item.OldInitialContribution != null ? context.Item.OldInitialContribution.Value.ToString("N0") : "—")
→ @(context.Item.NewInitialContribution != null ? context.Item.NewInitialContribution.Value.ToString("N0") : "—")
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="عملیات">
<CellTemplate>@ReportLabels.GetClubMembershipActionLabel(context.Item.Action)</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.PerformedBy" Title="انجام‌دهنده" />
<PropertyColumn Property="x => x.Reason" Title="دلیل" />
<TemplateColumn Title="تاریخ">
<CellTemplate>@context.Item.CreatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
</TemplateColumn>
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="ClubMembershipHistoryModel" PageSizeOptions="@(new int[] { 20, 50, 100 })" />
</PagerContent>
</MudDataGrid>
</MudTabPanel>
<MudTabPanel Text="تغییرات دوره" Icon="@Icons.Material.Filled.Timeline">
<MudAlert Severity="Severity.Warning" Variant="Variant.Text" Dense="true" Class="mb-3">
ردیابی تغییرات دوره از زمان deploy فعال شد. رکوردهای قدیمی‌تر ممکن است خالی باشند.
</MudAlert>
<MudStack Row="true" Spacing="2" Class="mb-3">
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_cycleUserId" />
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ReloadCycle">جستجو</MudButton>
</MudStack>
<MudDataGrid @ref="_cycleGrid" T="ClubMembershipCycleHistoryModel" ServerData="LoadCycleHistory" ReadOnly="true"
Hover="true" Dense="true" Height="calc(100vh - 400px)">
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<PropertyColumn Property="x => x.UserName" Title="کاربر" />
<PropertyColumn Property="x => x.CycleNumber" Title="دور" />
<TemplateColumn Title="دوره جاری">
<CellTemplate>@ReportLabels.FormatBoolChange(context.Item.OldIsCurrentCycle, context.Item.NewIsCurrentCycle)</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="عملیات">
<CellTemplate>@ReportLabels.GetClubCycleActionLabel(context.Item.Action)</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.PerformedBy" Title="انجام‌دهنده" />
<PropertyColumn Property="x => x.Reason" Title="دلیل" />
<TemplateColumn Title="تاریخ">
<CellTemplate>@context.Item.CreatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
</TemplateColumn>
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="ClubMembershipCycleHistoryModel" PageSizeOptions="@(new int[] { 20, 50, 100 })" />
</PagerContent>
</MudDataGrid>
</MudTabPanel>
</MudTabs>
@code {
private MudDataGrid<ClubMembershipHistoryModel>? _memGrid;
private MudDataGrid<ClubMembershipCycleHistoryModel>? _cycleGrid;
private long? _memUserId, _cycleUserId;
private int? _memAction;
private async Task<GridData<ClubMembershipHistoryModel>> LoadMembershipHistory(GridState<ClubMembershipHistoryModel> state)
{
try
{
var result = await ClubClient.GetClubMembershipHistoryAsync(new GetClubMembershipHistoryRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize },
Filter = new GetClubMembershipHistoryFilter
{
UserId = _memUserId > 0 ? _memUserId : null,
Action = _memAction
}
});
return new GridData<ClubMembershipHistoryModel>
{
Items = result?.Models?.ToList() ?? new(),
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
return new GridData<ClubMembershipHistoryModel>();
}
}
private async Task<GridData<ClubMembershipCycleHistoryModel>> LoadCycleHistory(GridState<ClubMembershipCycleHistoryModel> state)
{
try
{
var result = await CycleHistoryClient.GetAllClubMembershipCycleHistoryByFilterAsync(new GetAllClubMembershipCycleHistoryByFilterRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize },
Filter = new GetAllClubMembershipCycleHistoryByFilterFilter
{
UserId = _cycleUserId > 0 ? _cycleUserId : null
}
});
return new GridData<ClubMembershipCycleHistoryModel>
{
Items = result?.Models?.ToList() ?? new(),
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
return new GridData<ClubMembershipCycleHistoryModel>();
}
}
private async Task ReloadMembership() { if (_memGrid != null) await _memGrid.ReloadServerData(); }
private async Task ReloadCycle() { if (_cycleGrid != null) await _cycleGrid.ReloadServerData(); }
}
@@ -1,131 +0,0 @@
@using BackOffice.Common.BaseComponents
@using BackOffice.Pages.AutoComplete
@using CMSMicroservice.Protobuf.Protos.UserPackagePurchase
@using Google.Protobuf.WellKnownTypes
@inject UserPackagePurchaseContract.UserPackagePurchaseContractClient PurchaseClient
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
گزارش read-only خریدهای پکیج — فقط مشاهده و فیلتر.
</MudAlert>
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
<Filters>
<MudItem xs="12" sm="6" md="3">
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_userId" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="int?" Clearable="true" Label="روش خرید" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_purchaseMethod">
<MudSelectItem T="int?" Value="1">دایا</MudSelectItem>
<MudSelectItem T="int?" Value="3">دستی</MudSelectItem>
<MudSelectItem T="int?" Value="2">درگاه</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه خرید" />
</MudItem>
</Filters>
<Content>
<MudGrid Class="mb-3" Spacing="2">
<MudItem xs="6" sm="4">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">تعداد</MudText>
<MudText Typo="Typo.h6">@_totalCount.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6" sm="4">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">جمع مبلغ (ریال)</MudText>
<MudText Typo="Typo.h6">@_totalAmount.ToString("N0")</MudText>
</MudPaper>
</MudItem>
</MudGrid>
<MudDataGrid @ref="_grid" T="UserPackagePurchaseModel" ServerData="LoadData" ReadOnly="true"
Hover="true" Dense="true" Height="calc(100vh - 420px)">
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<TemplateColumn Title="کاربر">
<CellTemplate>
<MudStack Spacing="0">
<MudText Typo="Typo.body2">@context.Item.UserName</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">@context.Item.UserMobile</MudText>
</MudStack>
</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.PackageName" Title="پکیج" />
<TemplateColumn Title="روش خرید">
<CellTemplate>@ReportLabels.GetPurchaseMethodLabel(context.Item.PurchaseMethod)</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="مبلغ (ریال)">
<CellTemplate>@context.Item.Amount.ToString("N0")</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="تاریخ خرید">
<CellTemplate>@context.Item.PurchasedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.OrderId" Title="OrderId" />
<PropertyColumn Property="x => x.TransactionId" Title="TransactionId" />
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="UserPackagePurchaseModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد در صفحه" />
</PagerContent>
</MudDataGrid>
</Content>
</BasePageComponent>
@code {
private BasePageComponent? _basePage;
private MudDataGrid<UserPackagePurchaseModel>? _grid;
private long? _userId;
private int? _purchaseMethod;
private DateTime? _fromDate, _toDate;
private long _totalCount, _totalAmount;
private GetAllUserPackagePurchaseByFilterFilter BuildFilter() => new()
{
UserId = _userId > 0 ? _userId : null,
PurchaseMethod = _purchaseMethod,
PurchasedFrom = _fromDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_fromDate.Value, DateTimeKind.Utc)) : null,
PurchasedTo = _toDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_toDate.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc)) : null
};
private async Task<GridData<UserPackagePurchaseModel>> LoadData(GridState<UserPackagePurchaseModel> state)
{
try
{
var filter = BuildFilter();
var summary = await PurchaseClient.GetUserPackagePurchaseSummaryAsync(new GetUserPackagePurchaseSummaryRequest { Filter = filter });
_totalCount = summary.TotalCount;
_totalAmount = summary.TotalAmount;
var result = await PurchaseClient.GetAllUserPackagePurchaseByFilterAsync(new GetAllUserPackagePurchaseByFilterRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize },
Filter = filter
});
return new GridData<UserPackagePurchaseModel>
{
Items = result?.Models?.ToList() ?? new(),
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
return new GridData<UserPackagePurchaseModel>();
}
}
private async Task OnFilterSubmit() { if (_grid != null) await _grid.ReloadServerData(); }
private async Task OnFilterCleared()
{
_userId = null; _purchaseMethod = null; _fromDate = _toDate = null;
if (_grid != null) await _grid.ReloadServerData();
}
}
@@ -1,105 +0,0 @@
@using CMSMicroservice.Protobuf.Protos.UserPackagePurchase
@using Google.Protobuf.WellKnownTypes
@using DateTimeConverterCL
@inject UserPackagePurchaseContract.UserPackagePurchaseContractClient PurchaseClient
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">
جزئیات خرید پکیج — @UserName
</MudText>
</TitleContent>
<DialogContent>
@if (_loading)
{
<MudProgressLinear Indeterminate="true" Color="Color.Primary" Class="my-4" />
}
else if (_items.Count == 0)
{
<MudAlert Severity="Severity.Info" Dense="true">خریدی برای این کاربر یافت نشد.</MudAlert>
}
else
{
<MudTable Items="_items" Dense="true" Hover="true" Breakpoint="Breakpoint.Sm">
<HeaderContent>
<MudTh>شناسه</MudTh>
<MudTh>پکیج</MudTh>
<MudTh>روش</MudTh>
<MudTh>مبلغ (ریال)</MudTh>
<MudTh>تاریخ خرید</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="شناسه">@context.Id</MudTd>
<MudTd DataLabel="پکیج">@context.PackageName</MudTd>
<MudTd DataLabel="روش">
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined"
Color="@GetMethodColor(context.PurchaseMethod)">
@ReportLabels.GetPurchaseMethodLabel(context.PurchaseMethod)
</MudChip>
</MudTd>
<MudTd DataLabel="مبلغ">@context.Amount.ToString("N0")</MudTd>
<MudTd DataLabel="تاریخ">
@(context.PurchasedAt != null
? context.PurchasedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()
: "-")
</MudTd>
</RowTemplate>
</MudTable>
}
</DialogContent>
<DialogActions>
<MudButton OnClick="Close" Variant="Variant.Text">بستن</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!;
[Parameter] public long UserId { get; set; }
[Parameter] public string UserName { get; set; } = string.Empty;
private bool _loading = true;
private List<UserPackagePurchaseModel> _items = new();
protected override async Task OnInitializedAsync()
{
try
{
var result = await PurchaseClient.GetAllUserPackagePurchaseByFilterAsync(
new GetAllUserPackagePurchaseByFilterRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = 1,
PageSize = 500
},
Filter = new GetAllUserPackagePurchaseByFilterFilter
{
UserId = UserId
}
});
_items = result?.Models?
.OrderBy(x => x.PurchasedAt?.ToDateTime() ?? DateTime.MinValue)
.ToList() ?? new();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در بارگذاری جزئیات: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
private static Color GetMethodColor(int method) => method switch
{
1 => Color.Info, // دایا
2 => Color.Primary, // درگاه
3 => Color.Secondary, // دستی
_ => Color.Default
};
private void Close() => MudDialog.Close();
}
@@ -1,282 +0,0 @@
@using System.Text
@using BackOffice.Common.BaseComponents
@using BackOffice.Common.Utilities
@using BackOffice.Pages.AutoComplete
@using BackOffice.Pages.Club.Reports.Components
@using CMSMicroservice.Protobuf.Protos.UserPackagePurchase
@using Google.Protobuf.WellKnownTypes
@using DateTimeConverterCL
@using Microsoft.JSInterop
@inject UserPackagePurchaseContract.UserPackagePurchaseContractClient PurchaseClient
@inject IDialogService DialogService
@inject IJSRuntime JsRuntime
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3">
گزارش تجمیعی مشتریان — اولین/آخرین پکیج، تفکیک دایا / دستی / درگاه. خروجی Excel مطابق فیلتر فعال است.
</MudAlert>
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
<Filters>
<MudItem xs="12" sm="6" md="3">
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_userId" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="int?" Clearable="true" Label="روش خرید" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_purchaseMethod">
<MudSelectItem T="int?" Value="1">دایا</MudSelectItem>
<MudSelectItem T="int?" Value="3">دستی</MudSelectItem>
<MudSelectItem T="int?" Value="2">درگاه</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه خرید" />
</MudItem>
</Filters>
<Content>
<MudStack Row="true" Justify="Justify.FlexEnd" Class="mb-3">
<MudButton Variant="Variant.Outlined" Color="Color.Success"
StartIcon="@Icons.Material.Filled.FileDownload"
OnClick="ExportToExcel" Disabled="_exporting">
خروجی Excel
</MudButton>
</MudStack>
<MudDataGrid @ref="_grid" T="CustomerPackagePurchaseRollupModel" ServerData="LoadData" ReadOnly="true"
Hover="true" Dense="true" Height="calc(100vh - 420px)">
<Columns>
<TemplateColumn Title="مشتری">
<CellTemplate>
<MudStack Spacing="0">
<MudText Typo="Typo.body2">@context.Item.UserName</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">@context.Item.UserMobile</MudText>
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="اولین پکیج">
<CellTemplate>
<MudStack Spacing="0">
<MudText Typo="Typo.body2">@context.Item.FirstPackageName</MudText>
<MudText Typo="Typo.caption">@context.Item.FirstAmount.ToString("N0") ریال</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">
@(context.Item.FirstPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-")
</MudText>
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="آخرین پکیج">
<CellTemplate>
<MudStack Spacing="0">
<MudText Typo="Typo.body2">@context.Item.LastPackageName</MudText>
<MudText Typo="Typo.caption">@context.Item.LastAmount.ToString("N0") ریال</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">
@(context.Item.LastPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-")
</MudText>
<MudChip T="string" Size="Size.Small" Class="mt-1" Variant="Variant.Filled"
Color="@GetMethodColor(context.Item.LastPurchaseMethod)">
@ReportLabels.GetPurchaseMethodLabel(context.Item.LastPurchaseMethod)
</MudChip>
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="تعداد / جمع">
<CellTemplate>
<MudStack Spacing="0">
<MudText>@context.Item.PurchaseCount خرید</MudText>
<MudText Typo="Typo.caption">@context.Item.TotalAmount.ToString("N0") ریال</MudText>
</MudStack>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="دایا">
<CellTemplate>
<MudText Typo="Typo.caption">@context.Item.DayaCount عدد</MudText>
<MudText Typo="Typo.caption">@context.Item.DayaAmount.ToString("N0")</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="دستی">
<CellTemplate>
<MudText Typo="Typo.caption">@context.Item.ManualCount عدد</MudText>
<MudText Typo="Typo.caption">@context.Item.ManualAmount.ToString("N0")</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="درگاه">
<CellTemplate>
<MudText Typo="Typo.caption">@context.Item.GatewayCount عدد</MudText>
<MudText Typo="Typo.caption">@context.Item.GatewayAmount.ToString("N0")</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="عملیات">
<CellTemplate>
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
OnClick="() => OpenDetails(context.Item)">
جزئیات
</MudButton>
</CellTemplate>
</TemplateColumn>
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="CustomerPackagePurchaseRollupModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد در صفحه" />
</PagerContent>
</MudDataGrid>
</Content>
</BasePageComponent>
@code {
private BasePageComponent? _basePage;
private MudDataGrid<CustomerPackagePurchaseRollupModel>? _grid;
private long? _userId;
private int? _purchaseMethod;
private DateTime? _fromDate, _toDate;
private bool _exporting;
private GetCustomerPackagePurchaseRollupFilter BuildFilter()
{
var filter = new GetCustomerPackagePurchaseRollupFilter();
if (_userId is > 0)
filter.UserId = _userId;
if (_purchaseMethod.HasValue)
filter.PurchaseMethod = _purchaseMethod.Value;
if (_fromDate.HasValue)
filter.PurchasedFrom = Timestamp.FromDateTime(DateTime.SpecifyKind(_fromDate.Value.Date, DateTimeKind.Utc));
if (_toDate.HasValue)
filter.PurchasedTo = Timestamp.FromDateTime(DateTime.SpecifyKind(_toDate.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc));
return filter;
}
private async Task<GridData<CustomerPackagePurchaseRollupModel>> LoadData(GridState<CustomerPackagePurchaseRollupModel> state)
{
try
{
var result = await PurchaseClient.GetCustomerPackagePurchaseRollupAsync(
new GetCustomerPackagePurchaseRollupRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = state.Page + 1,
PageSize = state.PageSize
},
Filter = BuildFilter()
});
return new GridData<CustomerPackagePurchaseRollupModel>
{
Items = result?.Models?.ToList() ?? new(),
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
return new GridData<CustomerPackagePurchaseRollupModel>();
}
}
private async Task OnFilterSubmit()
{
if (_grid != null) await _grid.ReloadServerData();
}
private async Task OnFilterCleared()
{
_userId = null;
_purchaseMethod = null;
_fromDate = _toDate = null;
if (_grid != null) await _grid.ReloadServerData();
}
private async Task OpenDetails(CustomerPackagePurchaseRollupModel row)
{
var parameters = new DialogParameters
{
{ nameof(CustomerPackagePurchaseDetailsDialog.UserId), row.UserId },
{ nameof(CustomerPackagePurchaseDetailsDialog.UserName), row.UserName }
};
var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true, CloseButton = true };
await DialogService.ShowAsync<CustomerPackagePurchaseDetailsDialog>("جزئیات خرید پکیج", parameters, options);
}
private async Task ExportToExcel()
{
_exporting = true;
try
{
// PageSize=0 → همهٔ ردیف‌های مطابق فیلتر، مستقل از صفحه‌بندی گرید
var result = await PurchaseClient.GetCustomerPackagePurchaseRollupAsync(
new GetCustomerPackagePurchaseRollupRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = 1,
PageSize = 0
},
Filter = BuildFilter()
});
var all = result?.Models?.ToList() ?? new();
if (all.Count == 0)
{
Snackbar.Add("داده‌ای برای خروجی با فیلتر فعلی وجود ندارد.", Severity.Info);
return;
}
var headers = new[]
{
"شناسه کاربر", "نام مشتری", "موبایل",
"اولین پکیج", "مبلغ اولین", "تاریخ اولین",
"آخرین پکیج", "مبلغ آخرین", "تاریخ آخرین", "روش آخرین",
"تعداد خرید", "جمع مبلغ",
"تعداد دایا", "مبلغ دایا",
"تعداد دستی", "مبلغ دستی",
"تعداد درگاه", "مبلغ درگاه"
};
var rows = all.Select(o => new object?[]
{
o.UserId,
o.UserName,
o.UserMobile,
o.FirstPackageName,
o.FirstAmount,
o.FirstPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-",
o.LastPackageName,
o.LastAmount,
o.LastPurchasedAt?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-",
ReportLabels.GetPurchaseMethodLabel(o.LastPurchaseMethod),
o.PurchaseCount,
o.TotalAmount,
o.DayaCount,
o.DayaAmount,
o.ManualCount,
o.ManualAmount,
o.GatewayCount,
o.GatewayAmount
});
var filename = $"customer-packages-{DateTime.Now:yyyyMMddHHmmss}.xls";
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
CsvExportHelper.ToExcelXmlBase64("مشتریان پکیج", headers, rows));
Snackbar.Add($"خروجی Excel آماده شد ({all.Count} مشتری).", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در خروجی Excel: {ex.Message}", Severity.Error);
}
finally
{
_exporting = false;
}
}
private static Color GetMethodColor(int method) => method switch
{
1 => Color.Info,
2 => Color.Primary,
3 => Color.Secondary,
_ => Color.Default
};
}
@@ -41,11 +41,11 @@
</tr>
<tr>
<td><strong>ارزش هر بالانس:</strong></td>
<td>@Payout.ValuePerBalance.ToString("N0") تومان</td>
<td>@Payout.ValuePerBalance.ToString("N0") ریال</td>
</tr>
<tr>
<td><strong>مبلغ کل:</strong></td>
<td><strong>@Payout.TotalAmount.ToString("N0") تومان</strong></td>
<td><strong>@Payout.TotalAmount.ToString("N0") ریال</strong></td>
</tr>
<tr>
<td><strong>وضعیت:</strong></td>
@@ -2,7 +2,7 @@
<DialogContent>
<MudStack Spacing="3">
<MudAlert Severity="Severity.Warning" Dense="true">
درخواست برداشت @Amount.ToString("N0") تومان برای @UserName رد خواهد شد.
درخواست برداشت @Amount.ToString("N0") ریال برای @UserName رد خواهد شد.
</MudAlert>
<MudTextField @bind-Value="_reason"
Label="دلیل رد درخواست"
@@ -27,7 +27,7 @@
</tr>
<tr>
<td><strong>مبلغ:</strong></td>
<td><MudText Color="Color.Primary"><strong>@Request.Amount.ToString("N0") تومان</strong></MudText></td>
<td><MudText Color="Color.Primary"><strong>@Request.Amount.ToString("N0") ریال</strong></MudText></td>
</tr>
<tr>
<td><strong>روش برداشت:</strong></td>
+184 -252
View File
@@ -5,89 +5,146 @@
@using MudBlazor
@using BackOffice.Pages.AutoComplete
<MudStack Spacing="4">
<div>
<MudText Typo="Typo.h4" Class="mb-4">داشبورد کمیسیون</MudText>
@* ── عنوان صفحه ── *@
<div>
<MudText Typo="Typo.h5" Class="mb-1">داشبورد کمیسیون</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">
مبلغ پورسانت هر هفته بین اعضاء تقسیم می‌شود. هر پکیج استخر مستقل دارد.
</MudText>
</div>
@if (_isLoading)
{
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
}
else
{
<!-- Pool Summary Section -->
<MudGrid>
<MudItem xs="12" sm="6" md="3">
<MudCard Elevation="2">
<MudCardContent>
<MudText Typo="Typo.h6" Color="Color.Primary">استخر هفتگی</MudText>
<MudText Typo="Typo.h4">@(_poolData?.TotalPoolAmount.ToString("N0") ?? "0") ریال</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">@_currentWeekDisplayText</MudText>
</MudCardContent>
</MudCard>
</MudItem>
@* ── فیلتر (اول از همه) ── *@
<MudPaper Elevation="1" Class="pa-4">
<MudGrid AlignItems="AlignItems.End" Spacing="3">
<MudItem xs="12" sm="5">
<MudItem xs="12" sm="6" md="3">
<MudCard Elevation="2">
<MudCardContent>
<MudText Typo="Typo.h6" Color="Color.Success">تعداد تعادلها</MudText>
<MudText Typo="Typo.h4">@(_poolData?.TotalBalances.ToString("N0") ?? "0")</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">مجموع تعادل های فعال</MudText>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudCard Elevation="2">
<MudCardContent>
<MudText Typo="Typo.h6" Color="Color.Warning">ارزش هر تعادل</MudText>
<MudText Typo="Typo.h4">@(_poolData?.ValuePerBalance.ToString("N0") ?? "0") ریال</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">قیمت واحد تعادل</MudText>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudCard Elevation="2">
<MudCardContent>
<MudText Typo="Typo.h6" Color="@(_poolData?.IsCalculated == true ? Color.Success : Color.Error)">
وضعیت محاسبه
</MudText>
<MudText Typo="Typo.h4">
@(_poolData?.IsCalculated == true ? "محاسبه شده ✓" : "محاسبه نشده ✗")
</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">
@if (_poolData?.CalculatedAt != null)
{
var calculatedDate = _poolData.CalculatedAt.ToDateTime().ToLocalTime();
var persianDate = PersianDateTime.ConvertToPersianDateTime(calculatedDate);
@($"در تاریخ {persianDate}")
}
else
{
@("در انتظار محاسبه")
}
</MudText>
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
<!-- Week Selector -->
<MudPaper Class="pa-4 mt-4" Elevation="2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
<MudText Typo="Typo.h6">انتخاب هفته:</MudText>
@if (_allWeeks.Any())
{
<MudSelect T="long"
Value="_currentWeekDefinitionId"
Label="هفته"
Variant="Variant.Outlined"
Margin="Margin.Dense"
AnchorOrigin="Origin.BottomLeft"
TransformOrigin="Origin.TopLeft"
AdornmentIcon="@Icons.Material.Filled.CalendarToday"
Adornment="Adornment.Start"
ValueChanged="OnWeekChanged">
<MudSelect T="long"
Value="_currentWeekDefinitionId"
Label="شماره هفته"
Variant="Variant.Outlined"
Margin="Margin.Dense"
AnchorOrigin="Origin.BottomCenter"
Style="min-width: 400px;"
ValueChanged="OnWeekChanged">
@if (_availableWeeks?.CurrentWeek != null)
{
<MudSelectItem T="long" Value="@_availableWeeks.CurrentWeek.WeekDefinitionId">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.Today" Size="Size.Small" Color="Color.Primary" />
<span>@_availableWeeks.CurrentWeek.DisplayText</span>
<MudChip T="string" Size="Size.Small" Color="Color.Primary" Variant="Variant.Text">جاری</MudChip>
</MudStack>
<MudText>
<MudIcon Icon="@Icons.Material.Filled.Today" Size="Size.Small" Class="ml-2" />
@_availableWeeks.CurrentWeek.DisplayText
</MudText>
</MudSelectItem>
<MudDivider />
}
@if (_availableWeeks?.CalculatedWeeks?.Any() == true)
{
<MudListSubheader>تسویه‌شده</MudListSubheader>
<MudListSubheader>هفته‌های محاسبه شده</MudListSubheader>
@foreach (var week in _availableWeeks.CalculatedWeeks)
{
<MudSelectItem T="long" Value="@week.WeekDefinitionId">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Size="Size.Small" Color="Color.Success" />
<span>@week.DisplayText</span>
<MudText>
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" Class="ml-2" />
@week.DisplayText
@if (week.TotalPoolAmount > 0)
{
<MudText Typo="Typo.caption" Color="Color.Secondary">(@week.TotalPoolAmount.ToString("N0") T)</MudText>
<MudChip T="string" Size="Size.Small" Color="Color.Success" Class="mr-2">
@week.TotalPoolAmount.ToString("N0") ریال
</MudChip>
}
</MudStack>
</MudText>
</MudSelectItem>
}
<MudDivider />
}
@if (_availableWeeks?.PendingWeeks?.Any() == true)
{
<MudListSubheader>در انتظار تسویه</MudListSubheader>
<MudListSubheader>هفته‌های محاسبه نشده</MudListSubheader>
@foreach (var week in _availableWeeks.PendingWeeks)
{
<MudSelectItem T="long" Value="@week.WeekDefinitionId">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.AccessTime" Size="Size.Small" Color="Color.Warning" />
<span>@week.DisplayText</span>
</MudStack>
<MudText>
<MudIcon Icon="@Icons.Material.Filled.AccessTime" Color="Color.Warning" Size="Size.Small" Class="ml-2" />
@week.DisplayText
</MudText>
</MudSelectItem>
}
<MudDivider />
}
@if (_availableWeeks?.FutureWeeks?.Any() == true)
{
<MudListSubheader>هفته‌های آینده</MudListSubheader>
@foreach (var week in _availableWeeks.FutureWeeks)
{
<MudSelectItem T="long" Value="@week.WeekDefinitionId">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.CalendarMonth" Size="Size.Small" Color="Color.Tertiary" />
<span>@week.DisplayText</span>
</MudStack>
<MudText>
<MudIcon Icon="@Icons.Material.Filled.CalendarMonth" Color="Color.Info" Size="Size.Small" Class="ml-2" />
@week.DisplayText
</MudText>
</MudSelectItem>
}
}
@@ -97,222 +154,97 @@
{
<MudText Color="Color.Warning">لیست هفته‌ها بارگذاری نشده</MudText>
}
</MudItem>
<MudItem xs="12" sm="5">
<PackageSelect Label="پکیج"
SelectedPackageId="_selectedPackageId"
SelectedPackageIdChanged="OnPackageChanged" />
</MudItem>
<MudItem xs="12" sm="2">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
FullWidth="true"
OnClick="LoadPoolData"
StartIcon="@Icons.Material.Filled.Search"
Disabled="_isLoading">
نمایش
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="LoadPoolData"
StartIcon="@Icons.Material.Filled.Refresh">
بارگذاری
</MudButton>
</MudItem>
</MudGrid>
</MudPaper>
@* ── لودینگ ── *@
@if (_isLoading)
{
<MudPaper Elevation="0" Class="pa-6 d-flex justify-center align-center">
<MudStack AlignItems="AlignItems.Center" Spacing="2">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Medium" />
<MudText Typo="Typo.body2" Color="Color.Secondary">در حال بارگذاری اطلاعات استخر…</MudText>
</MudStack>
</MudPaper>
}
else
{
@* ── نوار وضعیت هفته / پکیج ── *@
<MudPaper Elevation="0" Class="pa-3 mud-background-gray rounded">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Wrap="Wrap.Wrap">
<MudIcon Icon="@Icons.Material.Filled.DateRange" Color="Color.Default" Size="Size.Small" />
<MudText Typo="Typo.body1">@_currentWeekDisplayText</MudText>
@if (!string.IsNullOrEmpty(_poolData?.PackageTitle))
{
<MudDivider Vertical="true" FlexItem="true" />
<MudChip T="string" Size="Size.Small" Color="Color.Primary" Icon="@Icons.Material.Filled.WorkspacePremium">
@_poolData.PackageTitle
</MudChip>
}
<MudSpacer />
@if (_poolData != null && _poolData.Id > 0)
{
@if (_poolData.IsCalculated)
{
<MudChip T="string" Color="Color.Success" Size="Size.Small"
Icon="@Icons.Material.Filled.CheckCircle" Variant="Variant.Filled">
تسویه انجام شده
</MudChip>
}
else
{
<MudChip T="string" Color="Color.Warning" Size="Size.Small"
Icon="@Icons.Material.Filled.Pending" Variant="Variant.Filled">
در انتظار تسویه
</MudChip>
}
}
</MudStack>
</MudPaper>
@* ── ۳ کارت اصلی ── *@
<MudGrid Spacing="3">
@* کارت ۱: کل مبلغ استخر *@
<MudItem xs="12" sm="4">
<MudPaper Elevation="2" Class="pa-4" Style="border-top: 4px solid var(--mud-palette-primary);">
<MudStack Spacing="1">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.AccountBalanceWallet" Color="Color.Primary" Size="Size.Small" />
<MudText Typo="Typo.caption" Color="Color.Secondary">کل مبلغ استخر پورسانت</MudText>
</MudStack>
<MudText Typo="Typo.h5" Style="font-weight:700; direction:ltr; text-align:right;">
@((_poolData?.TotalPoolAmount ?? 0).ToString("N0"))
</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">تومان — جمع کارمزد فعال‌سازی‌های این هفته</MudText>
</MudStack>
</MudPaper>
</MudItem>
@* کارت ۲: تعداد واحدهای پورسانت *@
<MudItem xs="12" sm="4">
<MudPaper Elevation="2" Class="pa-4" Style="border-top: 4px solid var(--mud-palette-success);">
<MudStack Spacing="1">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.People" Color="Color.Success" Size="Size.Small" />
<MudText Typo="Typo.caption" Color="Color.Secondary">واحدهای پورسانت (بالانس)</MudText>
<MudTooltip Text="تعداد واحدهایی که پورسانت به آن‌ها تعلق می‌گیرد. هر عضو بر اساس شبکه‌اش چند واحد دریافت می‌کند.">
<MudIcon Icon="@Icons.Material.Filled.HelpOutline" Size="Size.Small" Color="Color.Tertiary" />
</MudTooltip>
</MudStack>
<MudText Typo="Typo.h5" Style="font-weight:700; direction:ltr; text-align:right;">
@((_poolData?.TotalBalances ?? 0).ToString("N0"))
</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">واحد — مجموع همه اعضاء فعال</MudText>
</MudStack>
</MudPaper>
</MudItem>
@* کارت ۳: سهم هر واحد *@
<MudItem xs="12" sm="4">
<MudPaper Elevation="2" Class="pa-4" Style="border-top: 4px solid var(--mud-palette-warning);">
<MudStack Spacing="1">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.LocalAtm" Color="Color.Warning" Size="Size.Small" />
<MudText Typo="Typo.caption" Color="Color.Secondary">سهم پورسانت هر واحد</MudText>
</MudStack>
<MudText Typo="Typo.h5" Style="font-weight:700; direction:ltr; text-align:right;">
@((_poolData?.ValuePerBalance ?? 0).ToString("N0"))
</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">
تومان — @((_poolData?.IsCalculated == true) ? "قطعی شده" : "قطعی نشده")
</MudText>
</MudStack>
</MudPaper>
</MudItem>
</MudGrid>
@* ── اعتبارسنجی مبلغ استخر ── *@
@if (_poolData != null && _poolData.Id > 0)
<!-- Pool Details -->
@if (_poolData != null)
{
@if (!_poolData.ValidationIsValid)
{
<MudAlert Severity="Severity.Error" Variant="Variant.Filled" Icon="@Icons.Material.Filled.ReportProblem" NoIcon="false">
<MudText Typo="Typo.subtitle2" Class="mb-2">مبلغ استخر با تعداد فعال‌سازی‌های دیتابیس مطابقت ندارد</MudText>
<MudGrid Spacing="2">
<MudItem xs="12" sm="4">
<MudText Typo="Typo.caption">مبلغ ثبت‌شده</MudText>
<MudText Typo="Typo.body2"><strong>@_poolData.TotalPoolAmount.ToString("N0") تومان</strong></MudText>
</MudItem>
<MudItem xs="12" sm="4">
<MudText Typo="Typo.caption">مبلغ انتظاری (@_poolData.ValidationActivationCount فعال‌سازی)</MudText>
<MudText Typo="Typo.body2"><strong>@_poolData.ValidationExpectedAmount.ToString("N0") تومان</strong></MudText>
</MudItem>
<MudItem xs="12" sm="4">
<MudText Typo="Typo.caption">اختلاف</MudText>
<MudText Typo="Typo.body2" Style="color:var(--mud-palette-warning-lighten);">
<strong>@_poolData.ValidationDiscrepancy.ToString("N0") تومان</strong>
</MudText>
</MudItem>
</MudGrid>
</MudAlert>
}
else if (_poolData.ValidationActivationCount > 0)
{
<MudAlert Severity="Severity.Success" Variant="Variant.Text" Dense="true" Icon="@Icons.Material.Filled.VerifiedUser">
مبلغ استخر صحیح است — با @_poolData.ValidationActivationCount فعال‌سازی در این هفته مطابقت دارد.
</MudAlert>
}
<MudPaper Class="pa-4 mt-4" Elevation="2">
<MudText Typo="Typo.h6" Class="mb-3">جزئیات استخر</MudText>
<MudSimpleTable Hover="true" Dense="true">
<tbody>
<tr>
<td><strong>شناسه استخر:</strong></td>
<td>@_poolData.Id</td>
</tr>
<tr>
<td><strong>شماره هفته:</strong></td>
<td>@_currentWeekDisplayName</td>
</tr>
<tr>
<td><strong>مجموع استخر:</strong></td>
<td>@_poolData.TotalPoolAmount.ToString("N0") ریال</td>
</tr>
<tr>
<td><strong>تعداد تعادل:</strong></td>
<td>@_poolData.TotalBalances</td>
</tr>
<tr>
<td><strong>ارزش هر تعادل:</strong></td>
<td>@_poolData.ValuePerBalance.ToString("N0") ریال</td>
</tr>
<tr>
<td><strong>وضعیت:</strong></td>
<td>
<MudChip T="string" Color="@(_poolData.IsCalculated ? Color.Success : Color.Error)" Size="Size.Small">
@(_poolData.IsCalculated ? "محاسبه شده" : "محاسبه نشده")
</MudChip>
</td>
</tr>
<tr>
<td><strong>تاریخ محاسبه:</strong></td>
<td>
@if (_poolData.CalculatedAt != null)
{
@_poolData.CalculatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()
}
else
{
<MudText Color="Color.Secondary">-</MudText>
}
</td>
</tr>
<tr>
<td><strong>تاریخ ایجاد:</strong></td>
<td>@_poolData.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</td>
</tr>
</tbody>
</MudSimpleTable>
</MudPaper>
}
@* ── جزئیات تکمیلی (بدون تکرار اطلاعات کارت‌ها) ── *@
@if (_poolData != null && _poolData.Id > 0)
{
<MudExpansionPanels Elevation="1">
<MudExpansionPanel Text="جزئیات بیشتر" Icon="@Icons.Material.Filled.ExpandMore">
<MudSimpleTable Dense="true" Hover="true" Style="max-width:540px;">
<tbody>
<tr>
<td style="width:200px;"><MudText Typo="Typo.body2" Color="Color.Secondary">شناسه استخر</MudText></td>
<td><MudText Typo="Typo.body2">#@_poolData.Id</MudText></td>
</tr>
<tr>
<td><MudText Typo="Typo.body2" Color="Color.Secondary">تاریخ ایجاد استخر</MudText></td>
<td><MudText Typo="Typo.body2">@_poolData.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</MudText></td>
</tr>
@if (_poolData.CalculatedAt != null)
{
<tr>
<td><MudText Typo="Typo.body2" Color="Color.Secondary">تاریخ تسویه</MudText></td>
<td><MudText Typo="Typo.body2">@_poolData.CalculatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</MudText></td>
</tr>
}
</tbody>
</MudSimpleTable>
</MudExpansionPanel>
</MudExpansionPanels>
}
@* ── دکمه‌های عملیاتی ── *@
<MudPaper Elevation="0" Class="pa-3 mud-background-gray rounded">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Wrap="Wrap.Wrap">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Payments"
Href="/commission/payouts"
Size="Size.Medium">
پرداخت‌های این هفته
<!-- Quick Actions -->
<MudPaper Class="pa-4 mt-4" Elevation="2">
<MudText Typo="Typo.h6" Class="mb-3">عملیات سریع</MudText>
<MudStack Row="true" Spacing="2">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Payments"
Href="/commission/payouts">
مشاهده پرداخت‌ها
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Default"
StartIcon="@Icons.Material.Filled.AccountBalance"
Href="/commission/withdrawals"
Size="Size.Medium">
<MudButton Variant="Variant.Filled"
Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.AccountBalance"
Href="/commission/withdrawals">
درخواست‌های برداشت
</MudButton>
<MudSpacer />
@if (_poolData?.IsCalculated != true)
{
<MudButton Variant="Variant.Filled"
Color="Color.Warning"
StartIcon="@Icons.Material.Filled.Calculate"
OnClick="TriggerCalculation"
Size="Size.Medium">
اجرای تسویه دستی
</MudButton>
}
<MudButton Variant="Variant.Outlined"
Color="Color.Info"
StartIcon="@Icons.Material.Filled.Calculate"
Disabled="@(_poolData?.IsCalculated == true)"
OnClick="TriggerCalculation">
اجرای محاسبه دستی
</MudButton>
</MudStack>
</MudPaper>
}
</MudStack>
</div>
@@ -22,9 +22,6 @@ public partial class Dashboard
private string _currentWeekDisplayText = string.Empty; // نمایش فارسی
private GetWeeklyCommissionPoolResponse? _poolData;
// Package filter
private long? _selectedPackageId;
// Available weeks data
private CMSMicroservice.Protobuf.Protos.Commission.GetAvailableWeeksResponse? _availableWeeks;
private List<CMSMicroservice.Protobuf.Protos.Commission.WeekInfo> _allWeeks = new();
@@ -94,12 +91,6 @@ public partial class Dashboard
await LoadPoolData();
}
private async Task OnPackageChanged(long? packageId)
{
_selectedPackageId = packageId;
await LoadPoolData();
}
private async Task LoadPoolData()
{
if (_currentWeekDefinitionId <= 0)
@@ -115,8 +106,7 @@ public partial class Dashboard
var request = new GetWeeklyCommissionPoolRequest
{
WeekDefinitionId = _currentWeekDefinitionId,
PackageId = _selectedPackageId
WeekDefinitionId = _currentWeekDefinitionId
};
_poolData = await CommissionContract.GetWeeklyCommissionPoolAsync(request);
@@ -136,41 +126,24 @@ public partial class Dashboard
private async Task TriggerCalculation()
{
var packageInfo = _selectedPackageId.HasValue
? $" (فقط پکیج #{_selectedPackageId})"
: " (همه پکیج‌ها)";
var confirmed = await DialogService.ShowMessageBox(
"تایید",
$"آیا از اجرای محاسبه دستی برای هفته {_currentWeekDisplayName}{packageInfo} مطمئن هستید؟",
$"آیا از اجرای محاسبه دستی برای هفته {_currentWeekDisplayName} مطمئن هستید؟",
yesText: "بله", cancelText: "خیر");
if (confirmed == true)
{
try
{
var req = new TriggerWeeklyCalculationRequest()
await CommissionContract.TriggerWeeklyCalculationAsync(new TriggerWeeklyCalculationRequest()
{
WeekDefinitionId = _currentWeekDefinitionId,
ForceRecalculate = true,
SkipBalances = false,
SkipPayouts = false,
SkipPool = false,
};
req.PackageId = _selectedPackageId;
var response = await CommissionContract.TriggerWeeklyCalculationAsync(req);
if (response.Success)
{
Snackbar.Add("محاسبه با موفقیت انجام شد.", Severity.Success);
}
else
{
Snackbar.Add($"خطا در محاسبه: {response.Message}", Severity.Error);
return;
}
});
Snackbar.Add("محاسبه با موفقیت آغاز شد. این عملیات ممکن است چند دقیقه طول بکشد.", Severity.Info);
// Reload data after a delay
await Task.Delay(2000);
@@ -19,5 +19,4 @@ public class WithdrawalRequestModel
public string? BankTrackingCode { get; set; }
public string? PaymentFailureReason { get; set; }
public string? IbanNumber { get; set; }
public string? PackageName { get; set; }
}
@@ -9,11 +9,6 @@
<div>
<MudText Typo="Typo.h4" Class="mb-4"> پرداخت های کاربران</MudText>
@* ── G11b: راهنمای پرداخت‌ها (Q28) ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Info">
پاداش‌ها بر اساس هر پکیج جداگانه محاسبه می‌شود. ستون «پکیج» نشان‌دهنده پکیج منبع پاداش است.
</MudAlert>
<MudDataGrid T="UserCommissionPayoutModel"
ServerData="@(new Func<GridState<UserCommissionPayoutModel>, Task<GridData<UserCommissionPayoutModel>>>(ServerReload))"
Hover="true"
@@ -41,29 +36,12 @@
<MudSelectItem T="int?" Value="@((int?)4)">شکست خورده</MudSelectItem>
<MudSelectItem T="int?" Value="@((int?)5)">لغو شده</MudSelectItem>
</MudSelect>
<PackageSelect Label="پکیج"
@bind-SelectedPackageId="_filterPackageId" />
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="ApplyFilter"
StartIcon="@Icons.Material.Filled.Search">
جستجو
</MudButton>
<MudButton Variant="Variant.Filled"
Color="Color.Success"
StartIcon="@Icons.Material.Filled.AccountBalanceWallet"
Disabled="@(!_filterWeekDefinitionId.HasValue || _isCreditingWallets)"
OnClick="BulkCreditWallets">
@if (_isCreditingWallets)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="ms-n1 me-2" />
<span>در حال واریز...</span>
}
else
{
<span>واریز به کیف پول</span>
}
</MudButton>
</MudStack>
</ToolBarContent>
<Columns>
@@ -86,14 +64,6 @@
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.PackageTitle" Title="پکیج">
<CellTemplate>
<MudChip T="string" Color="Color.Tertiary" Size="Size.Small">
@(string.IsNullOrEmpty(context.Item.PackageTitle) ? "-" : context.Item.PackageTitle)
</MudChip>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.BalancesEarned" Title="بالانس‌ها">
<CellTemplate>
<MudChip T="string" Color="Color.Info" Size="Size.Small">
@@ -17,8 +17,6 @@ public partial class UserPayouts
private long? _filterUserId;
private long? _filterWeekDefinitionId;
private int? _filterStatus;
private long? _filterPackageId;
private bool _isCreditingWallets;
private async Task<GridData<UserCommissionPayoutModel>> ServerReload(GridState<UserCommissionPayoutModel> state)
{
@@ -46,11 +44,6 @@ public partial class UserPayouts
request.Status = _filterStatus.Value;
}
if (_filterPackageId.HasValue && _filterPackageId.Value > 0)
{
request.PackageId = _filterPackageId.Value;
}
var result = await CommissionContract.GetUserCommissionPayoutsAsync(request);
if (result?.Models != null && result.Models.Any())
@@ -126,7 +119,7 @@ public partial class UserPayouts
{
var confirmed = await DialogService.ShowMessageBox(
"تایید برداشت",
$"آیا از تایید برداشت برای کاربر {payout.UserName} (مبلغ: {payout.TotalAmount:N0} تومان) مطمئن هستید؟",
$"آیا از تایید برداشت برای کاربر {payout.UserName} (مبلغ: {payout.TotalAmount:N0} ریال) مطمئن هستید؟",
yesText: "تایید", cancelText: "لغو");
if (confirmed == true)
@@ -148,50 +141,6 @@ public partial class UserPayouts
}
}
private async Task BulkCreditWallets()
{
if (!_filterWeekDefinitionId.HasValue)
{
Snackbar.Add("لطفاً ابتدا یک هفته انتخاب کنید", Severity.Warning);
return;
}
var confirmed = await DialogService.ShowMessageBox(
"واریز دسته‌جمعی به کیف پول",
$"آیا از واریز کمیسیون همه پرداخت‌های «در انتظار» هفته انتخاب‌شده به کیف پول کاربران مطمئن هستید؟ این عملیات برگشت‌پذیر نیست.",
yesText: "بله، واریز کن", cancelText: "لغو");
if (confirmed != true)
return;
_isCreditingWallets = true;
StateHasChanged();
try
{
var result = await CommissionContract.BulkCreditPayoutsAsync(new BulkCreditPayoutsRequest
{
WeekDefinitionId = _filterWeekDefinitionId.Value
});
if (result.CreditedCount == 0)
Snackbar.Add("هیچ پرداخت در انتظاری برای این هفته یافت نشد", Severity.Info);
else
Snackbar.Add(result.Message, Severity.Success);
await ApplyFilter();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در واریز: {ex.Message}", Severity.Error);
}
finally
{
_isCreditingWallets = false;
StateHasChanged();
}
}
private async Task RejectWithdrawal(UserCommissionPayoutModel payout)
{
var reason = await DialogService.ShowMessageBox(
@@ -67,13 +67,13 @@
<PropertyColumn Property="x => x.WeekDisplayName" Title="هفته" />
<PropertyColumn Property="x => x.TotalPoolAmount" Title="مبلغ کل استخر" Format="N0">
<CellTemplate>
<MudText>@context.Item.TotalPoolAmount.ToString("N0") تومان</MudText>
<MudText>@context.Item.TotalPoolAmount.ToString("N0") ریال</MudText>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.TotalBalances" Title="مجموع موجودی‌ها" />
<PropertyColumn Property="x => x.ValuePerBalance" Title="ارزش هر موجودی" Format="N0">
<CellTemplate>
<MudText>@context.Item.ValuePerBalance.ToString("N0") تومان</MudText>
<MudText>@context.Item.ValuePerBalance.ToString("N0") ریال</MudText>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.IsCalculated" Title="وضعیت">
@@ -128,7 +128,7 @@
<MudPaper Elevation="0" Class="pa-4 text-center">
<MudText Typo="Typo.h6" Color="Color.Primary">مجموع استخرها</MudText>
<MudText Typo="Typo.h4">@_totalPoolSum.ToString("N0")</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">تومان</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">ریال</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" md="3">
@@ -149,7 +149,7 @@
<MudPaper Elevation="0" Class="pa-4 text-center">
<MudText Typo="Typo.h6" Color="Color.Info">میانگین ارزش</MudText>
<MudText Typo="Typo.h4">@_averageValue.ToString("N0")</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">تومان</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">ریال</MudText>
</MudPaper>
</MudItem>
</MudGrid>
@@ -55,7 +55,7 @@
Margin="Margin.Dense">
<MudSelectItem T="int?" Value="@(null as int?)">همه وضعیت‌ها</MudSelectItem>
<MudSelectItem T="int?" Value="0">در انتظار واریز به کیف پول</MudSelectItem>
<MudSelectItem T="int?" Value="1">واریز شده به پاداش های دریافتی</MudSelectItem>
<MudSelectItem T="int?" Value="1">واریز شده به کیف پول طلایی</MudSelectItem>
<MudSelectItem T="int?" Value="2">درخواست برداشت ثبت شده</MudSelectItem>
<MudSelectItem T="int?" Value="3">برداشت انجام شده</MudSelectItem>
<MudSelectItem T="int?" Value="4">خطا در پرداخت بانکی</MudSelectItem>
@@ -107,19 +107,19 @@
<MudItem xs="12" md="3">
<MudPaper Elevation="1" Class="pa-4 text-center">
<MudText Typo="Typo.subtitle2" Color="Color.Primary">مجموع مبلغ برداشت‌ها</MudText>
<MudText Typo="Typo.h5">@_summary.TotalAmount.ToString("N0") تومان</MudText>
<MudText Typo="Typo.h5">@_summary.TotalAmount.ToString("N0") ریال</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" md="3">
<MudPaper Elevation="1" Class="pa-4 text-center">
<MudText Typo="Typo.subtitle2" Color="Color.Success">مبلغ پرداخت شده</MudText>
<MudText Typo="Typo.h5">@_summary.TotalPaid.ToString("N0") تومان</MudText>
<MudText Typo="Typo.h5">@_summary.TotalPaid.ToString("N0") ریال</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" md="3">
<MudPaper Elevation="1" Class="pa-4 text-center">
<MudText Typo="Typo.subtitle2" Color="Color.Warning">مبلغ در انتظار</MudText>
<MudText Typo="Typo.h5">@_summary.TotalPending.ToString("N0") تومان</MudText>
<MudText Typo="Typo.h5">@_summary.TotalPending.ToString("N0") ریال</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" md="3">
@@ -189,17 +189,17 @@
<PropertyColumn Property="x => x.PendingCount" Title="در انتظار" />
<PropertyColumn Property="x => x.TotalAmount" Title="مبلغ کل" Format="N0">
<CellTemplate>
@context.Item.TotalAmount.ToString("N0") تومان
@context.Item.TotalAmount.ToString("N0") ریال
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.PaidAmount" Title="پرداخت شده" Format="N0">
<CellTemplate>
@context.Item.PaidAmount.ToString("N0") تومان
@context.Item.PaidAmount.ToString("N0") ریال
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.PendingAmount" Title="در انتظار پرداخت" Format="N0">
<CellTemplate>
@context.Item.PendingAmount.ToString("N0") تومان
@context.Item.PendingAmount.ToString("N0") ریال
</CellTemplate>
</PropertyColumn>
</Columns>
@@ -431,9 +431,9 @@
sb.AppendLine("خلاصه:");
sb.AppendLine($"تعداد کل درخواست‌ها: {_summary.TotalRequests}");
sb.AppendLine($"مبلغ کل برداشت‌ها: {_summary.TotalAmount:N0} تومان");
sb.AppendLine($"مبلغ پرداخت شده: {_summary.TotalPaid:N0} تومان");
sb.AppendLine($"مبلغ در انتظار: {_summary.TotalPending:N0} تومان");
sb.AppendLine($"مبلغ کل برداشت‌ها: {_summary.TotalAmount:N0} ریال");
sb.AppendLine($"مبلغ پرداخت شده: {_summary.TotalPaid:N0} ریال");
sb.AppendLine($"مبلغ در انتظار: {_summary.TotalPending:N0} ریال");
sb.AppendLine($"تعداد کاربران یکتا: {_summary.UniqueUsers}");
sb.AppendLine($"نرخ موفقیت: {_summary.SuccessRate:N2}%");
sb.AppendLine();
@@ -69,7 +69,7 @@
<PropertyColumn Property="x => x.Amount" Title="مبلغ">
<CellTemplate>
<MudText Typo="Typo.body2" Color="Color.Primary">
<strong>@context.Item.Amount.ToString("N0") تومان</strong>
<strong>@context.Item.Amount.ToString("N0") ریال</strong>
</MudText>
</CellTemplate>
</PropertyColumn>
@@ -87,8 +87,7 @@ public partial class WithdrawalRequests
BankReferenceId = grpcModel.BankReferenceId,
BankTrackingCode = grpcModel.BankTrackingCode,
PaymentFailureReason = grpcModel.PaymentFailureReason,
IbanNumber = grpcModel.IbanNumber,
PackageName = grpcModel.PackageName
IbanNumber = grpcModel.IbanNumber
};
}
@@ -164,7 +163,7 @@ public partial class WithdrawalRequests
{
var confirmed = await DialogService.ShowMessageBox(
"تایید درخواست",
$"آیا از تایید درخواست برداشت {request.Amount:N0} تومان برای {request.UserName} مطمئن هستید؟",
$"آیا از تایید درخواست برداشت {request.Amount:N0} ریال برای {request.UserName} مطمئن هستید؟",
yesText: "تایید", cancelText: "لغو");
if (confirmed == true)
@@ -223,7 +222,7 @@ public partial class WithdrawalRequests
{
var confirmed = await DialogService.ShowMessageBox(
"پردازش پرداخت",
$"آیا پرداخت {request.Amount:N0} تومان به {request.UserName} انجام شده است؟",
$"آیا پرداخت {request.Amount:N0} ریال به {request.UserName} انجام شده است؟",
yesText: "بله، پردازش شد", cancelText: "لغو");
if (confirmed == true)
@@ -270,7 +269,7 @@ public partial class WithdrawalRequests
}
var sb = new StringBuilder();
sb.AppendLine("شناسه,کاربر,نام کاربر,پکیج,مبلغ,وضعیت,روش,شماره شبا,تاریخ درخواست,تاریخ پردازش,شماره مرجع بانکی");
sb.AppendLine("شناسه,کاربر,نام کاربر,مبلغ,وضعیت,روش,شماره شبا,تاریخ درخواست,تاریخ پردازش,شماره مرجع بانکی");
foreach (var r in result.Models)
{
@@ -279,7 +278,6 @@ public partial class WithdrawalRequests
mapped.Id,
mapped.UserId,
EscapeCsv(mapped.UserName),
EscapeCsv(mapped.PackageName ?? "-"),
mapped.Amount,
EscapeCsv(GetStatusText(mapped.Status)),
EscapeCsv(GetMethodText(mapped.Method)),
@@ -1,66 +0,0 @@
@using BackOffice.Pages.Content.Components.SettingsModels
<MudExpansionPanels MultiExpansion="true" Elevation="0">
@* ── Vision ── *@
<MudExpansionPanel Text="چشم‌انداز" Icon="@Icons.Material.Filled.Visibility" IsInitiallyExpanded="true">
<MudStack Spacing="2" Class="pa-2">
<MudTextField @bind-Value="Model.VisionTitle"
Label="عنوان"
Placeholder="چشم‌انداز"
Variant="Variant.Outlined" />
<MudTextField @bind-Value="Model.VisionText"
Label="متن چشم‌انداز"
Variant="Variant.Outlined"
Lines="3" />
<MudTextField @bind-Value="Model.VisionIcon"
Label="نام آیکون"
Placeholder="Visibility"
Variant="Variant.Outlined"
HelperText="نام آیکون از مجموعه Material Icons" />
</MudStack>
</MudExpansionPanel>
@* ── Mission ── *@
<MudExpansionPanel Text="مأموریت" Icon="@Icons.Material.Filled.TrackChanges" IsInitiallyExpanded="true">
<MudStack Spacing="2" Class="pa-2">
<MudTextField @bind-Value="Model.MissionTitle"
Label="عنوان"
Placeholder="مأموریت"
Variant="Variant.Outlined" />
<MudTextField @bind-Value="Model.MissionText"
Label="متن مأموریت"
Variant="Variant.Outlined"
Lines="3" />
<MudTextField @bind-Value="Model.MissionIcon"
Label="نام آیکون"
Placeholder="TrackChanges"
Variant="Variant.Outlined"
HelperText="نام آیکون از مجموعه Material Icons" />
</MudStack>
</MudExpansionPanel>
@* ── Section Titles ── *@
<MudExpansionPanel Text="عناوین بخش‌ها" Icon="@Icons.Material.Filled.Title">
<MudStack Spacing="2" Class="pa-2">
<MudTextField @bind-Value="Model.ValuesTitle"
Label="عنوان بخش ارزش‌ها"
Placeholder="ارزش‌های ما"
Variant="Variant.Outlined" />
<MudTextField @bind-Value="Model.TeamTitle"
Label="عنوان بخش تیم"
Placeholder="تیم ما"
Variant="Variant.Outlined" />
</MudStack>
</MudExpansionPanel>
@* ── Hint ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mt-2">
آیتم‌های ارزش‌ها و اعضای تیم از بخش «مدیریت تصاویر» قابل ویرایش هستند.
</MudAlert>
</MudExpansionPanels>
@code {
[Parameter] public AboutSettingsModel Model { get; set; } = new();
}
@@ -1,68 +0,0 @@
@using BackOffice.Pages.Content.Components.SettingsModels
<MudExpansionPanels MultiExpansion="true" Elevation="0">
@* ── Contact Info ── *@
<MudExpansionPanel Text="اطلاعات تماس" Icon="@Icons.Material.Filled.ContactPhone" IsInitiallyExpanded="true">
<MudStack Spacing="2" Class="pa-2">
<MudTextField @bind-Value="Model.Address"
Label="آدرس"
Variant="Variant.Outlined"
Lines="2"
Placeholder="کرج مهرویلا میدان مادر..." />
<MudGrid Spacing="2">
<MudItem xs="12" md="6">
<MudTextField @bind-Value="Model.Phone"
Label="شماره تلفن"
Placeholder="026-34233563"
Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="12" md="6">
<MudTextField @bind-Value="Model.Email"
Label="ایمیل"
Placeholder="info@kbs1.co"
Variant="Variant.Outlined" />
</MudItem>
</MudGrid>
<MudTextField @bind-Value="Model.WorkingHours"
Label="ساعات کاری"
Placeholder="شنبه تا پنج‌شنبه — ۹ صبح تا ۶ عصر"
Variant="Variant.Outlined" />
</MudStack>
</MudExpansionPanel>
@* ── Social Media ── *@
<MudExpansionPanel Text="شبکه‌های اجتماعی" Icon="@Icons.Material.Filled.Share" IsInitiallyExpanded="true">
<MudStack Spacing="2" Class="pa-2">
<MudTextField @bind-Value="Model.TelegramUrl"
Label="تلگرام"
Placeholder="https://t.me/kbs1"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Custom.Brands.Telegram" />
<MudTextField @bind-Value="Model.InstagramUrl"
Label="اینستاگرام"
Placeholder="https://instagram.com/kbs1"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Custom.Brands.Instagram" />
<MudTextField @bind-Value="Model.LinkedinUrl"
Label="لینکدین"
Placeholder="https://linkedin.com/company/kbs1"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Custom.Brands.LinkedIn" />
<MudTextField @bind-Value="Model.WhatsappUrl"
Label="واتساپ"
Placeholder="https://wa.me/989123456789"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Custom.Brands.WhatsApp" />
</MudStack>
</MudExpansionPanel>
</MudExpansionPanels>
@code {
[Parameter] public ContactSettingsModel Model { get; set; } = new();
}
@@ -1,332 +0,0 @@
@using BackOffice.Pages.Content.Components.SettingsModels
<MudExpansionPanels MultiExpansion="true" Elevation="0">
@* ── Hero Buttons ── *@
<MudExpansionPanel Text="دکمه‌های هیرو" Icon="@Icons.Material.Filled.TouchApp" IsInitiallyExpanded="true">
<MudStack Spacing="2" Class="pa-2">
<MudTextField @bind-Value="Model.HeroButtonPrimaryText"
Label="متن دکمه اصلی"
Placeholder="شروع رایگان"
Variant="Variant.Outlined" />
<MudTextField @bind-Value="Model.HeroButtonSecondaryText"
Label="متن دکمه ثانویه"
Placeholder="آخرین اخبار"
Variant="Variant.Outlined" />
</MudStack>
</MudExpansionPanel>
@* ── Trust Badges ── *@
<MudExpansionPanel Text="@($"نشان‌های اعتماد ({Model.TrustBadges.Count})")" Icon="@Icons.Material.Filled.Verified">
<MudStack Spacing="2" Class="pa-2">
@for (var i = 0; i < Model.TrustBadges.Count; i++)
{
var index = i;
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Style="border:1px solid var(--mud-palette-lines-default);">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudText Typo="Typo.caption" Class="mud-text-secondary" Style="min-width:20px;">@(index + 1)</MudText>
<MudTextField @bind-Value="Model.TrustBadges[index].IconName"
Label="نام آیکون"
Placeholder="Timer"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Style="flex:1;" />
<MudTextField @bind-Value="Model.TrustBadges[index].Text"
Label="متن"
Placeholder="ثبت‌نام زیر ۲ دقیقه"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Style="flex:2;" />
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
Size="Size.Small"
OnClick="() => Model.TrustBadges.RemoveAt(index)" />
</MudStack>
</MudPaper>
}
<MudButton StartIcon="@Icons.Material.Filled.Add"
Color="Color.Primary"
Variant="Variant.Text"
OnClick="() => Model.TrustBadges.Add(new TrustBadgeModel())">
افزودن نشان
</MudButton>
</MudStack>
</MudExpansionPanel>
@* ── Steps ── *@
<MudExpansionPanel Text="@($"مراحل ({Model.Steps.Count})")" Icon="@Icons.Material.Filled.FormatListNumbered">
<MudStack Spacing="2" Class="pa-2">
<MudTextField @bind-Value="Model.StepsTitle"
Label="عنوان بخش مراحل"
Placeholder="سه گام تا شروع"
Variant="Variant.Outlined" />
@for (var i = 0; i < Model.Steps.Count; i++)
{
var index = i;
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Style="border:1px solid var(--mud-palette-lines-default);">
<MudStack Spacing="2">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudAvatar Size="Size.Small" Color="Color.Primary">
<MudText Typo="Typo.caption" Style="color:#fff;">@(index + 1)</MudText>
</MudAvatar>
<MudTextField @bind-Value="Model.Steps[index].Title"
Label="عنوان مرحله"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Style="flex:1;" />
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
Size="Size.Small"
OnClick="() => Model.Steps.RemoveAt(index)" />
</MudStack>
<MudTextField @bind-Value="Model.Steps[index].Description"
Label="توضیحات"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Lines="2" />
</MudStack>
</MudPaper>
}
<MudButton StartIcon="@Icons.Material.Filled.Add"
Color="Color.Primary"
Variant="Variant.Text"
OnClick="() => Model.Steps.Add(new StepModel())">
افزودن مرحله
</MudButton>
</MudStack>
</MudExpansionPanel>
@* ── Features ── *@
<MudExpansionPanel Text="@($"ویژگی‌ها ({Model.Features.Count})")" Icon="@Icons.Material.Filled.Star">
<MudStack Spacing="2" Class="pa-2">
<MudTextField @bind-Value="Model.FeaturesTitle"
Label="عنوان بخش ویژگی‌ها"
Placeholder="چرا کارا بازار سلامت؟"
Variant="Variant.Outlined" />
@for (var i = 0; i < Model.Features.Count; i++)
{
var index = i;
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Style="border:1px solid var(--mud-palette-lines-default);">
<MudStack Spacing="2">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudText Typo="Typo.caption" Class="mud-text-secondary" Style="min-width:20px;">@(index + 1)</MudText>
<MudTextField @bind-Value="Model.Features[index].IconName"
Label="نام آیکون"
Placeholder="VerifiedUser"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Style="flex:1;" />
<MudTextField @bind-Value="Model.Features[index].Title"
Label="عنوان"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Style="flex:2;" />
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
Size="Size.Small"
OnClick="() => Model.Features.RemoveAt(index)" />
</MudStack>
<MudTextField @bind-Value="Model.Features[index].Description"
Label="توضیحات"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Lines="2" />
</MudStack>
</MudPaper>
}
<MudButton StartIcon="@Icons.Material.Filled.Add"
Color="Color.Primary"
Variant="Variant.Text"
OnClick="() => Model.Features.Add(new FeatureModel())">
افزودن ویژگی
</MudButton>
</MudStack>
</MudExpansionPanel>
@* ── Stats ── *@
<MudExpansionPanel Text="@($"آمار ({Model.Stats.Count})")" Icon="@Icons.Material.Filled.BarChart">
<MudStack Spacing="2" Class="pa-2">
<MudTextField @bind-Value="Model.StatsTitle"
Label="عنوان بخش آمار"
Placeholder="آمار پلتفرم"
Variant="Variant.Outlined" />
@for (var i = 0; i < Model.Stats.Count; i++)
{
var index = i;
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Style="border:1px solid var(--mud-palette-lines-default);">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudText Typo="Typo.caption" Class="mud-text-secondary" Style="min-width:20px;">@(index + 1)</MudText>
<MudTextField @bind-Value="Model.Stats[index].Label"
Label="عنوان"
Placeholder="رشد میانگین تیم"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Style="flex:2;" />
<MudNumericField @bind-Value="Model.Stats[index].Value"
Label="مقدار"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Style="flex:1;" />
<MudTextField @bind-Value="Model.Stats[index].Suffix"
Label="پسوند"
Placeholder="%+"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Style="flex:1;" />
<MudSelect @bind-Value="Model.Stats[index].Color"
Label="رنگ"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Style="flex:1;">
<MudSelectItem Value="@("Primary")">آبی</MudSelectItem>
<MudSelectItem Value="@("Secondary")">بنفش</MudSelectItem>
<MudSelectItem Value="@("Success")">سبز</MudSelectItem>
<MudSelectItem Value="@("Warning")">زرد</MudSelectItem>
<MudSelectItem Value="@("Error")">قرمز</MudSelectItem>
<MudSelectItem Value="@("Info")">آبی روشن</MudSelectItem>
</MudSelect>
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
Size="Size.Small"
OnClick="() => Model.Stats.RemoveAt(index)" />
</MudStack>
</MudPaper>
}
<MudButton StartIcon="@Icons.Material.Filled.Add"
Color="Color.Primary"
Variant="Variant.Text"
OnClick="() => Model.Stats.Add(new StatModel())">
افزودن آمار
</MudButton>
</MudStack>
</MudExpansionPanel>
@* ── Testimonials ── *@
<MudExpansionPanel Text="@($"نظرات مشتریان ({Model.Testimonials.Count})")" Icon="@Icons.Material.Filled.FormatQuote">
<MudStack Spacing="2" Class="pa-2">
<MudTextField @bind-Value="Model.TestimonialsTitle"
Label="عنوان بخش نظرات"
Placeholder="اعتماد مشتریان"
Variant="Variant.Outlined" />
@for (var i = 0; i < Model.Testimonials.Count; i++)
{
var index = i;
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Style="border:1px solid var(--mud-palette-lines-default);">
<MudStack Spacing="2">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudText Typo="Typo.caption" Class="mud-text-secondary" Style="min-width:20px;">@(index + 1)</MudText>
<MudTextField @bind-Value="Model.Testimonials[index].Name"
Label="نام"
Placeholder="شرکت سینا نت"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Style="flex:1;" />
<MudTextField @bind-Value="Model.Testimonials[index].Role"
Label="سمت"
Placeholder="مدیر عملیات"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Style="flex:1;" />
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
Size="Size.Small"
OnClick="() => Model.Testimonials.RemoveAt(index)" />
</MudStack>
<MudTextField @bind-Value="Model.Testimonials[index].Quote"
Label="متن نظر"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Lines="2" />
</MudStack>
</MudPaper>
}
<MudButton StartIcon="@Icons.Material.Filled.Add"
Color="Color.Primary"
Variant="Variant.Text"
OnClick="() => Model.Testimonials.Add(new TestimonialModel())">
افزودن نظر
</MudButton>
</MudStack>
</MudExpansionPanel>
@* ── FAQs ── *@
<MudExpansionPanel Text="@($"سوالات متداول ({Model.Faqs.Count})")" Icon="@Icons.Material.Filled.QuestionAnswer">
<MudStack Spacing="2" Class="pa-2">
<MudTextField @bind-Value="Model.FaqTitle"
Label="عنوان بخش سوالات"
Placeholder="سوالات متداول"
Variant="Variant.Outlined" />
@for (var i = 0; i < Model.Faqs.Count; i++)
{
var index = i;
<MudPaper Elevation="0" Class="pa-3 rounded-lg" Style="border:1px solid var(--mud-palette-lines-default);">
<MudStack Spacing="2">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudText Typo="Typo.caption" Class="mud-text-secondary" Style="min-width:20px;">@(index + 1)</MudText>
<MudTextField @bind-Value="Model.Faqs[index].Question"
Label="سوال"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Style="flex:1;" />
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
Size="Size.Small"
OnClick="() => Model.Faqs.RemoveAt(index)" />
</MudStack>
<MudTextField @bind-Value="Model.Faqs[index].Answer"
Label="پاسخ"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Lines="2" />
</MudStack>
</MudPaper>
}
<MudButton StartIcon="@Icons.Material.Filled.Add"
Color="Color.Primary"
Variant="Variant.Text"
OnClick="() => Model.Faqs.Add(new FaqModel())">
افزودن سوال
</MudButton>
</MudStack>
</MudExpansionPanel>
@* ── CTA Section ── *@
<MudExpansionPanel Text="بنر فراخوان (CTA)" Icon="@Icons.Material.Filled.Campaign">
<MudStack Spacing="2" Class="pa-2">
<MudTextField @bind-Value="Model.CtaTitle"
Label="عنوان CTA"
Placeholder="آماده شروع هستید؟"
Variant="Variant.Outlined" />
<MudTextField @bind-Value="Model.CtaDescription"
Label="توضیحات CTA"
Placeholder="همین الان ثبت‌نام کنید..."
Variant="Variant.Outlined"
Lines="2" />
<MudTextField @bind-Value="Model.CtaButtonText"
Label="متن دکمه CTA"
Placeholder="شروع رایگان"
Variant="Variant.Outlined" />
</MudStack>
</MudExpansionPanel>
@* ── Blog Section ── *@
<MudExpansionPanel Text="بخش بلاگ" Icon="@Icons.Material.Filled.Article">
<MudStack Spacing="2" Class="pa-2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudSwitch T="bool" @bind-Value="_featuredBlogEnabled" Color="Color.Primary" />
<MudText Typo="Typo.body2">نمایش آخرین مطالب بلاگ</MudText>
</MudStack>
</MudStack>
</MudExpansionPanel>
</MudExpansionPanels>
@code {
[Parameter] public LandingSettingsModel Model { get; set; } = new();
private bool _featuredBlogEnabled
{
get => Model.FeaturedBlogEnabled ?? true;
set => Model.FeaturedBlogEnabled = value;
}
}
@@ -1,32 +0,0 @@
@using BackOffice.Pages.Content.Components.SettingsModels
<MudExpansionPanels MultiExpansion="true" Elevation="0">
@* ── Page Settings ── *@
<MudExpansionPanel Text="تنظیمات صفحه" Icon="@Icons.Material.Filled.Settings" IsInitiallyExpanded="true">
<MudStack Spacing="2" Class="pa-2">
<MudTextField @bind-Value="Model.PageDescription"
Label="توضیحات صفحه"
Variant="Variant.Outlined"
Lines="3"
Placeholder="مجوزها و نمادهای اعتماد الکترونیکی ما..." />
<MudSelect @bind-Value="Model.DisplayStyle"
Label="نوع نمایش"
Variant="Variant.Outlined">
<MudSelectItem Value="@("grid")">گرید (Grid)</MudSelectItem>
<MudSelectItem Value="@("list")">لیست (List)</MudSelectItem>
<MudSelectItem Value="@("carousel")">اسلایدر (Carousel)</MudSelectItem>
</MudSelect>
</MudStack>
</MudExpansionPanel>
@* ── Hint ── *@
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true" Class="mt-2">
تصاویر مجوزها از بخش «مدیریت تصاویر» قابل افزودن و ویرایش هستند.
</MudAlert>
</MudExpansionPanels>
@code {
[Parameter] public LicensesSettingsModel Model { get; set; } = new();
}
@@ -1,100 +0,0 @@
@using BackOffice.Services.Content
@using Microsoft.AspNetCore.Components.Forms
@using BackOffice.Common.BaseComponents
<MudDialog>
<DialogContent>
<MudForm @ref="_form" @bind-IsValid="@_isValid">
<MudStack Spacing="2">
<MudSelect T="string" @bind-Value="Model.ImageGroup"
Label="گروه تصویر"
Required="true"
Variant="Variant.Outlined">
@foreach (var group in GetAvailableGroups())
{
<MudSelectItem T="string" Value="@group.Key">@group.Value</MudSelectItem>
}
</MudSelect>
<MudTextField @bind-Value="Model.Title"
Label="عنوان"
Variant="Variant.Outlined" />
<MudTextField @bind-Value="Model.Subtitle"
Label="زیرعنوان"
Variant="Variant.Outlined" />
<MudTextField @bind-Value="Model.Description"
Label="توضیحات"
Variant="Variant.Outlined"
Lines="3" />
<MudTextField @bind-Value="Model.LinkUrl"
Label="لینک"
Variant="Variant.Outlined"
Placeholder="https://..." />
<MudTextField @bind-Value="Model.IconName"
Label="نام آیکون"
Variant="Variant.Outlined"
HelperText="نام آیکون Material Design — مثلاً: Verified, Star, Group" />
<MudNumericField @bind-Value="Model.SortOrder"
Label="ترتیب نمایش"
Variant="Variant.Outlined"
Min="0" />
<MudText Typo="Typo.subtitle2">تصویر</MudText>
<MudStack Justify="Justify.Center" AlignItems="AlignItems.Center">
@if (!string.IsNullOrWhiteSpace(_imagePreview))
{
<Image Src="@_imagePreview" Width="200" Height="150" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
}
else if (!string.IsNullOrEmpty(ExistingImagePath))
{
<Image Src="@ExistingImagePath" Width="200" Height="150" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
}
else
{
<MudPaper Class="d-flex align-center justify-center" Style="width:200px;height:150px;">
<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>
<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>
</MudForm>
</DialogContent>
<DialogActions>
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="SubmitAsync"
Disabled="@(!_isValid || _loading)">
@(IsNew ? "افزودن" : "ذخیره تغییرات")
</MudButton>
<MudButton Color="Color.Default" Variant="Variant.Text" OnClick="Cancel">
انصراف
</MudButton>
</DialogActions>
</MudDialog>
@@ -1,112 +0,0 @@
using BackOffice.Services.Content;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using MudBlazor;
namespace BackOffice.Pages.Content.Components;
public partial class PageImageEditDialog
{
[CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!;
[Inject] public ISitePageSettingsService PageSettingsService { get; set; } = default!;
[Parameter] public SavePageImageDto Model { get; set; } = new();
[Parameter] public string PageKey { get; set; } = string.Empty;
[Parameter] public bool IsNew { get; set; }
[Parameter] public string? ExistingImagePath { get; set; }
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 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 SubmitAsync()
{
if (_form != null)
{
await _form.Validate();
if (!_isValid) return;
}
_loading = true;
if (_imageFile != null && _imageBuffer != null)
{
Model.ImageFile = _imageBuffer;
Model.ImageMime = _imageFile.ContentType;
Model.ImageFileName = Path.GetFileNameWithoutExtension(_imageFile.Name);
}
else if (IsNew && _imageBuffer == null)
{
Snackbar.Add("لطفاً یک تصویر انتخاب کنید", Severity.Warning);
_loading = false;
return;
}
try
{
var result = await PageSettingsService.SaveImageAsync(Model);
if (result.Success)
{
DialogInstance.Close(DialogResult.Ok(true));
}
else
{
Snackbar.Add("خطا در ذخیره تصویر", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
private Dictionary<string, string> GetAvailableGroups() => PageKey switch
{
"about" => new Dictionary<string, string>
{
{ "team", "اعضای تیم" },
{ "values", "ارزش‌ها" }
},
"licenses" => new Dictionary<string, string>
{
{ "licenses", "مجوزها" }
},
"landing" => new Dictionary<string, string>
{
{ "values", "ارزش‌ها" }
},
_ => new Dictionary<string, string>
{
{ "values", "ارزش‌ها" },
{ "team", "اعضای تیم" },
{ "licenses", "مجوزها" }
}
};
private void Cancel()
{
DialogInstance.Cancel();
}
}
@@ -1,88 +0,0 @@
@using BackOffice.Services.Content
@using BackOffice.Common.BaseComponents
<MudDialog>
<DialogContent>
<MudStack Spacing="2">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
<MudText Typo="Typo.subtitle1">تصاویر صفحه «@PageTitle»</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Small"
StartIcon="@Icons.Material.Filled.Add"
OnClick="AddImage">
افزودن تصویر
</MudButton>
</MudStack>
@if (!Images.Any())
{
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">
تصویری ثبت نشده است.
</MudAlert>
}
else
{
<MudDataGrid T="PageImageItemDto"
Items="Images"
Hover="true"
Dense="true"
Elevation="0">
<Columns>
<TemplateColumn Title="تصویر" Sortable="false">
<CellTemplate>
@if (!string.IsNullOrWhiteSpace(context.Item.ThumbnailPath))
{
<Image Src="@context.Item.ThumbnailPath" Width="48" Height="48" ObjectFit="ObjectFit.Cover" />
}
else if (!string.IsNullOrWhiteSpace(context.Item.ImagePath))
{
<Image Src="@context.Item.ImagePath" Width="48" Height="48" ObjectFit="ObjectFit.Cover" />
}
else
{
<MudIcon Icon="@Icons.Material.Filled.ImageNotSupported" Size="Size.Medium" />
}
</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.ImageGroup" Title="گروه" />
<PropertyColumn Property="x => x.Title" Title="عنوان" />
<PropertyColumn Property="x => x.SortOrder" Title="ترتیب" />
<PropertyColumn Property="x => x.IsActive" Title="وضعیت">
<CellTemplate>
<MudChip T="string"
Color="@(context.Item.IsActive ? Color.Success : Color.Error)"
Size="Size.Small">
@(context.Item.IsActive ? "فعال" : "غیرفعال")
</MudChip>
</CellTemplate>
</PropertyColumn>
<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="@(() => EditImage(context.Item))" />
</MudTooltip>
<MudTooltip Text="حذف">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Size="Size.Small"
Color="Color.Error"
OnClick="@(() => DeleteImage(context.Item))" />
</MudTooltip>
</MudStack>
</CellTemplate>
</TemplateColumn>
</Columns>
</MudDataGrid>
}
</MudStack>
</DialogContent>
<DialogActions>
<MudButton Color="Color.Default" Variant="Variant.Text" OnClick="Close">
بستن
</MudButton>
</DialogActions>
</MudDialog>
@@ -1,133 +0,0 @@
using BackOffice.Services.Content;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace BackOffice.Pages.Content.Components;
public partial class PageImagesDialog
{
[CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!;
[Inject] public ISitePageSettingsService PageSettingsService { get; set; } = default!;
[Parameter] public long PageId { get; set; }
[Parameter] public string PageKey { get; set; } = string.Empty;
[Parameter] public string PageTitle { get; set; } = string.Empty;
[Parameter] public List<PageImageItemDto> Images { get; set; } = new();
private bool _hasChanges;
private async Task AddImage()
{
var dto = new SavePageImageDto
{
SitePageSettingsId = PageId,
ImageGroup = GetDefaultImageGroup(),
IsActive = true,
SortOrder = Images.Count + 1
};
var parameters = new DialogParameters<PageImageEditDialog>
{
{ nameof(PageImageEditDialog.Model), dto },
{ nameof(PageImageEditDialog.PageKey), PageKey },
{ nameof(PageImageEditDialog.IsNew), true }
};
var dialog = await DialogService.ShowAsync<PageImageEditDialog>(
"افزودن تصویر", parameters,
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true });
var result = await dialog.Result;
if (result is { Canceled: false })
{
Snackbar.Add("تصویر با موفقیت اضافه شد", Severity.Success);
_hasChanges = true;
await ReloadImages();
}
}
private async Task EditImage(PageImageItemDto item)
{
var dto = new SavePageImageDto
{
Id = item.Id,
SitePageSettingsId = PageId,
ImageGroup = item.ImageGroup,
Title = item.Title,
Subtitle = item.Subtitle,
Description = item.Description,
LinkUrl = item.LinkUrl,
IconName = item.IconName,
SortOrder = item.SortOrder,
IsActive = item.IsActive
};
var parameters = new DialogParameters<PageImageEditDialog>
{
{ nameof(PageImageEditDialog.Model), dto },
{ nameof(PageImageEditDialog.PageKey), PageKey },
{ nameof(PageImageEditDialog.IsNew), false },
{ nameof(PageImageEditDialog.ExistingImagePath), item.ImagePath }
};
var dialog = await DialogService.ShowAsync<PageImageEditDialog>(
"ویرایش تصویر", parameters,
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true });
var result = await dialog.Result;
if (result is { Canceled: false })
{
Snackbar.Add("تصویر با موفقیت ویرایش شد", Severity.Success);
_hasChanges = true;
await ReloadImages();
}
}
private async Task DeleteImage(PageImageItemDto item)
{
var confirm = await DialogService.ShowMessageBox(
"تایید حذف",
$"آیا از حذف تصویر «{item.Title ?? item.ImageGroup}» اطمینان دارید؟",
yesText: "بله، حذف شود",
cancelText: "انصراف");
if (confirm != true) return;
try
{
await PageSettingsService.DeleteImageAsync(item.Id);
Snackbar.Add("تصویر با موفقیت حذف شد", Severity.Success);
_hasChanges = true;
await ReloadImages();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در حذف تصویر: {ex.Message}", Severity.Error);
}
}
private async Task ReloadImages()
{
var details = await PageSettingsService.GetByKeyAsync(PageKey);
if (details != null)
{
Images = details.Images;
StateHasChanged();
}
}
private string GetDefaultImageGroup() => PageKey switch
{
"about" => "team",
"licenses" => "licenses",
_ => "values"
};
private void Close()
{
if (_hasChanges)
DialogInstance.Close(DialogResult.Ok(true));
else
DialogInstance.Cancel();
}
}
@@ -1,111 +0,0 @@
@using BackOffice.Services.Content
@using BackOffice.Pages.Content.Components.SettingsModels
@using Microsoft.AspNetCore.Components.Forms
@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.MetaDescription"
Label="توضیحات متا (SEO)"
Variant="Variant.Outlined"
Lines="2" />
<MudTextField @bind-Value="Model.HeroTitle"
Label="عنوان هیرو"
Variant="Variant.Outlined" />
<MudTextField @bind-Value="Model.HeroSubtitle"
Label="زیرعنوان هیرو"
Variant="Variant.Outlined"
Lines="2" />
<MudText Typo="Typo.subtitle2">تصویر هیرو</MudText>
<MudStack Justify="Justify.Center" AlignItems="AlignItems.Center">
@if (!string.IsNullOrWhiteSpace(_imagePreview))
{
<Image Src="@_imagePreview" Width="280" Height="160" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
}
else if (!string.IsNullOrEmpty(ExistingHeroImagePath))
{
<Image Src="@ExistingHeroImagePath" Width="280" Height="160" ObjectPosition="ObjectPosition.Center" ObjectFit="ObjectFit.Cover" />
}
else
{
<MudPaper Class="d-flex align-center justify-center" Style="width:280px;height:160px;">
<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>
<MudDivider Class="my-2" />
<MudText Typo="Typo.subtitle1" Class="mb-1">
<MudIcon Icon="@Icons.Material.Filled.Tune" Size="Size.Small" Class="ml-1" />
تنظیمات اختصاصی «@PageTitle»
</MudText>
@* ── Typed Settings Editors ── *@
@switch (Model.PageKey)
{
case "landing":
<LandingSettingsEditor Model="_landingSettings" />
break;
case "about":
<AboutSettingsEditor Model="_aboutSettings" />
break;
case "contact":
<ContactSettingsEditor Model="_contactSettings" />
break;
case "licenses":
<LicensesSettingsEditor Model="_licensesSettings" />
break;
default:
<MudTextField @bind-Value="Model.SettingsJson"
Label="تنظیمات JSON"
Variant="Variant.Outlined"
Lines="8"
HelperText="تنظیمات اختصاصی صفحه (JSON)" />
break;
}
<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>
</MudForm>
</DialogContent>
<DialogActions>
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="SubmitAsync" Disabled="@(!_isValid || _loading)">
ذخیره تغییرات
</MudButton>
<MudButton Color="Color.Default" Variant="Variant.Text" OnClick="Cancel">
انصراف
</MudButton>
</DialogActions>
</MudDialog>
@@ -1,151 +0,0 @@
using System.Text.Json;
using BackOffice.Pages.Content.Components.SettingsModels;
using BackOffice.Services.Content;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using MudBlazor;
namespace BackOffice.Pages.Content.Components;
public partial class PageSettingsEditDialog
{
[CascadingParameter] IMudDialogInstance DialogInstance { get; set; } = default!;
[Inject] public ISitePageSettingsService PageSettingsService { get; set; } = default!;
[Parameter] public SavePageSettingsDto Model { get; set; } = new();
[Parameter] public string? ExistingHeroImagePath { get; set; }
[Parameter] public string PageTitle { get; set; } = string.Empty;
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;
// ── Typed settings models ──
private LandingSettingsModel _landingSettings = new();
private AboutSettingsModel _aboutSettings = new();
private ContactSettingsModel _contactSettings = new();
private LicensesSettingsModel _licensesSettings = new();
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
protected override void OnParametersSet()
{
DeserializeSettings();
}
/// <summary>
/// Parse existing SettingsJson into the typed model based on PageKey
/// </summary>
private void DeserializeSettings()
{
if (string.IsNullOrWhiteSpace(Model.SettingsJson)) return;
try
{
switch (Model.PageKey)
{
case "landing":
_landingSettings = JsonSerializer.Deserialize<LandingSettingsModel>(Model.SettingsJson, JsonOptions) ?? new();
break;
case "about":
_aboutSettings = JsonSerializer.Deserialize<AboutSettingsModel>(Model.SettingsJson, JsonOptions) ?? new();
break;
case "contact":
_contactSettings = JsonSerializer.Deserialize<ContactSettingsModel>(Model.SettingsJson, JsonOptions) ?? new();
break;
case "licenses":
_licensesSettings = JsonSerializer.Deserialize<LicensesSettingsModel>(Model.SettingsJson, JsonOptions) ?? new();
break;
}
}
catch
{
// If JSON is malformed, start with empty model
}
}
/// <summary>
/// Serialize the typed model back to JSON before saving
/// </summary>
private void SerializeSettings()
{
Model.SettingsJson = Model.PageKey switch
{
"landing" => JsonSerializer.Serialize(_landingSettings, JsonOptions),
"about" => JsonSerializer.Serialize(_aboutSettings, JsonOptions),
"contact" => JsonSerializer.Serialize(_contactSettings, JsonOptions),
"licenses" => JsonSerializer.Serialize(_licensesSettings, JsonOptions),
_ => Model.SettingsJson // keep raw JSON for unknown pages
};
}
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 SubmitAsync()
{
if (_form != null)
{
await _form.Validate();
if (!_isValid) return;
}
_loading = true;
// Serialize typed model → JSON before saving
SerializeSettings();
if (_imageFile != null && _imageBuffer != null)
{
Model.ImageFile = _imageBuffer;
Model.ImageMime = _imageFile.ContentType;
Model.ImageFileName = Path.GetFileNameWithoutExtension(_imageFile.Name);
}
try
{
var result = await PageSettingsService.SaveSettingsAsync(Model);
if (result.Success)
{
DialogInstance.Close(DialogResult.Ok(true));
}
else
{
Snackbar.Add("خطا در ذخیره تنظیمات", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
private void Cancel()
{
DialogInstance.Cancel();
}
}
@@ -1,108 +0,0 @@
namespace BackOffice.Pages.Content.Components.SettingsModels;
// ══════════════════════════════════════════════════════════════
// LANDING PAGE
// ══════════════════════════════════════════════════════════════
public class LandingSettingsModel
{
public string? HeroButtonPrimaryText { get; set; }
public string? HeroButtonSecondaryText { get; set; }
public List<TrustBadgeModel> TrustBadges { get; set; } = new();
public string? StepsTitle { get; set; }
public List<StepModel> Steps { get; set; } = new();
public string? FeaturesTitle { get; set; }
public List<FeatureModel> Features { get; set; } = new();
public string? StatsTitle { get; set; }
public List<StatModel> Stats { get; set; } = new();
public string? TestimonialsTitle { get; set; }
public List<TestimonialModel> Testimonials { get; set; } = new();
public string? FaqTitle { get; set; }
public List<FaqModel> Faqs { get; set; } = new();
public string? CtaTitle { get; set; }
public string? CtaDescription { get; set; }
public string? CtaButtonText { get; set; }
public bool? FeaturedBlogEnabled { get; set; }
}
public class TrustBadgeModel
{
public string? IconName { get; set; }
public string? Text { get; set; }
}
public class StepModel
{
public string? Title { get; set; }
public string? Description { get; set; }
}
public class FeatureModel
{
public string? IconName { get; set; }
public string? Title { get; set; }
public string? Description { get; set; }
}
public class StatModel
{
public string? Label { get; set; }
public double Value { get; set; }
public string? Suffix { get; set; }
public string? Color { get; set; }
}
public class TestimonialModel
{
public string? Quote { get; set; }
public string? Name { get; set; }
public string? Role { get; set; }
}
public class FaqModel
{
public string? Question { get; set; }
public string? Answer { get; set; }
}
// ══════════════════════════════════════════════════════════════
// ABOUT PAGE
// ══════════════════════════════════════════════════════════════
public class AboutSettingsModel
{
public string? VisionTitle { get; set; }
public string? VisionText { get; set; }
public string? VisionIcon { get; set; }
public string? MissionTitle { get; set; }
public string? MissionText { get; set; }
public string? MissionIcon { get; set; }
public string? ValuesTitle { get; set; }
public string? TeamTitle { get; set; }
}
// ══════════════════════════════════════════════════════════════
// CONTACT PAGE
// ══════════════════════════════════════════════════════════════
public class ContactSettingsModel
{
public string? Address { get; set; }
public string? Phone { get; set; }
public string? Email { get; set; }
public string? WorkingHours { get; set; }
public string? TelegramUrl { get; set; }
public string? InstagramUrl { get; set; }
public string? LinkedinUrl { get; set; }
public string? WhatsappUrl { get; set; }
}
// ══════════════════════════════════════════════════════════════
// LICENSES PAGE
// ══════════════════════════════════════════════════════════════
public class LicensesSettingsModel
{
public string? PageDescription { get; set; }
public string? DisplayStyle { get; set; }
}
@@ -1,74 +0,0 @@
@page "/content/page-settings"
@using BackOffice.Services.Content
@inject ISitePageSettingsService PageSettingsService
<div>
<MudStack Spacing="3">
<MudText Typo="Typo.h5">مدیریت تنظیمات صفحات</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">تنظیمات ۴ صفحه ثابت سایت: صفحه اصلی، درباره ما، تماس با ما و مجوزها</MudText>
@if (_loading)
{
<MudProgressLinear Indeterminate="true" Color="Color.Primary" />
}
else
{
<MudGrid>
@foreach (var item in _pages)
{
<MudItem xs="12" sm="6" md="3">
<MudCard Elevation="2" Class="pa-2">
<MudCardHeader>
<CardHeaderContent>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@GetPageIcon(item.PageKey)" Size="Size.Large" Color="Color.Primary" />
<MudStack Spacing="0">
<MudText Typo="Typo.h6">@item.Title</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">@item.PageKey</MudText>
</MudStack>
</MudStack>
</CardHeaderContent>
<CardHeaderActions>
<MudChip T="string"
Color="@(item.IsActive ? Color.Success : Color.Error)"
Size="Size.Small">
@(item.IsActive ? "فعال" : "غیرفعال")
</MudChip>
</CardHeaderActions>
</MudCardHeader>
<MudCardContent>
<MudStack Spacing="1">
<MudText Typo="Typo.body2">
<MudIcon Icon="@Icons.Material.Filled.Image" Size="Size.Small" Class="ml-1" />
@item.ImageCount تصویر
</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">
آخرین ویرایش: @item.LastModified.MiladiToJalaliWithTime()
</MudText>
</MudStack>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Small"
StartIcon="@Icons.Material.Filled.Edit"
OnClick="@(() => EditPageSettings(item))">
ویرایش
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Info"
Size="Size.Small"
StartIcon="@Icons.Material.Filled.Collections"
OnClick="@(() => ManageImages(item))">
تصاویر
</MudButton>
</MudCardActions>
</MudCard>
</MudItem>
}
</MudGrid>
}
</MudStack>
</div>
@@ -1,119 +0,0 @@
using BackOffice.Services.Content;
using BackOffice.Pages.Content.Components;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace BackOffice.Pages.Content;
public partial class PageSettingsManagementPage
{
private List<PageSettingsSummaryDto> _pages = new();
private bool _loading = true;
protected override async Task OnInitializedAsync()
{
await LoadPagesAsync();
}
private async Task LoadPagesAsync()
{
_loading = true;
try
{
_pages = await PageSettingsService.GetAllAsync();
}
catch (Exception ex)
{
Snackbar.Add($"خطا در دریافت صفحات: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
private async Task EditPageSettings(PageSettingsSummaryDto summary)
{
var details = await PageSettingsService.GetByKeyAsync(summary.PageKey);
if (details == null)
{
Snackbar.Add("خطا در دریافت تنظیمات صفحه", Severity.Error);
return;
}
var dto = new SavePageSettingsDto
{
PageKey = details.PageKey,
Title = details.Title,
MetaDescription = details.MetaDescription,
HeroTitle = details.HeroTitle,
HeroSubtitle = details.HeroSubtitle,
IsActive = details.IsActive,
SettingsJson = details.SettingsJson
};
var parameters = new DialogParameters<PageSettingsEditDialog>
{
{ nameof(PageSettingsEditDialog.Model), dto },
{ nameof(PageSettingsEditDialog.ExistingHeroImagePath), details.HeroImagePath },
{ nameof(PageSettingsEditDialog.PageTitle), GetPageDisplayName(summary.PageKey) }
};
var dialog = await DialogService.ShowAsync<PageSettingsEditDialog>(
$"ویرایش تنظیمات «{GetPageDisplayName(summary.PageKey)}»", parameters,
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true });
var result = await dialog.Result;
if (result is { Canceled: false })
{
Snackbar.Add("تنظیمات صفحه با موفقیت ذخیره شد", Severity.Success);
await LoadPagesAsync();
}
}
private async Task ManageImages(PageSettingsSummaryDto summary)
{
var details = await PageSettingsService.GetByKeyAsync(summary.PageKey);
if (details == null)
{
Snackbar.Add("خطا در دریافت اطلاعات صفحه", Severity.Error);
return;
}
var parameters = new DialogParameters<PageImagesDialog>
{
{ nameof(PageImagesDialog.PageId), details.Id },
{ nameof(PageImagesDialog.PageKey), details.PageKey },
{ nameof(PageImagesDialog.PageTitle), GetPageDisplayName(summary.PageKey) },
{ nameof(PageImagesDialog.Images), details.Images }
};
var dialog = await DialogService.ShowAsync<PageImagesDialog>(
$"مدیریت تصاویر «{GetPageDisplayName(summary.PageKey)}»", parameters,
new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Large, FullWidth = true });
var result = await dialog.Result;
if (result is { Canceled: false })
{
await LoadPagesAsync();
}
}
private static string GetPageIcon(string pageKey) => pageKey switch
{
"landing" => Icons.Material.Filled.Home,
"about" => Icons.Material.Filled.Info,
"contact" => Icons.Material.Filled.ContactMail,
"licenses" => Icons.Material.Filled.VerifiedUser,
_ => Icons.Material.Filled.Article
};
private static string GetPageDisplayName(string pageKey) => pageKey switch
{
"landing" => "صفحه اصلی",
"about" => "درباره ما",
"contact" => "تماس با ما",
"licenses" => "مجوزها",
_ => pageKey
};
}
@@ -32,10 +32,10 @@
<MudStack Spacing="1">
<MudText Typo="Typo.body2" Color="Color.Secondary">مجموع فروش (۷ روز)</MudText>
<MudText Typo="Typo.h5" Color="Color.Primary">
@_stats.TotalSalesLast7Days.ToString("N0") تومان
@_stats.TotalSalesLast7Days.ToString("N0") ریال
</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">
امروز: @_stats.TodaySales.ToString("N0") تومان
امروز: @_stats.TodaySales.ToString("N0") ریال
</MudText>
</MudStack>
</MudItem>
@@ -43,7 +43,7 @@
<MudStack Spacing="1">
<MudText Typo="Typo.body2" Color="Color.Secondary">میانگین مبلغ هر سفارش</MudText>
<MudText Typo="Typo.h5">
@_stats.AverageOrderAmount.ToString("N0") تومان
@_stats.AverageOrderAmount.ToString("N0") ریال
</MudText>
</MudStack>
</MudItem>
@@ -26,7 +26,6 @@
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Processing">در حال آماده‌سازی</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.ReadyForOfficePickup">آماده تحویل در دفتر</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
</MudSelect>
@@ -56,15 +55,6 @@
</MudItem>
}
@if (Model.Status == OrderStatus.ReadyForOfficePickup)
{
<MudItem xs="12">
<MudAlert Severity="Severity.Info">
<MudText>سفارش برای تحویل حضوری در دفتر آماده است.</MudText>
</MudAlert>
</MudItem>
}
@if (Model.Status == OrderStatus.Shipped)
{
<MudItem xs="12">
@@ -136,9 +126,8 @@
OrderStatus.Processing => Color.Primary,
OrderStatus.Shipped => Color.Secondary,
OrderStatus.Delivered => Color.Success,
OrderStatus.ReadyForOfficePickup => Color.Primary,
OrderStatus.Cancelled => Color.Dark,
OrderStatus.Returned => Color.Error,
OrderStatus.Cancelled => Color.Error,
OrderStatus.Returned => Color.Dark,
_ => Color.Default
};
}
@@ -152,7 +141,6 @@
OrderStatus.Processing => "در حال آماده‌سازی",
OrderStatus.Shipped => "ارسال شده",
OrderStatus.Delivered => "تحویل داده شده",
OrderStatus.ReadyForOfficePickup => "آماده تحویل در دفتر",
OrderStatus.Cancelled => "لغو شده",
OrderStatus.Returned => "مرجوع شده",
_ => "نامشخص"
@@ -1,72 +1,36 @@
@using BackOffice.Services.DiscountOrder
@using BackOffice.Common.BaseComponents
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.ShoppingCart" Class="ml-2" />
جزئیات سفارش #@Order.OrderNumber
</MudText>
</TitleContent>
<DialogContent>
<MudStack Spacing="2">
<MudText Typo="Typo.h6">جزئیات سفارش شماره @(string.IsNullOrWhiteSpace(Order.OrderNumber) ? Order.Id.ToString() : Order.OrderNumber)</MudText>
<MudText Typo="Typo.body2">
کاربر:
@if (!string.IsNullOrWhiteSpace(Order.UserFullName))
{
@Order.UserFullName
}
else
{
<text>#@Order.UserId</text>
}
@if (!string.IsNullOrWhiteSpace(DisplayMobile))
{
<text> (@DisplayMobile)</text>
}
</MudText>
<MudGrid>
<!-- اطلاعات کاربر -->
<MudItem xs="12">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" GutterBottom="true">اطلاعات سفارش</MudText>
<MudGrid>
<MudItem xs="6">
<MudText Typo="Typo.body2" Color="Color.Secondary">شماره سفارش:</MudText>
<MudText Typo="Typo.body1"><strong>@Order.OrderNumber</strong></MudText>
</MudItem>
<MudItem xs="6">
<MudText Typo="Typo.body2" Color="Color.Secondary">شناسه کاربر:</MudText>
<MudText Typo="Typo.body1">@Order.UserId</MudText>
</MudItem>
</MudGrid>
</MudPaper>
</MudItem>
<MudPaper Class="pa-3">
<MudStack Spacing="2">
<MudText Typo="Typo.subtitle2">خلاصه سفارش</MudText>
<MudDivider />
<MudText Typo="Typo.body2">مبلغ کل: @Order.TotalPrice.ToString("N0") تومان</MudText>
<MudText Typo="Typo.body2" Color="Color.Success">
از کیف پول اعتباری: @Order.DiscountBalanceUsed.ToString("N0") تومان
</MudText>
<MudText Typo="Typo.body2">
مبلغ پرداختی از درگاه: @Order.GatewayAmount.ToString("N0") تومان
</MudText>
@if (!string.IsNullOrEmpty(Order.TransactionId))
{
<MudText Typo="Typo.body2">شناسه پرداخت: @Order.TransactionId</MudText>
}
@if (Order.Created.HasValue)
{
<MudText Typo="Typo.body2">
تاریخ ثبت: @Order.Created.Value.MiladiToJalaliWithTime()
</MudText>
}
<MudText Typo="Typo.body2">
وضعیت پرداخت:
<MudChip T="string"
Color="@GetPaymentStatusColor(Order.PaymentStatusValue)"
Size="Size.Small"
Icon="@GetPaymentStatusIcon(Order.PaymentStatusValue)">
@GetPaymentStatusText(Order.PaymentStatusValue)
</MudChip>
</MudText>
@if (Order.VatAmount > 0)
{
<MudDivider Class="my-2" />
<MudText Typo="Typo.subtitle2">جزئیات مالیات بر ارزش افزوده</MudText>
<MudText Typo="Typo.body2">
مالیات: @Order.VatAmount.ToString("N0") تومان
</MudText>
<MudText Typo="Typo.body2">
مبلغ نهایی (شامل مالیات): @Order.TotalPrice.ToString("N0") تومان
</MudText>
}
<MudDivider Class="my-2" />
<MudText Typo="Typo.subtitle2">Timeline وضعیت</MudText>
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center">
<!-- Timeline وضعیت -->
<MudItem xs="12">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.subtitle2" GutterBottom="true">Timeline وضعیت</MudText>
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center" Justify="Justify.Center">
@foreach (var step in _timelineSteps)
{
<MudStack Spacing="0" AlignItems="AlignItems.Center">
@@ -77,155 +41,218 @@
</MudStack>
}
</MudStack>
</MudPaper>
</MudItem>
<MudDivider Class="my-2" />
<MudText Typo="Typo.body2">
وضعیت ارسال:
<MudSelect T="OrderStatus"
Label="وضعیت ارسال"
Dense="true"
Variant="Variant.Outlined"
@bind-Value="_newStatus">
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Pending">در انتظار پرداخت</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Paid">پرداخت شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Processing">در حال آماده‌سازی</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.ReadyForOfficePickup">آماده تحویل در دفتر</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
</MudSelect>
</MudText>
<MudTextField T="string"
Label="کد رهگیری"
Variant="Variant.Outlined"
Dense="true"
@bind-Value="_trackingCode" />
<MudTextField T="string"
Label="یادداشت ادمین"
Variant="Variant.Outlined"
Dense="true"
Lines="3"
HelperText="دلیل تغییر وضعیت یا توضیحات اضافی"
@bind-Value="_adminNotes" />
<!-- وضعیت سفارش -->
<MudItem xs="12" sm="6">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" GutterBottom="true">وضعیت سفارش</MudText>
<MudChip T="string" Color="@GetStatusColor(Order.Status)" Size="Size.Large">
@GetStatusText(Order.Status)
</MudChip>
@if (Order.Created.HasValue)
{
<MudText Typo="Typo.body2" Class="mt-2">
تاریخ ثبت: @Order.Created.Value.MiladiToJalaliWithTime()
</MudText>
}
</MudPaper>
</MudItem>
<!-- وضعیت پرداخت -->
<MudItem xs="12" sm="6">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" GutterBottom="true">وضعیت پرداخت</MudText>
@if (Order.PaymentCompleted)
{
<MudChip T="string" Color="Color.Success" Size="Size.Large" Icon="@Icons.Material.Filled.CheckCircle">
پرداخت شده
</MudChip>
@if (!string.IsNullOrEmpty(Order.TransactionId))
{
<MudText Typo="Typo.body2" Class="mt-2">
کد تراکنش: @Order.TransactionId
</MudText>
}
}
else
{
<MudChip T="string" Color="Color.Warning" Size="Size.Large" Icon="@Icons.Material.Filled.Schedule">
در انتظار پرداخت
</MudChip>
}
</MudPaper>
</MudItem>
<!-- آدرس ارسال -->
@if (Order.Address != null)
{
<MudItem xs="12">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" GutterBottom="true">
<MudIcon Icon="@Icons.Material.Filled.LocationOn" Size="Size.Small" />
آدرس ارسال
</MudText>
<MudText Typo="Typo.body1"><strong>@Order.Address.Title</strong></MudText>
<MudText Typo="Typo.body2">@Order.Address.Address</MudText>
<MudText Typo="Typo.body2">کد پستی: @Order.Address.PostalCode</MudText>
@if (!string.IsNullOrEmpty(Order.Address.Phone))
{
<MudText Typo="Typo.body2">تلفن: @Order.Address.Phone</MudText>
}
</MudPaper>
</MudItem>
}
<!-- آیتم‌های سفارش -->
<MudItem xs="12">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.h6" GutterBottom="true">آیتم‌های سفارش</MudText>
<MudTable Items="@Order.Items" Hover="true" Dense="true" Elevation="0">
<HeaderContent>
<MudTh>محصول</MudTh>
<MudTh>تعداد</MudTh>
<MudTh>قیمت واحد</MudTh>
<MudTh>تخفیف</MudTh>
<MudTh>قیمت نهایی</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
@if (!string.IsNullOrWhiteSpace(context.ThumbnailPath))
{
<MudImage Src="@context.ThumbnailPath" Width="40" Height="40"
ObjectFit="ObjectFit.Cover" Class="rounded" />
}
<MudText>@context.ProductTitle</MudText>
</MudStack>
</MudTd>
<MudTd>@context.Count</MudTd>
<MudTd>@context.UnitPrice.ToString("N0") ریال</MudTd>
<MudTd>
<MudChip T="string" Size="Size.Small" Color="Color.Success">@context.MaxDiscountPercent%</MudChip>
</MudTd>
<MudTd><strong>@context.FinalPrice.ToString("N0") ریال</strong></MudTd>
</RowTemplate>
</MudTable>
</MudPaper>
</MudItem>
<!-- خلاصه مالی -->
<MudItem xs="12">
<MudPaper Class="pa-4" Elevation="2">
<MudText Typo="Typo.h6" GutterBottom="true">خلاصه مالی</MudText>
<MudGrid>
<MudItem xs="6">
<MudText Color="Color.Secondary">مبلغ کل:</MudText>
</MudItem>
<MudItem xs="6" Class="text-left">
<MudText>@Order.TotalPrice.ToString("N0") ریال</MudText>
</MudItem>
<MudItem xs="6">
<MudText Color="Color.Success">از کیف پول تخفیف:</MudText>
</MudItem>
<MudItem xs="6" Class="text-left">
<MudText Color="Color.Success">@Order.DiscountBalanceUsed.ToString("N0") ریال</MudText>
</MudItem>
<MudItem xs="12"><MudDivider /></MudItem>
<MudItem xs="6">
<MudText Typo="Typo.h6">مبلغ پرداختی از درگاه:</MudText>
</MudItem>
<MudItem xs="6" Class="text-left">
<MudText Typo="Typo.h6" Color="Color.Primary">
<strong>@Order.GatewayAmount.ToString("N0") ریال</strong>
</MudText>
</MudItem>
</MudGrid>
</MudPaper>
</MudItem>
<!-- ویرایش وضعیت ارسال -->
<MudItem xs="12">
<MudPaper Class="pa-4" Elevation="1">
<MudText Typo="Typo.subtitle2" GutterBottom="true">مدیریت وضعیت ارسال</MudText>
<MudDivider Class="mb-3" />
<MudGrid>
<MudItem xs="12" sm="6">
<MudSelect T="OrderStatus"
Label="وضعیت ارسال"
Dense="true"
Variant="Variant.Outlined"
@bind-Value="_newStatus">
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Pending">در انتظار پرداخت</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Paid">پرداخت شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Processing">در حال آماده‌سازی</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
<MudSelectItem T="OrderStatus" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6">
<MudTextField T="string"
Label="کد رهگیری"
Variant="Variant.Outlined"
Dense="true"
@bind-Value="_trackingCode" />
</MudItem>
<MudItem xs="12">
<MudTextField T="string"
Label="یادداشت ادمین"
Variant="Variant.Outlined"
Dense="true"
Lines="3"
HelperText="دلیل تغییر وضعیت یا توضیحات اضافی"
@bind-Value="_adminNotes" />
</MudItem>
</MudGrid>
@if (_newStatus == OrderStatus.Cancelled || _newStatus == OrderStatus.Returned)
{
<MudAlert Severity="Severity.Warning" Dense="true">
<MudAlert Severity="Severity.Warning" Class="mt-2">
توجه: در صورت لغو یا مرجوعی سفارش، موجودی محصولات به انبار بازگردانده می‌شود.
</MudAlert>
}
@if (_newStatus == OrderStatus.ReadyForOfficePickup)
{
<MudAlert Severity="Severity.Info" Dense="true">
سفارش برای تحویل حضوری در دفتر آماده است.
</MudAlert>
}
@if (_newStatus == OrderStatus.Shipped)
{
<MudAlert Severity="Severity.Info" Dense="true">
پس از تغییر وضعیت به «ارسال شده»، اطلاع‌رسانی به کاربر ارسال خواهد شد.
<MudAlert Severity="Severity.Info" Class="mt-2">
پس از تغییر وضعیت به "ارسال شده"، اطلاع‌رسانی به کاربر ارسال خواهد شد.
</MudAlert>
}
</MudStack>
</MudPaper>
<MudPaper Class="pa-3">
<MudStack Spacing="1">
<MudText Typo="Typo.subtitle2">آدرس تحویل</MudText>
<MudDivider />
@if (Order.Address != null && !string.IsNullOrWhiteSpace(Order.Address.Address))
{
@if (!string.IsNullOrWhiteSpace(Order.Address.Title))
{
<MudText Typo="Typo.body2"><strong>@Order.Address.Title</strong></MudText>
}
<MudText Typo="Typo.body2">@Order.Address.Address</MudText>
@if (!string.IsNullOrWhiteSpace(Order.Address.PostalCode))
{
<MudText Typo="Typo.body2">کد پستی: @Order.Address.PostalCode</MudText>
}
@if (!string.IsNullOrWhiteSpace(DisplayMobile))
{
<MudText Typo="Typo.body2">تلفن سفارش‌دهنده: @DisplayMobile</MudText>
}
}
else
{
<MudText Typo="Typo.caption">آدرسی ثبت نشده است.</MudText>
}
</MudStack>
</MudPaper>
<MudPaper Class="pa-3">
<MudStack Spacing="1">
<MudText Typo="Typo.subtitle2">اقلام فاکتور</MudText>
<MudDivider />
@if (Order.Items.Count > 0)
{
<MudTable Items="Order.Items" Dense="true">
<HeaderContent>
<MudTh>تصویر</MudTh>
<MudTh>محصول</MudTh>
<MudTh>قیمت واحد</MudTh>
<MudTh>تعداد</MudTh>
<MudTh>تخفیف</MudTh>
<MudTh>قیمت نهایی</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
@if (!string.IsNullOrWhiteSpace(context.ThumbnailPath))
{
<Image Src="@context.ThumbnailPath" Height="50" Width="50" />
}
</MudTd>
<MudTd>@context.ProductTitle</MudTd>
<MudTd>@context.UnitPrice.ToString("N0")</MudTd>
<MudTd>@context.Count</MudTd>
<MudTd>
@if (context.MaxDiscountPercent > 0)
{
<MudChip T="string" Size="Size.Small" Color="Color.Success">@context.MaxDiscountPercent%</MudChip>
}
else if (context.DiscountAmount > 0)
{
@context.DiscountAmount.ToString("N0")
}
else
{
<text>-</text>
}
</MudTd>
<MudTd>@context.FinalPrice.ToString("N0")</MudTd>
</RowTemplate>
</MudTable>
}
else
{
<MudText Typo="Typo.caption">آیتمی برای این سفارش ثبت نشده است.</MudText>
}
</MudStack>
</MudPaper>
@if (!string.IsNullOrEmpty(Order.Notes))
{
<MudPaper Class="pa-3">
<MudStack Spacing="1">
<MudText Typo="Typo.subtitle2">یادداشت کاربر</MudText>
<MudDivider />
<MudText Typo="Typo.body2">@Order.Notes</MudText>
</MudStack>
</MudPaper>
</MudItem>
<!-- یادداشت‌ها -->
@if (!string.IsNullOrEmpty(Order.Notes) || !string.IsNullOrEmpty(Order.AdminNotes))
{
<MudItem xs="12">
<MudPaper Class="pa-4" Elevation="1">
@if (!string.IsNullOrEmpty(Order.Notes))
{
<MudText Typo="Typo.h6" GutterBottom="true">
<MudIcon Icon="@Icons.Material.Filled.Comment" Size="Size.Small" />
یادداشت کاربر
</MudText>
<MudText Typo="Typo.body1" Class="mb-4">@Order.Notes</MudText>
}
@if (!string.IsNullOrEmpty(Order.AdminNotes))
{
<MudText Typo="Typo.h6" GutterBottom="true">
<MudIcon Icon="@Icons.Material.Filled.Note" Size="Size.Small" />
یادداشت ادمین
</MudText>
<MudText Typo="Typo.body1">@Order.AdminNotes</MudText>
}
</MudPaper>
</MudItem>
}
</MudStack>
</MudGrid>
</DialogContent>
<DialogActions>
<MudButton Variant="Variant.Outlined" Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.Print"
OnClick="PrintInvoiceAsync">
چاپ فاکتور
</MudButton>
<MudSpacer />
<MudButton Variant="Variant.Outlined" Color="Color.Default" OnClick="Close">
بستن
</MudButton>
@@ -1,7 +1,5 @@
using BackOffice.Common.Utilities;
using BackOffice.Services.DiscountOrder;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor;
namespace BackOffice.Pages.DiscountShop.Components;
@@ -10,7 +8,6 @@ public partial class OrderDetailsDialog
{
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!;
[Inject] public IDiscountOrderService OrderService { get; set; } = default!;
[Inject] private IJSRuntime JsRuntime { get; set; } = default!;
[Parameter] public DiscountOrderDetailsDto Order { get; set; } = null!;
@@ -20,9 +17,6 @@ public partial class OrderDetailsDialog
private bool _isSaving;
private List<TimelineStep> _timelineSteps = new();
private string? DisplayMobile =>
!string.IsNullOrWhiteSpace(Order.UserMobile) ? Order.UserMobile : Order.Address?.Phone;
protected override void OnInitialized()
{
_newStatus = Order.Status;
@@ -133,69 +127,34 @@ public partial class OrderDetailsDialog
private void Close() => MudDialog.Close();
private Color GetPaymentStatusColor(int status) => status switch
private Color GetStatusColor(OrderStatus status)
{
0 => Color.Warning,
1 => Color.Success,
2 => Color.Error,
3 => Color.Info,
_ => Color.Default
};
private string GetPaymentStatusText(int status) => status switch
{
0 => "در انتظار پرداخت",
1 => "پرداخت شده",
2 => "ناموفق",
3 => "بازپرداخت شده",
_ => "نامشخص"
};
private string GetPaymentStatusIcon(int status) => status switch
{
1 => Icons.Material.Filled.CheckCircle,
2 => Icons.Material.Filled.Cancel,
3 => Icons.Material.Filled.Replay,
_ => Icons.Material.Filled.Schedule
};
private async Task PrintInvoiceAsync()
{
var lines = Order.Items.Select(i => new OrderInvoicePrint.InvoiceLine
return status switch
{
Title = i.ProductTitle,
Quantity = i.Count,
UnitPrice = i.UnitPrice,
Discount = i.DiscountAmount,
LineTotal = i.FinalPrice > 0 ? i.FinalPrice : i.UnitPrice * i.Count
}).ToList();
OrderStatus.Pending => Color.Warning,
OrderStatus.Paid => Color.Info,
OrderStatus.Processing => Color.Primary,
OrderStatus.Shipped => Color.Secondary,
OrderStatus.Delivered => Color.Success,
OrderStatus.Cancelled => Color.Error,
OrderStatus.Returned => Color.Dark,
_ => Color.Default
};
}
var mobile = !string.IsNullOrWhiteSpace(Order.UserMobile)
? Order.UserMobile
: Order.Address?.Phone;
var html = OrderInvoicePrint.BuildHtml(new OrderInvoicePrint.InvoiceModel
private string GetStatusText(OrderStatus status)
{
return status switch
{
ShopTitle = "فروشگاه اعتباری — کارا بازار سلامت",
OrderId = string.IsNullOrWhiteSpace(Order.OrderNumber) ? Order.Id.ToString() : Order.OrderNumber,
CustomerName = Order.UserFullName,
CustomerMobile = mobile,
Address = Order.Address?.Address,
PostalCode = Order.Address?.PostalCode,
PaymentStatus = GetPaymentStatusText(Order.PaymentStatusValue),
TransactionId = Order.TransactionId,
TrackingCode = Order.TrackingCode,
Created = Order.Created,
Subtotal = lines.Sum(l => l.UnitPrice * l.Quantity),
DiscountTotal = lines.Sum(l => l.Discount),
CreditUsed = Order.DiscountBalanceUsed,
GatewayAmount = Order.GatewayAmount,
VatAmount = Order.VatAmount,
TotalAmount = Order.TotalPrice,
Lines = lines
});
await JsRuntime.InvokeVoidAsync("jsHtmlToPdf", html, $"فاکتور سفارش {Order.Id}");
OrderStatus.Pending => "در انتظار پرداخت",
OrderStatus.Paid => "پرداخت شده",
OrderStatus.Processing => "در حال آماده‌سازی",
OrderStatus.Shipped => "ارسال شده",
OrderStatus.Delivered => "تحویل داده شده",
OrderStatus.Cancelled => "لغو شده",
OrderStatus.Returned => "مرجوع شده",
_ => "نامشخص"
};
}
private enum StepState
@@ -70,7 +70,7 @@
Format="N0"
Immediate="true"
Disabled="_loading"
Label="قیمت (تومان) *"
Label="قیمت (ریال) *"
Required="true"
Variant="Variant.Outlined"
Margin="Margin.Dense" />
@@ -20,14 +20,14 @@
<MudList T="string" Dense="true">
<MudListItem T="string"
OnClick="@(() => OnTreeNodeSelected(null))"
Class="@(_selectedParentId == null ? "mud-selected-item" : "")">
Selected="@(_selectedParentId == null)">
<MudText Typo="Typo.body2">همه دسته‌بندی‌ها</MudText>
</MudListItem>
@foreach (var node in _treeNodes)
{
<MudListItem T="string"
OnClick="@(() => OnTreeNodeSelected(node.Id))"
Class="@(_selectedParentId == node.Id ? "mud-selected-item" : "")">
Selected="@(_selectedParentId == node.Id)">
<MudStack Row="true" AlignItems="AlignItems.Center">
<div style="width:@(node.Depth * 16)px;"></div>
<MudText Typo="Typo.body2">@node.Title</MudText>
@@ -23,11 +23,11 @@
@bind-Value="@_trackingCodeFilter" Variant="Variant.Outlined" Margin="Margin.Dense"/>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudDatePicker Label="از تاریخ" @bind-Date="_fromDateFilter"
<MudDatePicker T="DateTime?" Label="از تاریخ" @bind-Date="_fromDateFilter"
Variant="Variant.Outlined" Margin="Margin.Dense"/>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudDatePicker Label="تا تاریخ" @bind-Date="_toDateFilter"
<MudDatePicker T="DateTime?" Label="تا تاریخ" @bind-Date="_toDateFilter"
Variant="Variant.Outlined" Margin="Margin.Dense"/>
</MudItem>
<MudItem xs="12" sm="6" md="3">
@@ -49,8 +49,6 @@
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Shipped">ارسال شده</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Delivered">تحویل داده شده</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Cancelled">لغو شده</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.ReadyForOfficePickup">آماده تحویل در دفتر</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="@OrderStatus.Returned">مرجوع شده</MudSelectItem>
</MudSelect>
</MudItem>
</Filters>
@@ -99,20 +97,20 @@
<TemplateColumn Title="مبلغ کل">
<CellTemplate>
<MudText>@context.Item.TotalPrice.ToString("N0") تومان</MudText>
<MudText>@context.Item.TotalPrice.ToString("N0") ریال</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="کیف پول اعتباری">
<TemplateColumn Title="تخفیف کیف‌پول">
<CellTemplate>
<MudText Color="Color.Success">@context.Item.DiscountBalanceUsed.ToString("N0") تومان</MudText>
<MudText Color="Color.Success">@context.Item.DiscountBalanceUsed.ToString("N0") ریال</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="پرداخت درگاه">
<CellTemplate>
<MudText Typo="Typo.body2">
<strong>@context.Item.GatewayAmount.ToString("N0") تومان</strong>
<strong>@context.Item.GatewayAmount.ToString("N0") ریال</strong>
</MudText>
</CellTemplate>
</TemplateColumn>
@@ -1,6 +1,5 @@
using System.Text;
using BackOffice.Common.BaseComponents;
using BackOffice.Common.Utilities;
using BackOffice.Services.DiscountOrder;
using BackOffice.Pages.DiscountShop.Components;
using Microsoft.AspNetCore.Components;
@@ -75,15 +74,6 @@ public partial class DiscountOrdersMainPage
return;
}
// تکمیل فیلدهای فاکتور از ردیف لیست (جزئیات API نام/موبایل/VAT ندارد)
var listRow = _orders?.FirstOrDefault(o => o.Id == orderId);
if (listRow != null)
{
order.UserFullName = listRow.UserFullName;
order.UserMobile = listRow.UserMobile;
order.VatAmount = listRow.VatAmount;
}
var parameters = new DialogParameters<OrderDetailsDialog> { { x => x.Order, order } };
var options = new DialogOptions { CloseOnEscapeKey = true, MaxWidth = MaxWidth.Large, FullWidth = true };
var dialog = await DialogService.ShowAsync<OrderDetailsDialog>("جزئیات سفارش", parameters, options);
@@ -188,9 +178,8 @@ public partial class DiscountOrdersMainPage
OrderStatus.Processing => Color.Primary,
OrderStatus.Shipped => Color.Secondary,
OrderStatus.Delivered => Color.Success,
OrderStatus.Cancelled => Color.Dark,
OrderStatus.ReadyForOfficePickup => Color.Primary,
OrderStatus.Returned => Color.Error,
OrderStatus.Cancelled => Color.Error,
OrderStatus.Returned => Color.Dark,
_ => Color.Default
};
}
@@ -205,7 +194,6 @@ public partial class DiscountOrdersMainPage
OrderStatus.Shipped => "ارسال شده",
OrderStatus.Delivered => "تحویل داده شده",
OrderStatus.Cancelled => "لغو شده",
OrderStatus.ReadyForOfficePickup => "آماده تحویل در دفتر",
OrderStatus.Returned => "مرجوع شده",
_ => "نامشخص"
};
@@ -245,36 +233,44 @@ public partial class DiscountOrdersMainPage
try
{
var headers = new[]
{
"شناسه", "نام مشتری", "موبایل", "مبلغ کل", "کیف پول اعتباری", "پرداخت درگاه",
"تعداد آیتم", "وضعیت پرداخت", "وضعیت ارسال", "تاریخ پرداخت", "تاریخ ثبت", "آدرس"
};
var sb = new StringBuilder();
sb.AppendLine("شناسه,نام کاربر,موبایل,مبلغ کل,تخفیف کیف‌پول,پرداخت درگاه,تعداد آیتم,وضعیت پرداخت,وضعیت ارسال,تاریخ پرداخت,تاریخ ثبت,آدرس");
var rows = _orders.Select(o => new object?[]
foreach (var o in _orders)
{
o.Id,
CsvExportHelper.CustomerName(o.UserFullName, o.UserMobile, o.UserId),
o.UserMobile ?? "",
o.TotalPrice,
o.DiscountBalanceUsed,
o.GatewayAmount,
o.ItemsCount,
GetPaymentStatusText(o.PaymentStatusValue),
GetStatusText(o.Status),
o.PaymentDate?.MiladiToJalaliWithTime() ?? "-",
o.Created?.MiladiToJalaliWithTime() ?? "-",
o.ShippingAddress ?? "-"
});
sb.AppendLine(string.Join(",",
o.Id,
EscapeCsv(o.UserFullName),
EscapeCsv(o.UserMobile),
o.TotalPrice.ToString("N0"),
o.DiscountBalanceUsed.ToString("N0"),
o.GatewayAmount.ToString("N0"),
o.ItemsCount,
EscapeCsv(GetPaymentStatusText(o.PaymentStatusValue)),
EscapeCsv(GetStatusText(o.Status)),
o.PaymentDate?.MiladiToJalaliWithTime() ?? "-",
o.Created?.MiladiToJalaliWithTime() ?? "-",
EscapeCsv(o.ShippingAddress ?? "-")));
}
var filename = $"discount-orders-{DateTime.Now:yyyyMMddHHmmss}.xls";
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
CsvExportHelper.ToExcelXmlBase64("سفارش‌های اعتباری", headers, rows));
Snackbar.Add("خروجی Excel آماده شد.", Severity.Success);
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
var base64 = Convert.ToBase64String(bytes);
var filename = $"discount-orders-{DateTime.Now:yyyyMMddHHmmss}.csv";
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;
}
}
@@ -116,7 +116,7 @@
<TemplateColumn Title="قیمت" CellStyle="text-wrap: nowrap;">
<CellTemplate>
<MudText>@context.Item.Price.ToString("N0") تومان</MudText>
<MudText>@context.Item.Price.ToString("N0") ریال</MudText>
</CellTemplate>
</TemplateColumn>
@@ -48,7 +48,6 @@
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Processing">در حال پردازش</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Shipped">ارسال شده</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Delivered">تحویل شده</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="OrderStatus.ReadyForOfficePickup">آماده تحویل در دفتر</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Cancelled">لغو شده</MudSelectItem>
<MudSelectItem T="OrderStatus?" Value="OrderStatus.Returned">مرجوع شده</MudSelectItem>
</MudSelect>
@@ -114,19 +113,19 @@
<MudItem xs="12" md="3">
<MudPaper Elevation="1" Class="pa-4 text-center">
<MudText Typo="Typo.subtitle2" Color="Color.Success">مجموع فروش (مبلغ نهایی)</MudText>
<MudText Typo="Typo.h5">@_totalSales.ToString("N0") تومان</MudText>
<MudText Typo="Typo.h5">@_totalSales.ToString("N0") ریال</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" md="3">
<MudPaper Elevation="1" Class="pa-4 text-center">
<MudText Typo="Typo.subtitle2" Color="Color.Warning">مجموع تخفیف</MudText>
<MudText Typo="Typo.h5">@_totalDiscount.ToString("N0") تومان</MudText>
<MudText Typo="Typo.h5">@_totalDiscount.ToString("N0") ریال</MudText>
</MudPaper>
</MudItem>
<MudItem xs="12" md="3">
<MudPaper Elevation="1" Class="pa-4 text-center">
<MudText Typo="Typo.subtitle2" Color="Color.Info">میانگین مبلغ سفارش</MudText>
<MudText Typo="Typo.h5">@_averageOrder.ToString("N0") تومان</MudText>
<MudText Typo="Typo.h5">@_averageOrder.ToString("N0") ریال</MudText>
</MudPaper>
</MudItem>
</MudGrid>
@@ -243,12 +242,12 @@
</TemplateColumn>
<TemplateColumn Title="مبلغ نهایی">
<CellTemplate>
@context.Item.GatewayAmount.ToString("N0") تومان
@context.Item.GatewayAmount.ToString("N0") ریال
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="تخفیف">
<CellTemplate>
@context.Item.DiscountBalanceUsed.ToString("N0") تومان
@context.Item.DiscountBalanceUsed.ToString("N0") ریال
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="وضعیت">
@@ -499,8 +498,7 @@
OrderStatus.Processing => Color.Info,
OrderStatus.Shipped => Color.Info,
OrderStatus.Delivered => Color.Success,
OrderStatus.ReadyForOfficePickup => Color.Primary,
OrderStatus.Cancelled => Color.Dark,
OrderStatus.Cancelled => Color.Error,
OrderStatus.Returned => Color.Error,
_ => Color.Default
};
@@ -515,7 +513,6 @@
OrderStatus.Processing => "در حال پردازش",
OrderStatus.Shipped => "ارسال شده",
OrderStatus.Delivered => "تحویل شده",
OrderStatus.ReadyForOfficePickup => "آماده تحویل در دفتر",
OrderStatus.Cancelled => "لغو شده",
OrderStatus.Returned => "مرجوع شده",
_ => "نامشخص"
@@ -530,31 +527,28 @@
return;
}
var headers = new[]
{
"شناسه", "نام مشتری", "موبایل", "تاریخ ثبت", "تعداد آیتم", "مبلغ نهایی", "تخفیف", "وضعیت"
};
var sb = new StringBuilder();
sb.AppendLine("شناسه,نام کاربر,موبایل,تاریخ ثبت,تعداد آیتم,مبلغ نهایی,تخفیف,وضعیت");
var rows = _orders
.OrderBy(o => o.Created ?? o.PaymentDate ?? DateTime.MaxValue)
.Select(o => new object?[]
{
foreach (var o in _orders.Where(o => o.Created.HasValue).OrderBy(o => o.Created))
{
sb.AppendLine(string.Join(",",
o.Id,
CsvExportHelper.CustomerName(o.UserFullName, o.UserMobile, o.UserId),
o.UserMobile ?? "",
o.Created?.MiladiToJalaliWithTime()
?? o.PaymentDate?.MiladiToJalaliWithTime()
?? "-",
EscapeCsv(o.UserFullName),
EscapeCsv(o.UserMobile),
o.Created?.MiladiToJalaliWithTime() ?? "-",
o.ItemsCount,
o.GatewayAmount,
o.DiscountBalanceUsed,
GetStatusText(o.Status)
});
GetStatusText(o.Status)));
}
var filename = $"discount-sales-{DateTime.Now:yyyyMMddHHmmss}.xls";
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
CsvExportHelper.ToExcelXmlBase64("گزارش فروش اعتباری", headers, rows));
Snackbar.Add("خروجی Excel آماده شد.", Severity.Success);
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
var base64 = Convert.ToBase64String(bytes);
var filename = $"discount-sales-{DateTime.Now:yyyyMMddHHmmss}.csv";
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
Snackbar.Add("خروجی Excel (CSV) آماده شد.", Severity.Success);
}
private async Task ExportToPdf()
@@ -572,18 +566,18 @@
sb.AppendLine("خلاصه:");
sb.AppendLine($"تعداد سفارش‌ها: {_totalOrders}");
sb.AppendLine($"مجموع فروش (مبلغ نهایی): {_totalSales:N0} تومان");
sb.AppendLine($"مجموع تخفیف: {_totalDiscount:N0} تومان");
sb.AppendLine($"میانگین مبلغ سفارش: {_averageOrder:N0} تومان");
sb.AppendLine($"مجموع فروش (مبلغ نهایی): {_totalSales:N0} ریال");
sb.AppendLine($"مجموع تخفیف: {_totalDiscount:N0} ریال");
sb.AppendLine($"میانگین مبلغ سفارش: {_averageOrder:N0} ریال");
sb.AppendLine();
sb.AppendLine("جزئیات سفارش‌ها:");
sb.AppendLine("شناسه | نام مشتری | تاریخ | تعداد آیتم | مبلغ نهایی | تخفیف | وضعیت");
sb.AppendLine("شناسه | نام کاربر | تاریخ | تعداد آیتم | مبلغ نهایی | تخفیف | وضعیت");
foreach (var o in _orders.Where(o => o.Created.HasValue).OrderBy(o => o.Created))
{
sb.AppendLine(
$"{o.Id} | {CsvExportHelper.CustomerName(o.UserFullName, o.UserMobile, o.UserId)} | {o.Created?.MiladiToJalaliWithTime() ?? "-"} | {o.ItemsCount} | {o.GatewayAmount:N0} | {o.DiscountBalanceUsed:N0} | {GetStatusText(o.Status)}");
$"{o.Id} | {o.UserFullName} | {o.Created?.MiladiToJalaliWithTime() ?? "-"} | {o.ItemsCount} | {o.GatewayAmount:N0} | {o.DiscountBalanceUsed:N0} | {GetStatusText(o.Status)}");
}
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
@@ -593,4 +587,14 @@
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename, base64);
Snackbar.Add("خروجی PDF (نسخه متنی) آماده شد.", Severity.Success);
}
private static string EscapeCsv(string value)
{
if (string.IsNullOrEmpty(value))
return "";
var needsQuotes = value.Contains(',') || value.Contains('"') || value.Contains('\n');
var escaped = value.Replace("\"", "\"\"");
return needsQuotes ? $"\"{escaped}\"" : escaped;
}
}
+1 -1
View File
@@ -108,7 +108,7 @@
<MudStack Spacing="0">
<MudText Typo="Typo.caption" Color="Color.Secondary">مبلغ کل استخر</MudText>
<MudText Typo="Typo.h6" Color="Color.Primary">
@(_currentPool?.TotalPoolAmount.ToString("N0") ?? "0") تومان
@(_currentPool?.TotalPoolAmount.ToString("N0") ?? "0") ریال
</MudText>
</MudStack>
</MudItem>
@@ -1,7 +1,6 @@
@using CMSMicroservice.Protobuf.Protos.Inventory
@using Google.Protobuf.WellKnownTypes
@using InventoryProductType = CMSMicroservice.Protobuf.Protos.Inventory.ProductType
@using BackOffice.Pages.AutoComplete
<MudDialog>
<DialogContent>
@@ -20,16 +19,12 @@
</MudSelect>
</MudItem>
<MudItem xs="12">
@if (_productType == InventoryProductType.RegularProduct)
{
<ProductsAutoComplete @bind-Value="_productId"
Label="انتخاب محصول عادی" />
}
else
{
<DiscountProductsAutoComplete @bind-Value="_discountProductId"
Label="انتخاب محصول تخفیفی" />
}
<MudNumericField T="long" @bind-Value="_productId"
Label="شناسه محصول"
Variant="Variant.Outlined"
Required="true"
RequiredError="شناسه محصول الزامی است"
Min="1" />
</MudItem>
}
else
@@ -108,8 +103,7 @@
private bool _isSubmitting;
private InventoryProductType _productType = InventoryProductType.RegularProduct;
private long? _productId;
private long? _discountProductId;
private long _productId;
private long? _warehouseId;
private int _quantity = 1;
private string _referenceNumber = string.Empty;
@@ -117,12 +111,9 @@
protected override void OnInitialized()
{
if (ProductIdParam.HasValue && ProductTypeParam.HasValue)
if (ProductIdParam.HasValue)
{
if (ProductTypeParam.Value == InventoryProductType.DiscountProduct)
_discountProductId = ProductIdParam.Value;
else
_productId = ProductIdParam.Value;
_productId = ProductIdParam.Value;
}
if (ProductTypeParam.HasValue)
{
@@ -153,19 +144,9 @@
_isSubmitting = true;
try
{
var selectedProductId = ProductIdParam
?? (_productType == InventoryProductType.DiscountProduct ? _discountProductId : _productId);
if (!selectedProductId.HasValue || selectedProductId.Value <= 0)
{
Snackbar.Add("لطفاً یک محصول انتخاب کنید", Severity.Warning);
_isSubmitting = false;
return;
}
var request = new AddStockRequest
{
ProductId = selectedProductId.Value,
ProductId = ProductIdParam ?? _productId,
ProductType = ProductTypeParam ?? _productType,
Quantity = _quantity,
ReferenceNumber = _referenceNumber ?? string.Empty,
@@ -33,8 +33,10 @@
Min="0" />
</MudItem>
<MudItem xs="12" md="2">
<PackageSelect Label="پکیج"
@bind-SelectedPackageId="_filterPackageId" />
<MudNumericField @bind-Value="_maxBalance"
Label="حداکثر موجودی"
Variant="Variant.Outlined"
Min="0" />
</MudItem>
<MudItem xs="12" md="2" Class="d-flex align-center">
<MudButton Variant="Variant.Filled"
@@ -70,14 +72,6 @@
<PropertyColumn Property="x => x.UserFullName" Title="نام کاربر" />
<PropertyColumn Property="x => x.WeekDisplayName" Title="هفته" />
<PropertyColumn Property="x => x.PackageTitle" Title="پکیج">
<CellTemplate>
<MudChip T="string" Color="Color.Tertiary" Size="Size.Small">
@(string.IsNullOrEmpty(context.Item.PackageTitle) ? "-" : context.Item.PackageTitle)
</MudChip>
</CellTemplate>
</PropertyColumn>
<TemplateColumn Title="چپ (جدید + قبلی = مجموع)">
<CellTemplate>
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
@@ -135,7 +129,7 @@
</PropertyColumn>
<PropertyColumn Property="x => x.PoolContribution" Title="سهم از استخر">
<CellTemplate>
@context.Item.PoolContribution.ToString("N0") تومان
@context.Item.PoolContribution.ToString("N0") ریال
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.IsExpired" Title="وضعیت">
@@ -199,7 +193,6 @@
private MudDataGrid <UserWeeklyBalanceModel> _grid { get; set; }
private long? _filterUserId = null;
private long? _filterWeekDefinitionId = null;
private long? _filterPackageId = null;
private int? _minBalance = null;
private int? _maxBalance = null;
private bool _onlyActive = false;
@@ -229,11 +222,6 @@ private MudDataGrid <UserWeeklyBalanceModel> _grid { get; set; }
request.WeekDefinitionId = _filterWeekDefinitionId.Value;
}
if (_filterPackageId.HasValue && _filterPackageId.Value > 0)
{
request.PackageId = _filterPackageId.Value;
}
var response = await CommissionClient.GetUserWeeklyBalancesAsync(request);
var items = response.Models.Select(b => new UserWeeklyBalanceModel
@@ -250,8 +238,7 @@ private MudDataGrid <UserWeeklyBalanceModel> _grid { get; set; }
MatchedBalance = b.TotalBalances,
PoolContribution = b.WeeklyPoolContribution,
CalculatedAt = b.CalculatedAt?.ToDateTime(),
IsExpired = b.IsExpired,
PackageTitle = b.PackageTitle
IsExpired = b.IsExpired
}).ToList();
// Apply balance range filter if specified
@@ -321,6 +308,5 @@ private MudDataGrid <UserWeeklyBalanceModel> _grid { get; set; }
public long PoolContribution { get; set; }
public DateTime? CalculatedAt { get; set; }
public bool IsExpired { get; set; }
public string PackageTitle { get; set; } = string.Empty;
}
}
@@ -1,78 +0,0 @@
@using CMSMicroservice.Protobuf.Protos.NetworkMembership
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.SwapHoriz" Class="ml-1" />
تغییر والد در شبکه
</MudText>
</TitleContent>
<DialogContent>
<MudAlert Severity="Severity.Warning" Dense="true" Class="mb-4">
⚠️ این عملیات حساس است و ساختار درخت شبکه را تغییر می‌دهد.
تاریخچه تغییر ثبت خواهد شد.
</MudAlert>
<MudForm @ref="_form">
<MudStack Spacing="3">
<MudTextField T="string"
Label="کاربر فعلی"
Value="@UserDisplayName"
ReadOnly="true"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Person" />
<MudNumericField T="long"
@bind-Value="_newParentId"
Label="شناسه والد جدید"
Required="true"
RequiredError="شناسه والد جدید الزامی است"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.PersonSearch"
Min="1" />
<MudSelect T="int"
@bind-Value="_newLeg"
Label="ساق جدید"
Required="true"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.AccountTree">
<MudSelectItem T="int" Value="0">چپ (تیم اول)</MudSelectItem>
<MudSelectItem T="int" Value="1">راست (تیم دوم)</MudSelectItem>
</MudSelect>
<MudTextField T="string"
@bind-Value="_reason"
Label="دلیل تغییر"
Required="true"
RequiredError="دلیل تغییر الزامی است"
Variant="Variant.Outlined"
Lines="3"
Placeholder="دلیل جابجایی کاربر در شبکه را وارد کنید..."
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Description" />
</MudStack>
</MudForm>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel" Color="Color.Default">انصراف</MudButton>
<MudButton OnClick="Submit"
Color="Color.Error"
Variant="Variant.Filled"
Disabled="_isSubmitting"
StartIcon="@Icons.Material.Filled.SwapHoriz">
@if (_isSubmitting)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="ml-2" />
<span>در حال انجام...</span>
}
else
{
<span>تغییر والد</span>
}
</MudButton>
</DialogActions>
</MudDialog>
@@ -1,66 +0,0 @@
using CMSMicroservice.Protobuf.Protos.NetworkMembership;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace BackOffice.Pages.Network.Components;
public partial class ChangeParentDialog
{
[Parameter] public long UserId { get; set; }
[Parameter] public string UserDisplayName { get; set; } = "";
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!;
[Inject] public NetworkMembershipContract.NetworkMembershipContractClient NetworkContract { get; set; } = default!;
private MudForm _form = default!;
private long _newParentId;
private int _newLeg;
private string _reason = "";
private bool _isSubmitting;
private async Task Submit()
{
await _form.Validate();
if (!_form.IsValid)
return;
if (_newParentId == UserId)
{
Snackbar.Add("والد جدید نمی‌تواند خود کاربر باشد.", Severity.Error);
return;
}
_isSubmitting = true;
StateHasChanged();
try
{
var request = new ChangeNetworkParentRequest
{
UserId = UserId,
NewParentId = _newParentId,
NewLeg = _newLeg,
Reason = _reason
};
await NetworkContract.ChangeNetworkParentAsync(request);
Snackbar.Add("والد کاربر با موفقیت تغییر کرد.", Severity.Success);
MudDialog.Close(DialogResult.Ok(true));
}
catch (Grpc.Core.RpcException ex)
{
Snackbar.Add($"خطا: {ex.Status.Detail}", Severity.Error);
}
catch (Exception ex)
{
Snackbar.Add($"خطا در تغییر والد: {ex.Message}", Severity.Error);
}
finally
{
_isSubmitting = false;
StateHasChanged();
}
}
private void Cancel() => MudDialog.Cancel();
}
@@ -169,42 +169,6 @@
</CellTemplate>
</PropertyColumn>
<TemplateColumn Title="نوع فعالسازی" Sortable="false">
<CellTemplate>
@if (context.Item.IsNewActivation == null)
{
<span style="color: #bbb;">-</span>
}
else if (context.Item.IsNewActivation == true)
{
<MudChip T="string" Size="Size.Small" Color="Color.Success" Icon="@Icons.Material.Filled.FiberNew">
عضو جدید
</MudChip>
}
else
{
<MudChip T="string" Size="Size.Small" Color="Color.Secondary" Icon="@Icons.Material.Filled.Refresh">
خرید مجدد
</MudChip>
}
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="پکیج" Sortable="false">
<CellTemplate>
@if (!string.IsNullOrEmpty(context.Item.PackageName))
{
<MudChip T="string" Size="Size.Small" Color="Color.Dark">
@context.Item.PackageName
</MudChip>
}
else
{
<span style="color: #bbb;">-</span>
}
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="عملیات" Sortable="false">
<CellTemplate>
<MudButtonGroup Size="Size.Small" Variant="Variant.Text">
@@ -373,9 +337,7 @@
isClubActive = n.IsClubActive,
isActivatedInTargetWeek = n.IsActivatedInTargetWeek,
activationWeekDefinitionId = n.ActivationWeekDefinitionId,
clubActivatedAt = n.ClubActivatedAt?.ToDateTime().ToLocalTime().MiladiToJalali() ?? "",
isNewActivation = n.IsNewActivation,
packageName = n.PackageName ?? ""
clubActivatedAt = n.ClubActivatedAt?.ToDateTime().ToLocalTime().MiladiToJalali() ?? ""
}).ToArray();
var options = new { filterWeek = _activationWeekFilter };
@@ -254,7 +254,7 @@
<td><strong>کل کمیسیون کسب شده:</strong></td>
<td>
<MudText Typo="Typo.body1" Color="Color.Primary">
<strong>@_userInfo.TotalEarnedCommission.ToString("N0") تومان</strong>
<strong>@_userInfo.TotalEarnedCommission.ToString("N0") ریال</strong>
</MudText>
</td>
</tr>
@@ -262,7 +262,7 @@
<td><strong>کمیسیون پرداخت شده:</strong></td>
<td>
<MudText Typo="Typo.body1" Color="Color.Success">
@_userInfo.TotalPaidCommission.ToString("N0") تومان
@_userInfo.TotalPaidCommission.ToString("N0") ریال
</MudText>
</td>
</tr>
@@ -270,7 +270,7 @@
<td><strong>کمیسیون در انتظار:</strong></td>
<td>
<MudText Typo="Typo.body1" Color="Color.Warning">
@_userInfo.PendingCommission.ToString("N0") تومان
@_userInfo.PendingCommission.ToString("N0") ریال
</MudText>
</td>
</tr>
@@ -300,9 +300,9 @@
<MudSimpleTable Dense="true">
<tbody>
<tr>
<td><strong>خرید پکیج:</strong></td>
<td><strong>پکیج طلایی:</strong></td>
<td>
@if (_userInfo.HasPurchasedPackage)
@if (_userInfo.HasPurchasedGoldenPackage)
{
<MudChip T="string" Color="Color.Success" Size="Size.Small" Icon="@Icons.Material.Filled.CheckCircle">
خریداری شده
@@ -434,12 +434,6 @@
OnClick="@(() => NavigationManager.NavigateTo($"/commission/payouts?userId={UserId}"))">
پرداخت‌های کاربر
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Warning"
StartIcon="@Icons.Material.Filled.SwapHoriz"
OnClick="OpenChangeParentDialog">
تغییر والد
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.Refresh"
@@ -543,28 +537,4 @@
_ => "نامشخص"
};
}
private async Task OpenChangeParentDialog()
{
var displayName = $"{_userInfo.UserName} (ID: {UserId})";
var parameters = new DialogParameters<Network.Components.ChangeParentDialog>
{
{ x => x.UserId, UserId },
{ x => x.UserDisplayName, displayName }
};
var options = new DialogOptions
{
MaxWidth = MaxWidth.Small,
FullWidth = true,
CloseOnEscapeKey = true
};
var dialog = await DialogService.ShowAsync<Network.Components.ChangeParentDialog>("تغییر والد شبکه", parameters, options);
var result = await dialog.Result;
if (result is { Canceled: false })
{
await LoadUserInfo();
await LoadNetworkHistory();
}
}
}
@@ -1,5 +1,4 @@
@using CMSMicroservice.Protobuf.Protos.Package
@using CMSMicroservice.Protobuf.Protos.Configuration
@using BackOffice.Common.BaseComponents
@using Microsoft.AspNetCore.Components.Forms
@using Tizzani.MudBlazor.HtmlEditor
@@ -42,77 +41,12 @@
<MudTextField T="string" @bind-Value="Model.Title" Disabled="_isLoading" Label="عنوان" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="6">
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.Price" Format="N0" Immediate="true" Disabled="_isLoading" Label="قیمت (ریال)" Variant="Variant.Outlined" />
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.Price" Format="N0" Immediate="true" Disabled="_isLoading" Label="قیمت" Variant="Variant.Outlined" />
</MudItem>
</MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudItem xs="4">
<MudNumericField T="int" HideSpinButtons="true" @bind-Value="Model.SortOrder" Disabled="_isLoading" Label="ترتیب نمایش" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="4">
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.ActivationFee" Format="N0" Immediate="true" Disabled="_isLoading" Label="هزینه فعال‌سازی (ریال)" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="4">
<MudNumericField T="double" HideSpinButtons="true" @bind-Value="Model.DiscountMultiplier" Disabled="_isLoading" Label="ضریب تخفیف" Variant="Variant.Outlined" />
</MudItem>
</MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudItem xs="4">
<MudNumericField T="double" HideSpinButtons="true" @bind-Value="Model.MagicWalletMultiplier" Disabled="_isLoading" Label="ضریب کیف پول جادویی" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="4">
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.MagicWalletMaxDeposit" Format="N0" Immediate="true" Disabled="_isLoading" Label="سقف واریز جادویی" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="4">
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.MagicWalletMaxCredit" Format="N0" Immediate="true" Disabled="_isLoading" Label="سقف اعتبار جادویی" Variant="Variant.Outlined" />
</MudItem>
</MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudItem xs="6">
<MudNumericField T="int" HideSpinButtons="true" @bind-Value="Model.MaxBalancesPerLeg" Disabled="_isLoading" Label="حداکثر تعادل هر پا" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="6">
<MudNumericField T="int" HideSpinButtons="true" @bind-Value="Model.MaxNetworkLevel" Disabled="_isLoading" Label="حداکثر سطح شبکه" Variant="Variant.Outlined" />
</MudItem>
</MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudItem xs="3">
<MudCheckBox T="bool" @bind-Value="Model.IsActive" Disabled="_isLoading" Label="فعال" Color="Color.Success" />
</MudItem>
<MudItem xs="3">
<MudCheckBox T="bool" @bind-Value="Model.IsBasePackage" Disabled="_isLoading" Label="پکیج پایه" Color="Color.Primary" />
</MudItem>
<MudItem xs="3">
<MudCheckBox T="bool" @bind-Value="Model.SupportsDirectPurchase" Disabled="_isLoading" Label="پرداخت مستقیم" Color="Color.Info" />
</MudItem>
<MudItem xs="3">
<MudCheckBox T="bool" @bind-Value="Model.SupportsDayaPurchase" Disabled="_isLoading" Label="اعتبار دایا" Color="Color.Warning" />
</MudItem>
</MudStack>
@if (_features.Any())
{
<MudText Typo="Typo.subtitle1" Class="mt-4 mb-2"><strong>ویژگی‌های پکیج</strong></MudText>
<MudStack Row="true" Wrap="Wrap.Wrap">
@foreach (var feature in _features)
{
<MudCheckBox T="bool"
Value="@(_selectedFeatureIds.Contains(feature.Id))"
ValueChanged="@((bool v) => ToggleFeature(feature.Id, v))"
Disabled="_isLoading"
Label="@feature.Title"
Color="Color.Primary" />
}
</MudStack>
}
<MudItem xs="12">
<MudHtmlEditor @bind-Html="Model.Description" MinHeight="300px">
<MudHtmlToolbarOptions InsertImage="false" />
<MudHtmlToolbarOptions InsertImage="false" /> <!-- This will exclude the "insert image" toolbar option -->
</MudHtmlEditor>
</MudItem>
File diff suppressed because one or more lines are too long
@@ -1,5 +1,4 @@
@using CMSMicroservice.Protobuf.Protos.Package
@using CMSMicroservice.Protobuf.Protos.Configuration
@using BackOffice.Common.BaseComponents
@using Microsoft.AspNetCore.Components.Forms
@using Tizzani.MudBlazor.HtmlEditor
@@ -43,77 +42,12 @@
<MudTextField T="string" @bind-Value="Model.Title" Disabled="_isLoading" Label="عنوان" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="6">
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.Price" Disabled="_isLoading" Label="قیمت (ریال)" Variant="Variant.Outlined" />
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.Price" Disabled="_isLoading" Label="قیمت" Variant="Variant.Outlined" />
</MudItem>
</MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudItem xs="4">
<MudNumericField T="int" HideSpinButtons="true" @bind-Value="Model.SortOrder" Disabled="_isLoading" Label="ترتیب نمایش" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="4">
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.ActivationFee" Format="N0" Immediate="true" Disabled="_isLoading" Label="هزینه فعال‌سازی (ریال)" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="4">
<MudNumericField T="double" HideSpinButtons="true" @bind-Value="Model.DiscountMultiplier" Disabled="_isLoading" Label="ضریب تخفیف" Variant="Variant.Outlined" />
</MudItem>
</MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudItem xs="4">
<MudNumericField T="double" HideSpinButtons="true" @bind-Value="Model.MagicWalletMultiplier" Disabled="_isLoading" Label="ضریب کیف پول جادویی" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="4">
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.MagicWalletMaxDeposit" Format="N0" Immediate="true" Disabled="_isLoading" Label="سقف واریز جادویی" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="4">
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.MagicWalletMaxCredit" Format="N0" Immediate="true" Disabled="_isLoading" Label="سقف اعتبار جادویی" Variant="Variant.Outlined" />
</MudItem>
</MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudItem xs="6">
<MudNumericField T="int" HideSpinButtons="true" @bind-Value="Model.MaxBalancesPerLeg" Disabled="_isLoading" Label="حداکثر تعادل هر پا" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="6">
<MudNumericField T="int" HideSpinButtons="true" @bind-Value="Model.MaxNetworkLevel" Disabled="_isLoading" Label="حداکثر سطح شبکه" Variant="Variant.Outlined" />
</MudItem>
</MudStack>
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudItem xs="3">
<MudCheckBox T="bool" @bind-Value="Model.IsActive" Disabled="_isLoading" Label="فعال" Color="Color.Success" />
</MudItem>
<MudItem xs="3">
<MudCheckBox T="bool" @bind-Value="Model.IsBasePackage" Disabled="_isLoading" Label="پکیج پایه" Color="Color.Primary" />
</MudItem>
<MudItem xs="3">
<MudCheckBox T="bool" @bind-Value="Model.SupportsDirectPurchase" Disabled="_isLoading" Label="پرداخت مستقیم" Color="Color.Info" />
</MudItem>
<MudItem xs="3">
<MudCheckBox T="bool" @bind-Value="Model.SupportsDayaPurchase" Disabled="_isLoading" Label="اعتبار دایا" Color="Color.Warning" />
</MudItem>
</MudStack>
@if (_features.Any())
{
<MudText Typo="Typo.subtitle1" Class="mt-4 mb-2"><strong>ویژگی‌های پکیج</strong></MudText>
<MudStack Row="true" Wrap="Wrap.Wrap">
@foreach (var feature in _features)
{
<MudCheckBox T="bool"
Value="@(_selectedFeatureIds.Contains(feature.Id))"
ValueChanged="@((bool v) => ToggleFeature(feature.Id, v))"
Disabled="_isLoading"
Label="@feature.Title"
Color="Color.Primary" />
}
</MudStack>
}
<MudItem xs="12">
<MudHtmlEditor @bind-Html="Model.Description" MinHeight="300px">
<MudHtmlToolbarOptions InsertImage="false" />
<MudHtmlToolbarOptions InsertImage="false" /> <!-- This will exclude the "insert image" toolbar option -->
</MudHtmlEditor>
</MudItem>
</MudStack>
File diff suppressed because one or more lines are too long
@@ -12,21 +12,13 @@
</MudItem>
</Filters>
<Content>
@* ── G8: راهنمای مدیریت پکیج (Q28) ── *@
<MudAlert Severity="Severity.Warning" Variant="Variant.Text" Dense="true" Class="mb-3" Icon="@Icons.Material.Filled.Warning">
تغییر قیمت روی کاربران فعلی اثر ندارد — فقط خریدهای جدید. هر پکیج Pool پورسانت مستقل دارد.
</MudAlert>
<MudDataGrid T="DataModel" ServerData="@(new Func<GridState<DataModel>, Task<GridData<DataModel>>>(ServerReload))"
Hover="true" @ref="_gridData" Height="calc(100vh - 240px)">
<ColGroup>
<col />
<col />
<col />
<col />
<col />
<col />
<col />
<col style="width: 80px;" />
<col style="width: 58px;" />
</ColGroup>
<ToolBarContent>
<MudText>مدیریت پکیج</MudText>
@@ -59,42 +51,31 @@
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.Price" Title="قیمت" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
<PropertyColumn Property="x => x.Description" Title="توضیحات" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
<CellTemplate>
<MudText Typo="Typo.inherit">@context.Item.Price.ToString("N0")</MudText>
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.SortOrder" Title="ترتیب" />
<PropertyColumn Property="x => x.IsActive" Title="وضعیت" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
<CellTemplate>
@if (context.Item.IsActive)
@if (string.IsNullOrWhiteSpace(context.Item.Description))
{
<MudChip T="string" Color="Color.Success" Variant="Variant.Filled" Size="Size.Small">فعال</MudChip>
<MudText Typo="Typo.inherit">-</MudText>
}
else
{
<MudChip T="string" Color="Color.Default" Variant="Variant.Filled" Size="Size.Small">غیرفعال</MudChip>
}
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.IsBasePackage" Title="نوع" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
<CellTemplate>
@if (context.Item.IsBasePackage)
{
<MudChip T="string" Color="Color.Primary" Variant="Variant.Filled" Size="Size.Small">پایه</MudChip>
}
else
{
<MudChip T="string" Color="Color.Info" Variant="Variant.Outlined" Size="Size.Small">عادی</MudChip>
}
</CellTemplate>
</PropertyColumn>
<TemplateColumn Title="">
<CellTemplate>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudTooltip Text="ویرایش">
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Small" ButtonType="ButtonType.Button" OnClick="@(() => Update(context.Item))" />
<MudTooltip Text="@(context.Item.Description.HtmlToText())" Arrow="true" Style="@($"{(context.Item.Description.Length < 70 ? string.Empty : "width:600px;")}")">
<MudText Typo="Typo.inherit" Style="text-wrap: nowrap;">
@(context.Item.Description.HtmlToText().Truncate(20, true))
</MudText>
</MudTooltip>
}
</CellTemplate>
</PropertyColumn>
<TemplateColumn StickyLeft="true" Title="عملیات" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
<CellTemplate>
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudTooltip Text="ویرایش">
<MudIconButton Icon="@Icons.Material.Filled.EditNote" Size="Size.Small" ButtonType="ButtonType.Button" OnClick="@(() => Update(context.Item.Adapt<DataModel>()))" />
</MudTooltip>
<MudTooltip Text="آرشیو">
<MudIconButton Icon="@Icons.Material.Filled.DeleteOutline" Size="Size.Small" ButtonType="ButtonType.Button" OnClick="@(() => OnDelete(context.Item))" />
</MudTooltip>
@@ -33,7 +33,7 @@ public partial class PackageMainPage
{
var parameters = new DialogParameters<UpdateDialog> { { x => x.Model, model.Adapt<UpdatePackageRequest>() } };
var dialog = await DialogService.ShowAsync<UpdateDialog>($"ویرایش پکیج", parameters, new DialogOptions() { CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.Medium });
var dialog = await DialogService.ShowAsync<UpdateDialog>($"ویرایش پکیج", parameters, new DialogOptions() { CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.Small });
var result = await dialog.Result;
if (!result.Canceled)
@@ -70,7 +70,7 @@ public partial class PackageMainPage
public async Task CreateNew()
{
var dialog = await DialogService.ShowAsync<CreateDialog>($"افزودن پکیج", new DialogParameters<CreateDialog>() { { x => x.Model, new CreateNewPackageRequest() { IsActive = true, SupportsDirectPurchase = true, DiscountMultiplier = 2.0, MagicWalletMultiplier = 2.5, MaxBalancesPerLeg = 300, MaxNetworkLevel = 15, MagicWalletMaxDeposit = 1_000_000_000, MagicWalletMaxCredit = 2_500_000_000 } } }, new DialogOptions() { CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.Medium });
var dialog = await DialogService.ShowAsync<CreateDialog>($"افزودن پکیج", new DialogParameters<CreateDialog>() { { x => x.Model, new CreateNewPackageRequest() } }, new DialogOptions() { CloseButton = true, FullWidth = true, MaxWidth = MaxWidth.Small });
var result = await dialog.Result;
if (!result.Canceled)
{
@@ -1,106 +0,0 @@
@using BackOffice.Pages.AutoComplete
@using CMSMicroservice.Protobuf.Protos.UserWallet
@inject UserWalletContract.UserWalletContractClient WalletClient
<MudDialog>
<DialogContent>
<MudStack Spacing="3">
<MudAlert Severity="Severity.Warning" Dense="true" Variant="Variant.Outlined">
مبلغ مستقیماً به کیف پول اصلی (Balance) کاربر اضافه می‌شود و در دفتر مالی با نوع «شارژ دستی کیف اصلی» ثبت می‌گردد.
</MudAlert>
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_userId" />
<MudNumericField T="long" @bind-Value="_amount"
Label="مبلغ (تومان)"
Variant="Variant.Outlined"
Min="10000"
HelperText="حداقل ۱۰,۰۰۰ تومان" />
<MudTextField T="string" @bind-Value="_note"
Label="توضیحات ادمین (اختیاری)"
Variant="Variant.Outlined"
Lines="2" />
</MudStack>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel" Disabled="_isSubmitting">انصراف</MudButton>
<MudButton Color="Color.Primary" Variant="Variant.Filled"
OnClick="Submit" Disabled="_isSubmitting || !_userId.HasValue || _amount < 10000">
@if (_isSubmitting)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
}
ثبت شارژ
</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!;
private long? _userId;
private long _amount = 10000;
private string? _note;
private bool _isSubmitting;
private void Cancel() => MudDialog.Cancel();
private async Task Submit()
{
if (!_userId.HasValue || _userId.Value <= 0)
{
Snackbar.Add("لطفاً کاربر را انتخاب کنید", Severity.Warning);
return;
}
if (_amount < 10000)
{
Snackbar.Add("حداقل مبلغ شارژ ۱۰,۰۰۰ تومان است", Severity.Warning);
return;
}
bool? confirm = await DialogService.ShowMessageBox(
"تأیید شارژ دستی",
$"آیا از شارژ {_amount:N0} تومان به کیف پول اصلی کاربر #{_userId.Value} مطمئن هستید؟",
yesText: "بله، شارژ شود", cancelText: "لغو");
if (confirm != true)
return;
_isSubmitting = true;
try
{
var request = new AdminManualCreditChargeRequest
{
UserId = _userId.Value,
Amount = _amount
};
if (!string.IsNullOrWhiteSpace(_note))
request.Note = _note.Trim();
var response = await WalletClient.AdminManualCreditChargeAsync(request);
if (response.Success)
{
Snackbar.Add(
$"شارژ موفق — تراکنش #{response.TransactionId}، موجودی جدید: {response.NewBalance:N0}",
Severity.Success);
MudDialog.Close(DialogResult.Ok(true));
}
else
{
Snackbar.Add(response.Message ?? "شارژ ناموفق بود", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
}
finally
{
_isSubmitting = false;
}
}
}
@@ -1,5 +1,4 @@
@using CMSMicroservice.Protobuf.Protos.ManualPayment
@using CMSMicroservice.Protobuf.Protos.Package
@using BackOffice.Pages.Payment.Components
@using BackOffice.Pages.AutoComplete
@using Microsoft.AspNetCore.Components.Forms
@@ -14,36 +13,21 @@
<UserAutoComplete Label="انتخاب کاربر"
@bind-SelectedUserId="_selectedUserId" />
<MudSelect T="long?"
Label="انتخاب پکیج"
Value="@_selectedPackageId"
ValueChanged="OnPackageChanged"
Variant="Variant.Outlined"
Margin="Margin.Dense"
Required="true"
RequiredError="انتخاب پکیج الزامی است">
@foreach (var pkg in _packages)
{
<MudSelectItem T="long?" Value="@((long?)pkg.Id)">
@pkg.Title — @pkg.Price.ToString("N0") تومان
</MudSelectItem>
}
</MudSelect>
@if (_selectedPackagePrice.HasValue)
{
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text">
مبلغ پکیج: <strong>@_selectedPackagePrice.Value.ToString("N0") تومان</strong> — این مبلغ به کیف‌پول کاربر اضافه می‌شود.
</MudAlert>
}
<MudNumericField T="long"
Label="مبلغ"
Variant="Variant.Outlined"
Adornment="Adornment.End"
AdornmentText="تومان"
Disabled="true"
@bind-Value="_createModel.Amount" />
<MudSelect T="int"
Label="نوع پرداخت"
Variant="Variant.Outlined"
@bind-Value="_selectedType">
<MudSelectItem T="int" Value="1">واریز نقدی</MudSelectItem>
<MudSelectItem T="int" Value="2">شارژ کیف پول اعتباری</MudSelectItem>
<MudSelectItem T="int" Value="3">شارژ پاداش های دریافتی</MudSelectItem>
<MudSelectItem T="int" Value="2">شارژ کیف پول تخفیف</MudSelectItem>
<MudSelectItem T="int" Value="3">شارژ کیف پول شبکه</MudSelectItem>
<MudSelectItem T="int" Value="4">تسویه حساب</MudSelectItem>
<MudSelectItem T="int" Value="5">اصلاح خطا</MudSelectItem>
<MudSelectItem T="int" Value="6">بازپرداخت</MudSelectItem>
@@ -99,7 +83,7 @@
<MudText Typo="Typo.h6">جزئیات فعالسازی دستی</MudText>
<MudText Typo="Typo.body2">شناسه: @Model.Id</MudText>
<MudText Typo="Typo.body2">کاربر: @Model.UserFullName (@Model.UserId)</MudText>
<MudText Typo="Typo.body2">مبلغ: @Model.Amount.ToString("N0") تومان</MudText>
<MudText Typo="Typo.body2">مبلغ: @Model.Amount.ToString("N0") ریال</MudText>
<MudText Typo="Typo.body2">نوع: @Model.TypeDisplay</MudText>
<MudText Typo="Typo.body2">وضعیت: @Model.StatusDisplay</MudText>
@@ -1,5 +1,4 @@
using CMSMicroservice.Protobuf.Protos.ManualPayment;
using CMSMicroservice.Protobuf.Protos.Package;
using Google.Protobuf;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
@@ -25,23 +24,19 @@ public partial class ManualPaymentDialog
{
[CascadingParameter] IMudDialogInstance MudDialog { get; set; } = default!;
[Inject] public ManualPaymentContract.ManualPaymentContractClient ManualPaymentClient { get; set; } = default!;
[Inject] public PackageContract.PackageContractClient PackageClient { get; set; } = default!;
// Snackbar is injected via _Imports.razor
[Parameter] public ManualPaymentDialogMode Mode { get; set; }
[Parameter] public ManualPaymentModel? Model { get; set; }
private ManualPaymentModel _createModel = new(){ Amount = 0 };
private ManualPaymentModel _createModel = new(){
Amount = 56000000,
};
private string? _adminNote;
// User selection
private long? _selectedUserId;
// Package selection
private long? _selectedPackageId;
private long? _selectedPackagePrice;
private List<GetAllPackageByFilterResponseModel> _packages = new();
// Type and Description
private int _selectedType = 1; // Default: CashDeposit
private string _description = "عضویت دستی باشگاه مشتریان";
@@ -53,46 +48,6 @@ public partial class ManualPaymentDialog
private bool _isSubmitting;
protected override async Task OnInitializedAsync()
{
await LoadPackages();
}
private async Task LoadPackages()
{
try
{
var request = new GetAllPackageByFilterRequest
{
PaginationState = new() { PageNumber = 1, PageSize = 100 },
Filter = new()
};
var result = await PackageClient.GetAllPackageByFilterAsync(request);
_packages = result.Models?.OrderBy(p => p.SortOrder).ToList()
?? new List<GetAllPackageByFilterResponseModel>();
// اگر پکیج پایه وجود دارد، پیش‌فرض انتخاب کن
var basePackage = _packages.FirstOrDefault(p => p.IsBasePackage);
if (basePackage != null)
{
_selectedPackageId = basePackage.Id;
_selectedPackagePrice = basePackage.Price;
}
}
catch
{
_packages = new List<GetAllPackageByFilterResponseModel>();
}
}
private void OnPackageChanged(long? packageId)
{
_selectedPackageId = packageId;
var pkg = _packages.FirstOrDefault(p => p.Id == packageId);
_selectedPackagePrice = pkg?.Price;
}
private async Task OnImageFileSelected(IBrowserFile? file)
{
if (file == null)
@@ -126,12 +81,6 @@ public partial class ManualPaymentDialog
return;
}
if (!_selectedPackageId.HasValue || _selectedPackageId.Value <= 0)
{
Snackbar.Add("لطفاً پکیج را انتخاب کنید.", Severity.Warning);
return;
}
if (_selectedType <= 0)
{
Snackbar.Add("لطفاً نوع پرداخت را انتخاب کنید.", Severity.Warning);
@@ -152,8 +101,7 @@ public partial class ManualPaymentDialog
var request = new CreateManualPaymentRequest
{
UserId = _selectedUserId.Value,
Amount = 0, // deprecated — مبلغ از پکیج خوانده می‌شود
PackageId = _selectedPackageId.Value,
Amount = _createModel.Amount,
Type = (ManualPaymentType)_selectedType,
Description = _description
};
@@ -10,7 +10,7 @@
<MudText Typo="Typo.h6">جزئیات تراکنش</MudText>
<MudText Typo="Typo.body2">شناسه: @Model.Id</MudText>
<MudText Typo="Typo.body2">RefId: @Model.RefId</MudText>
<MudText Typo="Typo.body2">مبلغ: @Model.Amount.ToString("N0") تومان</MudText>
<MudText Typo="Typo.body2">مبلغ: @Model.Amount.ToString("N0") ریال</MudText>
<MudText Typo="Typo.body2">وضعیت: @StatusText</MudText>
<MudText Typo="Typo.body2">تاریخ پرداخت: @Model.PaymentDate.ToLocalTime().MiladiToJalaliWithTime()</MudText>
@@ -1,247 +0,0 @@
@page "/payment/ledger-transactions"
@using BackOffice.Common.BaseComponents
@using CMSMicroservice.Protobuf.Protos
@using CMSMicroservice.Protobuf.Protos.Transactions
@using Google.Protobuf.WellKnownTypes
@inject TransactionsContract.TransactionsContractClient TransactionsClient
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
<Filters>
<MudItem xs="12">
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true">
گزارش read-only دفتر تراکنش‌های مالی (جدول Transactions). برای جزئیات درگاه پرداخت به «گزارش تراکنش درگاه» مراجعه کنید.
</MudAlert>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudTextField T="long?" Label="شناسه" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_id" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudTextField T="string" Label="RefId" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_refId" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudTextField T="string" Label="توضیحات" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_description" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="PaymentStatus?" Clearable="true" Label="وضعیت پرداخت" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_paymentStatus">
<MudSelectItem T="PaymentStatus?" Value="PaymentStatus.Success">موفق</MudSelectItem>
<MudSelectItem T="PaymentStatus?" Value="PaymentStatus.Pending">در انتظار</MudSelectItem>
<MudSelectItem T="PaymentStatus?" Value="PaymentStatus.Reject">رد شده</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="TransactionType?" Clearable="true" Label="نوع تراکنش" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_type">
<MudSelectItem T="TransactionType?" Value="TransactionType.Buy">خرید</MudSelectItem>
<MudSelectItem T="TransactionType?" Value="TransactionType.DepositIpg">واریز درگاه</MudSelectItem>
<MudSelectItem T="TransactionType?" Value="TransactionType.DepositExternal1">واریز خارجی</MudSelectItem>
<MudSelectItem T="TransactionType?" Value="TransactionType.Withdraw">برداشت</MudSelectItem>
<MudSelectItem T="TransactionType?" Value="TransactionType.NetworkCommission">کمیسیون شبکه</MudSelectItem>
<MudSelectItem T="TransactionType?" Value="TransactionType.ClubActivation">فعال‌سازی باشگاه</MudSelectItem>
<MudSelectItem T="TransactionType?" Value="TransactionType.DiscountWalletCharge">شارژ کیف تخفیف</MudSelectItem>
<MudSelectItem T="TransactionType?" Value="TransactionType.DiscountShopPurchase">خرید فروشگاه تخفیف</MudSelectItem>
<MudSelectItem T="TransactionType?" Value="TransactionType.MagicWalletDeposit">واریز کیف جادویی</MudSelectItem>
<MudSelectItem T="TransactionType?" Value="TransactionType.MagicWalletBonus">بونوس کیف جادویی</MudSelectItem>
<MudSelectItem T="TransactionType?" Value="TransactionType.CreditWalletCharge">شارژ کیف اصلی</MudSelectItem>
<MudSelectItem T="TransactionType?" Value="TransactionType.ManualCreditWalletCharge">شارژ دستی کیف اصلی</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<DateRangePicker @bind-From="_createdFrom" @bind-To="_createdTo" Label="بازه ثبت" />
</MudItem>
</Filters>
<Content>
<MudGrid Class="mb-3" Spacing="2">
<MudItem xs="6" sm="3">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">تعداد</MudText>
<MudText Typo="Typo.h6">@_summaryTotalCount.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6" sm="3">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">جمع مبلغ (تومان)</MudText>
<MudText Typo="Typo.h6">@_summaryTotalAmount.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6" sm="2">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">موفق</MudText>
<MudText Typo="Typo.h6" Color="Color.Success">@_summarySuccess.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6" sm="2">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">در انتظار</MudText>
<MudText Typo="Typo.h6" Color="Color.Warning">@_summaryPending.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6" sm="2">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">رد شده</MudText>
<MudText Typo="Typo.h6" Color="Color.Error">@_summaryReject.ToString("N0")</MudText>
</MudPaper>
</MudItem>
</MudGrid>
<MudDataGrid @ref="_dataGrid" T="GetAllTransactionsByFilterResponseModel"
ServerData="LoadData" ReadOnly="true"
Hover="true" Dense="true" Height="calc(100vh - 400px)">
<ToolBarContent>
<MudText Typo="Typo.subtitle1">دفتر تراکنش‌های مالی</MudText>
<MudSpacer />
<MudButton Variant="Variant.Outlined" Color="Color.Info" Size="Size.Small"
StartIcon="@Icons.Material.Filled.FileDownload" OnClick="ExportCsv">CSV</MudButton>
</ToolBarContent>
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<TemplateColumn Title="نوع">
<CellTemplate>
<MudChip T="string" Size="Size.Small" Variant="Variant.Text">
@ReportLabels.GetTransactionTypeLabel(context.Item.HasType ? context.Item.Type : null)
</MudChip>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="مبلغ (تومان)">
<CellTemplate>@context.Item.Amount.ToString("N0")</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.Description" Title="توضیحات" />
<TemplateColumn Title="وضعیت">
<CellTemplate>
<MudChip T="string" Size="Size.Small" Color="@ReportLabels.GetTransactionPaymentStatusColor(context.Item.HasPaymentStatus ? context.Item.PaymentStatus : null)">
@ReportLabels.GetTransactionPaymentStatusLabel(context.Item.HasPaymentStatus ? context.Item.PaymentStatus : null)
</MudChip>
</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.RefId" Title="RefId" />
<TemplateColumn Title="تاریخ پرداخت">
<CellTemplate>
@(context.Item.PaymentDate != null
? context.Item.PaymentDate.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()
: "—")
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="تاریخ ثبت">
<CellTemplate>@context.Item.Created.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
</TemplateColumn>
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">
با فیلتر فعلی موردی یافت نشد — بازه تاریخ یا فیلترها را تغییر دهید.
</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="GetAllTransactionsByFilterResponseModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد در صفحه" />
</PagerContent>
</MudDataGrid>
</Content>
</BasePageComponent>
@code {
private BasePageComponent? _basePage;
private MudDataGrid<GetAllTransactionsByFilterResponseModel>? _dataGrid;
private long? _id;
private string? _refId, _description;
private PaymentStatus? _paymentStatus;
private TransactionType? _type;
private DateTime? _createdFrom, _createdTo;
private long _summaryTotalCount, _summaryTotalAmount, _summarySuccess, _summaryPending, _summaryReject;
private GetAllTransactionsByFilterFilter BuildFilter()
{
var filter = new GetAllTransactionsByFilterFilter
{
Id = _id > 0 ? _id : null,
RefId = string.IsNullOrWhiteSpace(_refId) ? null : _refId,
Description = string.IsNullOrWhiteSpace(_description) ? null : _description,
CreatedFrom = _createdFrom.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_createdFrom.Value, DateTimeKind.Utc)) : null,
CreatedTo = _createdTo.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_createdTo.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc)) : null
};
if (_paymentStatus.HasValue)
filter.PaymentStatus = _paymentStatus.Value;
else
filter.ClearPaymentStatusItem();
if (_type.HasValue)
filter.Type = _type.Value;
else
filter.ClearTypeItem();
return filter;
}
private async Task LoadSummary()
{
try
{
var s = await TransactionsClient.GetTransactionSummaryAsync(new GetTransactionSummaryRequest { Filter = BuildFilter() });
_summaryTotalCount = s.TotalCount;
_summaryTotalAmount = s.TotalAmount;
_summarySuccess = s.SuccessCount;
_summaryPending = s.PendingCount;
_summaryReject = s.RejectCount;
}
catch { }
}
private async Task<GridData<GetAllTransactionsByFilterResponseModel>> LoadData(GridState<GetAllTransactionsByFilterResponseModel> state)
{
try
{
await LoadSummary();
var result = await TransactionsClient.GetAllTransactionsByFilterAsync(new GetAllTransactionsByFilterRequest
{
PaginationState = new PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize },
SortBy = "-Created",
Filter = BuildFilter()
});
return new GridData<GetAllTransactionsByFilterResponseModel>
{
Items = result?.Models?.ToList() ?? new(),
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
return new GridData<GetAllTransactionsByFilterResponseModel>();
}
}
private async Task OnFilterSubmit() { if (_dataGrid != null) await _dataGrid.ReloadServerData(); }
private async Task OnFilterCleared()
{
_id = null; _refId = _description = null;
_paymentStatus = null; _type = null;
_createdFrom = _createdTo = null;
if (_dataGrid != null) await _dataGrid.ReloadServerData();
}
private async Task ExportCsv()
{
try
{
var result = await TransactionsClient.GetAllTransactionsByFilterAsync(new GetAllTransactionsByFilterRequest
{
PaginationState = new PaginationState { PageNumber = 1, PageSize = 5000 },
SortBy = "-Created",
Filter = BuildFilter()
});
var sb = new System.Text.StringBuilder();
sb.AppendLine("Id,Type,Amount,Description,Status,RefId,PaymentDate,Created");
foreach (var r in result.Models)
{
var type = r.HasType ? ReportLabels.GetTransactionTypeLabel(r.Type) : "";
var status = r.HasPaymentStatus ? ReportLabels.GetTransactionPaymentStatusLabel(r.PaymentStatus) : "";
var paymentDate = r.PaymentDate != null ? r.PaymentDate.ToDateTime().ToString("yyyy-MM-dd HH:mm") : "";
sb.AppendLine($"{r.Id},{type},{r.Amount},\"{r.Description}\",{status},{r.RefId},{paymentDate},{r.Created.ToDateTime():yyyy-MM-dd HH:mm}");
}
await jsRuntime.InvokeVoidAsync("jsSaveAsFile", "ledger-transactions.csv", sb.ToString());
}
catch (Exception ex) { Snackbar.Add($"خطا در export: {ex.Message}", Severity.Error); }
}
}
@@ -1,149 +0,0 @@
@page "/payment/manual-credit-charges"
@attribute [Authorize(Roles = "Administrator")]
@using BackOffice.Common.BaseComponents
@using BackOffice.Pages.AutoComplete
@using BackOffice.Pages.Payment.Components
@using CMSMicroservice.Protobuf.Protos
@using CMSMicroservice.Protobuf.Protos.UserWallet
@using DateTimeConverterCL
@using Google.Protobuf.WellKnownTypes
@inject UserWalletContract.UserWalletContractClient WalletClient
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
<Filters>
<MudItem xs="12">
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true">
شارژ دستی کیف پول اصلی (Balance) — بدون درگاه. جدا از «پرداخت‌های دستی» عضویت باشگاه.
</MudAlert>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<UserAutoComplete Label="جستجوی کاربر" @bind-SelectedUserId="_userIdFilter" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudTextField T="long?" Label="شناسه تراکنش" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_transactionIdFilter" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudTextField T="string" Label="RefId" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_refIdFilter" />
</MudItem>
<MudItem xs="12" sm="6" md="4">
<DateRangePicker @bind-From="_createdFrom" @bind-To="_createdTo" Label="بازه ثبت" />
</MudItem>
</Filters>
<Content>
<MudDataGrid @ref="_dataGrid" T="ManualCreditChargeModel"
ServerData="LoadData"
Hover="true" Dense="true"
Height="calc(100vh - 240px)">
<ToolBarContent>
<MudText Typo="Typo.subtitle1">شارژ دستی کیف پول اصلی</MudText>
<MudSpacer />
<MudButton Variant="Variant.Filled" Color="Color.Primary" Size="Size.Small"
StartIcon="@Icons.Material.Filled.Add" OnClick="OpenCreateDialog">
شارژ دستی جدید
</MudButton>
</ToolBarContent>
<Columns>
<PropertyColumn Property="x => x.TransactionId" Title="شناسه تراکنش" />
<PropertyColumn Property="x => x.UserId" Title="شناسه کاربر" />
<PropertyColumn Property="x => x.UserName" Title="نام کاربر" />
<PropertyColumn Property="x => x.Amount" Title="مبلغ">
<CellTemplate>
@context.Item.Amount.ToString("N0")
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.NewBalance" Title="موجودی بعد از شارژ">
<CellTemplate>
@context.Item.NewBalance.ToString("N0")
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.RefId" Title="RefId" />
<PropertyColumn Property="x => x.Description" Title="توضیحات" />
<TemplateColumn Title="تاریخ">
<CellTemplate>
@(context.Item.Created?.ToDateTime().ToLocalTime().MiladiToJalaliWithTime() ?? "-")
</CellTemplate>
</TemplateColumn>
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="ManualCreditChargeModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد در صفحه" />
</PagerContent>
</MudDataGrid>
</Content>
</BasePageComponent>
@code {
private BasePageComponent? _basePage;
private MudDataGrid<ManualCreditChargeModel>? _dataGrid;
private long? _userIdFilter;
private long? _transactionIdFilter;
private string? _refIdFilter;
private DateTime? _createdFrom;
private DateTime? _createdTo;
private async Task<GridData<ManualCreditChargeModel>> LoadData(GridState<ManualCreditChargeModel> state)
{
var request = new GetManualCreditChargesRequest
{
PaginationState = new PaginationState
{
PageNumber = state.Page + 1,
PageSize = state.PageSize
},
Filter = new GetManualCreditChargesFilter()
};
if (_userIdFilter.HasValue)
request.Filter.UserId = _userIdFilter.Value;
if (_transactionIdFilter.HasValue)
request.Filter.TransactionId = _transactionIdFilter.Value;
if (!string.IsNullOrWhiteSpace(_refIdFilter))
request.Filter.RefId = _refIdFilter.Trim();
if (_createdFrom.HasValue)
request.Filter.CreatedFrom = Timestamp.FromDateTime(DateTime.SpecifyKind(_createdFrom.Value, DateTimeKind.Utc));
if (_createdTo.HasValue)
request.Filter.CreatedTo = Timestamp.FromDateTime(DateTime.SpecifyKind(_createdTo.Value, DateTimeKind.Utc));
var response = await WalletClient.GetManualCreditChargesAsync(request);
return new GridData<ManualCreditChargeModel>
{
Items = response.Models.ToList(),
TotalItems = (int)(response.MetaData?.TotalCount ?? 0)
};
}
private async Task OnFilterSubmit()
{
if (_dataGrid != null)
await _dataGrid.ReloadServerData();
}
private async Task OnFilterCleared()
{
_userIdFilter = null;
_transactionIdFilter = null;
_refIdFilter = null;
_createdFrom = null;
_createdTo = null;
if (_dataGrid != null)
await _dataGrid.ReloadServerData();
}
private async Task OpenCreateDialog()
{
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true };
var dialog = DialogService.Show<ManualCreditChargeDialog>("شارژ دستی کیف پول اصلی", options);
var result = await dialog.Result;
if (result is { Canceled: false } && _dataGrid != null)
await _dataGrid.ReloadServerData();
}
}
@@ -6,12 +6,6 @@
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
<Filters>
@* ── G10: هشدار پرداخت دستی (Q28) ── *@
<MudItem xs="12">
<MudAlert Severity="Severity.Warning" Variant="Variant.Text" Dense="true" Icon="@Icons.Material.Filled.Warning">
مبلغ پرداخت باید مطابق قیمت پکیج انتخابی باشد. پکیج‌های مختلف قیمت متفاوتی دارند.
</MudAlert>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<UserAutoComplete Label="جستجوی کاربر" @bind-SelectedUserId="_userIdFilter" />
</MudItem>
@@ -32,8 +26,8 @@
<MudSelect T="int?" Clearable="true" Label="نوع" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_typeFilter">
<MudSelectItem T="int?" Value="@(1)">واریز نقدی</MudSelectItem>
<MudSelectItem T="int?" Value="@(2)">شارژ کیف پول اعتباری</MudSelectItem>
<MudSelectItem T="int?" Value="@(3)">شارژ پاداش های دریافتی</MudSelectItem>
<MudSelectItem T="int?" Value="@(2)">شارژ کیفپول تخفیف</MudSelectItem>
<MudSelectItem T="int?" Value="@(3)">شارژ کیف‌پول شبکه</MudSelectItem>
<MudSelectItem T="int?" Value="@(4)">تسویه حساب</MudSelectItem>
<MudSelectItem T="int?" Value="@(5)">اصلاح خطا</MudSelectItem>
<MudSelectItem T="int?" Value="@(6)">بازگشت وجه</MudSelectItem>
@@ -225,8 +219,8 @@
return type switch
{
1 => "واریز نقدی",
2 => "شارژ کیف پول اعتباری",
3 => "شارژ پاداش های دریافتی",
2 => "شارژ کیفپول تخفیف",
3 => "شارژ کیف‌پول شبکه",
4 => "تسویه حساب",
5 => "اصلاح خطا",
6 => "بازگشت وجه",
@@ -263,7 +257,7 @@
{
bool? confirm = await DialogService.ShowMessageBox(
"تایید پرداخت",
$"آیا از تایید پرداخت دستی #{model.Id} به مبلغ {model.Amount:N0} تومان مطمئن هستید؟",
$"آیا از تایید پرداخت دستی #{model.Id} به مبلغ {model.Amount:N0} ریال مطمئن هستید؟",
yesText: "تایید", cancelText: "لغو");
if (confirm == true)
{
@@ -37,7 +37,7 @@ public partial class ManualPayments
}
var sb = new StringBuilder();
sb.AppendLine("شناسه,شناسه کاربر,نام کاربر,پکیج,مبلغ,نوع,وضعیت,تاریخ ایجاد");
sb.AppendLine("شناسه,شناسه کاربر,نام کاربر,مبلغ,نوع,وضعیت,تاریخ ایجاد");
foreach (var m in response.Models)
{
@@ -45,7 +45,6 @@ public partial class ManualPayments
m.Id,
m.UserId,
EscapeCsv(m.UserFullName),
EscapeCsv(m.PackageName ?? "-"),
m.Amount.ToString("N0"),
EscapeCsv(GetTypeText((int)m.Type)),
EscapeCsv(GetStatusText((int)m.Status)),
+51 -172
View File
@@ -1,115 +1,78 @@
@page "/payment/transactions"
@using BackOffice.Common.BaseComponents
@using BackOffice.Pages.AutoComplete
@using CMSMicroservice.Protobuf.Protos.PaymentTransaction
@using Google.Protobuf.WellKnownTypes
@inject PaymentTransactionContract.PaymentTransactionContractClient PaymentTxClient
<BasePageComponent @ref="_basePage" OnSubmitClick="OnFilterSubmit" OnClearFilterClick="OnFilterCleared">
<Filters>
<MudItem xs="12">
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="true">
گزارش read-only تراکنش‌های درگاه پرداخت. برای ثبت پرداخت دستی به «پرداخت‌های دستی» مراجعه کنید.
</MudAlert>
<MudItem xs="12" sm="6" md="4">
<BackOffice.Common.BaseComponents.DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه تاریخی" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<UserAutoComplete Label="کاربر" @bind-SelectedUserId="_userId" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudTextField T="string" Label="موبایل" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_mobile" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudTextField T="string" Label="Authority" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_authority" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudTextField T="string" Label="RefId" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_refId" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudTextField T="string" Label="OrderId" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_orderId" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudTextField T="string" Label="درگاه" Variant="Variant.Outlined" Margin="Margin.Dense" @bind-Value="_gateway" />
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudSelect T="bool?" Clearable="true" Label="وضعیت پرداخت" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_paymentStatus">
<MudSelectItem T="bool?" Value="@((bool?)true)">موفق</MudSelectItem>
<MudSelectItem T="bool?" Value="@((bool?)false)">ناموفق / در انتظار</MudSelectItem>
<MudItem xs="12" sm="6" md="4">
<MudSelect T="int?" Clearable="true" Label="وضعیت پرداخت" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_statusFilter">
<MudSelectItem T="int?" Value="@(0)">در انتظار</MudSelectItem>
<MudSelectItem T="int?" Value="@(1)">موفق</MudSelectItem>
<MudSelectItem T="int?" Value="@(2)">ناموفق</MudSelectItem>
<MudSelectItem T="int?" Value="@(3)">بازگشت وجه</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<DateRangePicker @bind-From="_fromDate" @bind-To="_toDate" Label="بازه تاریخ" />
<MudSelect T="int?" Clearable="true" Label="نوع تراکنش" Variant="Variant.Outlined"
Margin="Margin.Dense" @bind-Value="_typeFilter">
<MudSelectItem T="int?" Value="@(0)">نامشخص</MudSelectItem>
<MudSelectItem T="int?" Value="@(1)">خرید پکیج</MudSelectItem>
<MudSelectItem T="int?" Value="@(2)">پرداخت دستی</MudSelectItem>
<MudSelectItem T="int?" Value="@(3)">شارژ کیف‌پول</MudSelectItem>
<MudSelectItem T="int?" Value="@(4)">بازگشت وجه</MudSelectItem>
</MudSelect>
</MudItem>
</Filters>
<Content>
<MudGrid Class="mb-3" Spacing="2">
<MudItem xs="6" sm="3">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">تعداد</MudText>
<MudText Typo="Typo.h6">@_summaryTotalCount.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6" sm="3">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">جمع مبلغ (تومان)</MudText>
<MudText Typo="Typo.h6">@_summaryTotalAmount.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6" sm="3">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">موفق</MudText>
<MudText Typo="Typo.h6" Color="Color.Success">@_summarySuccessful.ToString("N0")</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6" sm="3">
<MudPaper Class="pa-3" Outlined="true">
<MudText Typo="Typo.caption" Color="Color.Secondary">در انتظار</MudText>
<MudText Typo="Typo.h6" Color="Color.Warning">@_summaryPending.ToString("N0")</MudText>
</MudPaper>
</MudItem>
</MudGrid>
<MudDataGrid @ref="_dataGrid" T="PaymentTransactionModel"
ServerData="LoadData" ReadOnly="true"
Hover="true" Dense="true" Height="calc(100vh - 380px)">
<ToolBarContent>
<MudText Typo="Typo.subtitle1">گزارش تراکنش درگاه</MudText>
<MudSpacer />
<MudButton Variant="Variant.Outlined" Color="Color.Info" Size="Size.Small"
StartIcon="@Icons.Material.Filled.FileDownload" OnClick="ExportCsv">CSV</MudButton>
</ToolBarContent>
<MudDataGrid @ref="_dataGrid" T="TransactionModel"
ServerData="LoadData"
Hover="true" Dense="true"
Height="calc(100vh - 240px)">
<Columns>
<PropertyColumn Property="x => x.Id" Title="شناسه" />
<PropertyColumn Property="x => x.UserId" Title="کاربر" />
<PropertyColumn Property="x => x.Mobile" Title="موبایل" />
<PropertyColumn Property="x => x.GatewayProvider" Title="درگاه" />
<TemplateColumn Title="مبلغ (تومان)">
<CellTemplate>@context.Item.Amount.ToString("N0")</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.Authority" Title="Authority" />
<PropertyColumn Property="x => x.RefId" Title="RefId" />
<PropertyColumn Property="x => x.CardPan" Title="کارت" />
<PropertyColumn Property="x => x.RefId" Title="شماره مرجع" />
<PropertyColumn Property="x => x.Amount" Title="مبلغ">
<CellTemplate>
@context.Item.Amount.ToString("N0")
</CellTemplate>
</PropertyColumn>
<TemplateColumn Title="وضعیت">
<CellTemplate>
<MudChip T="string" Size="Size.Small" Color="@(context.Item.PaymentStatus ? Color.Success : Color.Warning)">
@ReportLabels.GetPaymentStatusLabel(context.Item.PaymentStatus)
<MudChip T="string"
Size="Size.Small"
Color="@GetStatusColor(context.Item.PaymentStatus)">
@GetStatusText(context.Item.PaymentStatus)
</MudChip>
</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.VerificationStatusCode" Title="کد Verify" />
<PropertyColumn Property="x => x.OrderId" Title="OrderId" />
<TemplateColumn Title="تاریخ">
<CellTemplate>@context.Item.CreatedAt.ToDateTime().ToLocalTime().MiladiToJalaliWithTime()</CellTemplate>
<TemplateColumn Title="تاریخ پرداخت">
<CellTemplate>
@context.Item.PaymentDate.ToLocalTime().MiladiToJalaliWithTime()
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="نوع">
<CellTemplate>
@GetTypeText(context.Item.Type)
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="عملیات">
<CellTemplate>
<MudTooltip Text="جزئیات">
<MudIconButton Icon="@Icons.Material.Filled.Info"
Size="Size.Small"
OnClick="@(() => OpenDetails(context.Item))" />
</MudTooltip>
</CellTemplate>
</TemplateColumn>
</Columns>
<NoRecordsContent>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">
با فیلتر فعلی موردی یافت نشد — بازه تاریخ یا کاربر را تغییر دهید.
</MudAlert>
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Text" Class="my-4">موردی یافت نشد.</MudAlert>
</NoRecordsContent>
<PagerContent>
<MudDataGridPager T="PaymentTransactionModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
<MudDataGridPager T="TransactionModel" PageSizeOptions="@(new int[] { 20, 50, 100 })"
InfoFormat="سطر {first_item} تا {last_item} از {all_items}"
RowsPerPageString="تعداد در صفحه" />
</PagerContent>
@@ -117,87 +80,3 @@
</Content>
</BasePageComponent>
@code {
private BasePageComponent? _basePage;
private MudDataGrid<PaymentTransactionModel>? _dataGrid;
private long? _userId;
private string? _mobile, _authority, _refId, _orderId, _gateway;
private bool? _paymentStatus;
private DateTime? _fromDate, _toDate;
private long _summaryTotalCount, _summaryTotalAmount, _summarySuccessful, _summaryPending;
private GetAllPaymentTransactionByFilterFilter BuildFilter() => new()
{
UserId = _userId > 0 ? _userId : null,
Mobile = string.IsNullOrWhiteSpace(_mobile) ? null : _mobile,
Authority = string.IsNullOrWhiteSpace(_authority) ? null : _authority,
RefId = string.IsNullOrWhiteSpace(_refId) ? null : _refId,
OrderId = string.IsNullOrWhiteSpace(_orderId) ? null : _orderId,
GatewayProvider = string.IsNullOrWhiteSpace(_gateway) ? null : _gateway,
PaymentStatus = _paymentStatus,
CreatedFrom = _fromDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_fromDate.Value, DateTimeKind.Utc)) : null,
CreatedTo = _toDate.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(_toDate.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Utc)) : null
};
private async Task LoadSummary()
{
try
{
var s = await PaymentTxClient.GetPaymentTransactionSummaryAsync(new GetPaymentTransactionSummaryRequest { Filter = BuildFilter() });
_summaryTotalCount = s.TotalCount;
_summaryTotalAmount = s.TotalAmount;
_summarySuccessful = s.SuccessfulCount;
_summaryPending = s.PendingCount;
}
catch { }
}
private async Task<GridData<PaymentTransactionModel>> LoadData(GridState<PaymentTransactionModel> state)
{
try
{
await LoadSummary();
var result = await PaymentTxClient.GetAllPaymentTransactionByFilterAsync(new GetAllPaymentTransactionByFilterRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = state.Page + 1, PageSize = state.PageSize },
Filter = BuildFilter()
});
return new GridData<PaymentTransactionModel>
{
Items = result?.Models?.ToList() ?? new(),
TotalItems = (int)(result?.MetaData?.TotalCount ?? 0)
};
}
catch (Exception ex)
{
Snackbar.Add($"خطا: {ex.Message}", Severity.Error);
return new GridData<PaymentTransactionModel>();
}
}
private async Task OnFilterSubmit() { if (_dataGrid != null) await _dataGrid.ReloadServerData(); }
private async Task OnFilterCleared()
{
_userId = null; _mobile = _authority = _refId = _orderId = _gateway = null;
_paymentStatus = null; _fromDate = _toDate = null;
if (_dataGrid != null) await _dataGrid.ReloadServerData();
}
private async Task ExportCsv()
{
try
{
var result = await PaymentTxClient.GetAllPaymentTransactionByFilterAsync(new GetAllPaymentTransactionByFilterRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState { PageNumber = 1, PageSize = 5000 },
Filter = BuildFilter()
});
var sb = new System.Text.StringBuilder();
sb.AppendLine("Id,UserId,Mobile,Gateway,Amount,Authority,RefId,Status,OrderId,Created");
foreach (var r in result.Models)
sb.AppendLine($"{r.Id},{r.UserId},{r.Mobile},{r.GatewayProvider},{r.Amount},{r.Authority},{r.RefId},{r.PaymentStatus},{r.OrderId},{r.CreatedAt.ToDateTime():yyyy-MM-dd HH:mm}");
await jsRuntime.InvokeVoidAsync("jsSaveAsFile", "payment-transactions.csv", sb.ToString());
}
catch (Exception ex) { Snackbar.Add($"خطا در export: {ex.Message}", Severity.Error); }
}
}
@@ -0,0 +1,95 @@
using BackOffice.Common.BaseComponents;
using MudBlazor;
namespace BackOffice.Pages.Payment;
public partial class Transactions
{
private BasePageComponent? _basePage;
private MudDataGrid<TransactionModel>? _dataGrid;
private DateTime? _fromDate;
private DateTime? _toDate;
private int? _statusFilter;
private int? _typeFilter;
private async Task<GridData<TransactionModel>> LoadData(GridState<TransactionModel> state)
{
// TODO: Connect to BackOffice.BFF Transactions when API is ready
await Task.CompletedTask;
return new GridData<TransactionModel>
{
Items = Array.Empty<TransactionModel>(),
TotalItems = 0
};
}
private async Task OnFilterSubmit()
{
if (_dataGrid != null)
await _dataGrid.ReloadServerData();
}
private async Task OnFilterCleared()
{
_fromDate = null;
_toDate = null;
_statusFilter = null;
_typeFilter = null;
if (_dataGrid != null)
await _dataGrid.ReloadServerData();
}
private void OpenDetails(TransactionModel model)
{
// TODO: Open TransactionDetailsDialog with model
}
private static string GetStatusText(int status)
{
return status switch
{
0 => "در انتظار",
1 => "موفق",
2 => "ناموفق",
3 => "بازگشت وجه",
_ => "نامشخص"
};
}
private static Color GetStatusColor(int status)
{
return status switch
{
0 => Color.Warning,
1 => Color.Success,
2 => Color.Error,
3 => Color.Info,
_ => Color.Default
};
}
private static string GetTypeText(int type)
{
return type switch
{
1 => "خرید پکیج",
2 => "پرداخت دستی",
3 => "شارژ کیف‌پول",
4 => "بازگشت وجه",
_ => "نامشخص"
};
}
private class TransactionModel
{
public long Id { get; set; }
public string? RefId { get; set; }
public long Amount { get; set; }
public int PaymentStatus { get; set; }
public DateTime PaymentDate { get; set; }
public int Type { get; set; }
}
}
+1 -1
View File
@@ -95,7 +95,7 @@
<MudNumericField T="long?"
HideSpinButtons="true"
Label="قیمت جدید (تومان)"
Label="قیمت جدید (ریال)"
@bind-Value="_newPrice"
Variant="Variant.Outlined"
Margin="Margin.Dense" />
@@ -51,7 +51,7 @@
<MudTextField T="string" @bind-Value="Model.Title" Disabled="_isLoading" Label="عنوان" Variant="Variant.Outlined" Margin="Margin.Dense" />
</MudItem>
<MudItem xs="6">
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.Price" Format="N0" Immediate="true" Disabled="_isLoading" Label="قیمت (تومان)" Variant="Variant.Outlined" Margin="Margin.Dense" />
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.Price" Format="N0" Immediate="true" Disabled="_isLoading" Label="قیمت (ریال)" Variant="Variant.Outlined" Margin="Margin.Dense" />
</MudItem>
</MudStack>
@@ -37,7 +37,7 @@
<MudTextField T="string" @bind-Value="Model.Title" Disabled="_isLoading" Label="عنوان" Variant="Variant.Outlined" Margin="Margin.Dense" />
</MudItem>
<MudItem xs="6">
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.Price" Format="N0" Immediate="true" Disabled="_isLoading" Label="قیمت (تومان)" Variant="Variant.Outlined" Margin="Margin.Dense" />
<MudNumericField T="long" HideSpinButtons="true" @bind-Value="Model.Price" Format="N0" Immediate="true" Disabled="_isLoading" Label="قیمت (ریال)" Variant="Variant.Outlined" Margin="Margin.Dense" />
</MudItem>
</MudStack>
@@ -128,7 +128,7 @@
<TemplateColumn Title="قیمت" CellStyle="text-wrap: nowrap;">
<CellTemplate>
@context.Item.Price.ToString("N0") تومان
@context.Item.Price.ToString("N0") ریال
</CellTemplate>
</TemplateColumn>
@@ -174,7 +174,8 @@
<MudItem xs="12">
<MudSwitch T="bool" @bind-Value="_networkConfig.SpilloverEnabled"
Color="Color.Primary"
Label="Spillover فعال باشد — اعضای اضافی به پایین درخت منتقل شوند" />
Label="Spillover فعال باشد"
HelperText="اعضای اضافی به پایین درخت منتقل شوند" />
</MudItem>
</MudGrid>
@@ -339,7 +340,8 @@
<MudItem xs="12">
<MudSwitch @bind-Value="_systemConfig.MaintenanceMode"
Color="Color.Warning"
Label="حالت تعمیر و نگهداری — غیرفعال کردن موقت سیستم برای به‌روزرسانی" />
Label="حالت تعمیر و نگهداری"
HelperText="غیرفعال کردن موقت سیستم برای به‌روزرسانی" />
</MudItem>
<MudItem xs="12">
+2 -17
View File
@@ -28,8 +28,6 @@
<col />
<col />
<col />
<col style="min-width: 180px;" />
<col />
<col style="width: 58px;" />
</ColGroup>
<ToolBarContent>
@@ -48,21 +46,8 @@
</CellTemplate>
</PropertyColumn>
<PropertyColumn Property="x => x.NationalCode" Title="کدملی" />
<TemplateColumn Title="آدرس پیش‌فرض">
<CellTemplate>
<MudText Typo="Typo.caption" Style="max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
Title="@(context.Item.DefaultAddress ?? "")">
@(string.IsNullOrWhiteSpace(context.Item.DefaultAddress) ? "-" : context.Item.DefaultAddress)
</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="کدپستی پیش‌فرض">
<CellTemplate>
<MudText Typo="Typo.caption">
@(string.IsNullOrWhiteSpace(context.Item.DefaultPostalCode) ? "-" : context.Item.DefaultPostalCode)
</MudText>
</CellTemplate>
</TemplateColumn>
<TemplateColumn StickyLeft="true" Title="عملیات" CellStyle="text-wrap: nowrap;" HeaderStyle="text-wrap: nowrap;">
<CellTemplate>
+32 -43
View File
@@ -1,10 +1,11 @@
using CMSMicroservice.Protobuf.Protos.User;
using BackOffice.Common.BaseComponents;
using BackOffice.Common.Utilities;
using BackOffice.Pages.User.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor;
using System.Text;
using DateTimeConverterCL;
using DataModel = CMSMicroservice.Protobuf.Protos.User.GetAllUserByFilterResponseModel;
namespace BackOffice.Pages.User;
@@ -83,63 +84,51 @@ public partial class UserMainPage
{
try
{
var filter = _request.Filter?.Clone() ?? new();
var all = new List<DataModel>();
const int pageSize = 200;
var page = 1;
while (page <= 500)
var exportRequest = new GetAllUserByFilterRequest
{
var result = await UserContract.GetAllUserByFilterAsync(new GetAllUserByFilterRequest
{
Filter = filter,
PaginationState = new() { PageNumber = page, PageSize = pageSize }
});
Filter = _request.Filter?.Clone() ?? new(),
PaginationState = new() { PageNumber = 1, PageSize = 1000 }
};
var batch = result?.Models?.ToList() ?? new();
if (batch.Count == 0)
break;
var result = await UserContract.GetAllUserByFilterAsync(exportRequest);
all.AddRange(batch);
var hasNext = result?.MetaData?.HasNext == true;
if (!hasNext || batch.Count < pageSize)
break;
page++;
}
if (all.Count == 0)
if (result?.Models == null || !result.Models.Any())
{
Snackbar.Add("داده‌ای برای خروجی وجود ندارد.", Severity.Info);
return;
}
var headers = new[]
{
"شناسه", "موبایل", "نام", "نام خانوادگی", "کدملی",
"آدرس پیش‌فرض", "کدپستی پیش‌فرض"
};
var sb = new StringBuilder();
sb.AppendLine("شناسه,موبایل,نام,نام خانوادگی,کدملی");
var rows = all.Select(u => new object?[]
foreach (var u in result.Models)
{
u.Id,
u.Mobile,
u.FirstName,
u.LastName,
u.NationalCode,
u.DefaultAddress,
u.DefaultPostalCode
});
sb.AppendLine(string.Join(",",
u.Id,
EscapeCsv(u.Mobile),
EscapeCsv(u.FirstName),
EscapeCsv(u.LastName),
EscapeCsv(u.NationalCode)));
}
var filename = $"users-{DateTime.Now:yyyyMMddHHmmss}.xls";
await JsRuntime.InvokeVoidAsync("jsSaveAsFile", filename,
CsvExportHelper.ToExcelXmlBase64("کاربران", headers, rows));
Snackbar.Add($"خروجی Excel آماده شد ({all.Count} کاربر).", Severity.Success);
var bytes = Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(sb.ToString())).ToArray();
var base64 = Convert.ToBase64String(bytes);
var filename = $"users-{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;
}
}
@@ -8,12 +8,12 @@
</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">
مبلغ تخفیف به‌صورت تومانی وارد شود.
مبلغ تخفیف به‌صورت ریالی وارد شود.
</MudText>
<MudNumericField T="long?"
@bind-Value="_discountAmount"
Label="مبلغ تخفیف (تومان)"
Label="مبلغ تخفیف (ریال)"
Variant="Variant.Outlined"
HideSpinButtons="true" />
@@ -40,7 +40,7 @@ public partial class ApplyDiscountDialog
if (response.Success)
{
Snackbar.Add($"تخفیف با موفقیت اعمال شد. مبلغ نهایی: {response.FinalAmount.ToString("N0")} تومان", Severity.Success);
Snackbar.Add($"تخفیف با موفقیت اعمال شد. مبلغ نهایی: {response.FinalAmount.ToString("N0")} ریال", Severity.Success);
MudDialog.Close(DialogResult.Ok(true));
}
else
@@ -27,8 +27,6 @@
<MudSelectItem T="int" Value="2">تحویل پست</MudSelectItem>
<MudSelectItem T="int" Value="3">تحویل به مشتری</MudSelectItem>
<MudSelectItem T="int" Value="4">مرجوع شده</MudSelectItem>
<MudSelectItem T="int" Value="5">لغو شده</MudSelectItem>
<MudSelectItem T="int" Value="6">آماده تحویل در دفتر</MudSelectItem>
</MudSelect>
</MudItem>
@@ -58,24 +56,6 @@
</MudItem>
}
@if (_newStatus == 5)
{
<MudItem xs="12">
<MudAlert Severity="Severity.Warning">
توجه: وضعیت «لغو شده» یعنی ارسال این سفارش متوقف شده است.
</MudAlert>
</MudItem>
}
@if (_newStatus == 6)
{
<MudItem xs="12">
<MudAlert Severity="Severity.Info">
سفارش برای تحویل حضوری در دفتر آماده است.
</MudAlert>
</MudItem>
}
@if (_newStatus == 2)
{
<MudItem xs="12">

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